Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a829447410 | ||
|
|
53e7ff4e16 | ||
|
|
4a85b4a4f2 | ||
|
|
d2e6dca3d3 | ||
|
|
cdbdc0af67 | ||
|
|
2af9b52213 | ||
|
|
894a3d1b32 | ||
|
|
82f0134368 | ||
|
|
e52ba46b0e | ||
|
|
6aca4954ef | ||
|
|
9d5fa4e2f1 | ||
|
|
c1db242edd | ||
|
|
91a2c63c88 | ||
|
|
f7d9bd7297 | ||
|
|
d94e880520 | ||
|
|
74e9053f85 | ||
|
|
ae5260d808 | ||
|
|
f72f8b7e2c | ||
|
|
43108eccde | ||
|
|
c93c9560e1 | ||
|
|
ddad1afd7b | ||
|
|
2b70ae3742 | ||
|
|
b110fd7932 | ||
|
|
cd537fb571 | ||
|
|
3cbd252980 | ||
|
|
0a99988e6e | ||
|
|
a19d20d973 | ||
|
|
b86eab9e04 | ||
|
|
b36530ff96 | ||
|
|
1019298060 | ||
|
|
99a2ad30ce | ||
|
|
9783a22abf | ||
|
|
68f63b3b90 | ||
|
|
1189d13c14 | ||
|
|
243a784480 | ||
|
|
c89b8c3bab | ||
|
|
b5172219af | ||
|
|
db90f85041 | ||
|
|
db9f90aa91 | ||
|
|
41b19582cc | ||
|
|
766102797b | ||
|
|
773cb1312b | ||
|
|
bb593020d0 | ||
|
|
33ffe62add | ||
|
|
6e1aabb2de | ||
|
|
ab3bdebc57 | ||
|
|
20d00e5ef2 | ||
|
|
e689c1cacb | ||
|
|
37c0737624 | ||
|
|
23bc448088 | ||
|
|
f1bdea98be | ||
|
|
9c00d1905f | ||
|
|
a31d737538 | ||
|
|
7535872222 | ||
|
|
7e7bf99fac | ||
|
|
ac5a7a3171 | ||
|
|
eda5da635d | ||
|
|
dc0cef5dbb | ||
|
|
24607c10ee | ||
|
|
8ddcfa8136 | ||
|
|
1f54043778 | ||
|
|
97bec94093 | ||
|
|
93df11e043 | ||
|
|
327b66c241 | ||
|
|
f3fd52fdbb | ||
|
|
f3727c6209 | ||
|
|
8a3281db03 | ||
|
|
19a21dca5a | ||
|
|
09dcad7427 | ||
|
|
172cbed9b5 | ||
|
|
f7e20a9ff6 | ||
|
|
4b213151d1 | ||
|
|
dd99efe692 | ||
|
|
ff303750cc | ||
|
|
ef6e1e0bf4 | ||
|
|
c0ed93132f | ||
|
|
3503537539 | ||
|
|
b9a3f33e97 | ||
|
|
c2f612d0b8 | ||
|
|
9f492ecec9 | ||
|
|
d87432cce3 | ||
|
|
cf34c91734 | ||
|
|
851b54fa47 | ||
|
|
c858a1800b | ||
|
|
f2405f1561 | ||
|
|
deee890647 | ||
|
|
ebabf0e100 | ||
|
|
e05d4693d2 | ||
|
|
77d49adc00 | ||
|
|
7dc45f0074 | ||
|
|
44d1da3c9c | ||
|
|
564ba4bd68 | ||
|
|
63df788908 | ||
|
|
fb463cbf98 | ||
|
|
cfd99efd48 | ||
|
|
3b2c5f8bf3 | ||
|
|
d08a276b67 | ||
|
|
23bc2cd39c | ||
|
|
a31eda8c68 | ||
|
|
d044ae51d1 | ||
|
|
552578afc3 | ||
|
|
00997db325 | ||
|
|
550464cf0b | ||
|
|
e43568a3d2 | ||
|
|
4869e8a093 | ||
|
|
d9b307ec87 | ||
|
|
3abfbef4c3 | ||
|
|
86aa1eb34f | ||
|
|
59edc9851a | ||
|
|
0e31dcef99 | ||
|
|
a67235ab8a | ||
|
|
eb88c9bdf8 | ||
|
|
77e4791cbe | ||
|
|
5d922dfae7 | ||
|
|
c704c84451 | ||
|
|
6f23488438 | ||
|
|
ca50abb290 | ||
|
|
15067114a2 | ||
|
|
645f14535b | ||
|
|
250a5b03d2 | ||
|
|
5ee060dfc6 | ||
|
|
f08502cde4 | ||
|
|
90ecb88249 | ||
|
|
16e60f74da | ||
|
|
0ab252cc7c | ||
|
|
ebadbaf30d | ||
|
|
7ce6b6817e | ||
|
|
6d4040d5ed | ||
|
|
4a7a4c0add | ||
|
|
883e53d8b5 | ||
|
|
a9e6b9ae5c | ||
|
|
a61ee1f29f | ||
|
|
eefdf60e31 | ||
|
|
19a633a756 | ||
|
|
e84c1a3ab8 | ||
|
|
4d41aa7e53 | ||
|
|
a4c28cc324 | ||
|
|
187f2e7b1d | ||
|
|
b355825295 | ||
|
|
33dc18356a | ||
|
|
c804d90f6a | ||
|
|
09aa547de1 | ||
|
|
57839eae5c | ||
|
|
a43f5f38a1 | ||
|
|
8220d07057 | ||
|
|
89ffe20363 | ||
|
|
9a3adf54b1 | ||
|
|
459129118e | ||
|
|
0862370ad6 | ||
|
|
1bddde99f0 | ||
|
|
fe2e7681da | ||
|
|
d209766f01 | ||
|
|
82bfe03dba | ||
|
|
15fc8e4c3c | ||
|
|
374dbf9716 | ||
|
|
6819988f90 | ||
|
|
e3079afb2f | ||
|
|
3b6689f44b | ||
|
|
fdf417e961 | ||
|
|
a751f1f0f1 | ||
|
|
5bf348b55e | ||
|
|
4ffadc8124 | ||
|
|
8069fb7ddd | ||
|
|
133fc90718 | ||
|
|
32a2a7956d | ||
|
|
01cc9832c1 | ||
|
|
6127ea6442 | ||
|
|
22fbc2a89d | ||
|
|
1ed2e230e0 | ||
|
|
6563716f21 | ||
|
|
487e0a77d6 | ||
|
|
c084d42b4a | ||
|
|
2d8cc2baa3 | ||
|
|
47630f7c5f | ||
|
|
082dfaf339 | ||
|
|
f8efbd51da | ||
|
|
22d98256ce | ||
|
|
00caa680f2 | ||
|
|
48d35eb637 | ||
|
|
61786fedfb | ||
|
|
1f3e3603be | ||
|
|
cee2c797c4 | ||
|
|
35668469f0 | ||
|
|
3cfe7df03e | ||
|
|
3301475810 | ||
|
|
04f703dec0 | ||
|
|
bc064bd65b | ||
|
|
917bb8931e | ||
|
|
150651334c | ||
|
|
858a72aaa1 | ||
|
|
22134f3917 | ||
|
|
62a0d87d77 | ||
|
|
e158039639 | ||
|
|
f62752dfa7 | ||
|
|
2214d7d0ee | ||
|
|
0c8a7e97ba | ||
|
|
ee39eef953 | ||
|
|
8d10e66744 | ||
|
|
92900672c8 | ||
|
|
e59808c1a1 | ||
|
|
704ea7b6ca | ||
|
|
8aac4dbccf | ||
|
|
78676b48a9 | ||
|
|
0077058241 | ||
|
|
343b4b9ff1 | ||
|
|
b09bc03837 | ||
|
|
b83e285a87 | ||
|
|
40fcede095 | ||
|
|
1ef3c7d328 | ||
|
|
30ebbf96f7 | ||
|
|
2c6a3c31c1 | ||
|
|
4a4fa811a2 | ||
|
|
381ef1d690 | ||
|
|
47686034a1 | ||
|
|
7258cfdba5 | ||
|
|
87611fc4f8 | ||
|
|
3886821091 | ||
|
|
523ec0dd1f | ||
|
|
071becb796 | ||
|
|
2fe920bbba | ||
|
|
509dc92539 | ||
|
|
1116783587 | ||
|
|
5b3e8bd9d1 | ||
|
|
daa3292814 | ||
|
|
6abea1a028 | ||
|
|
660a238731 | ||
|
|
1c24604228 | ||
|
|
ba50a6f45d | ||
|
|
b65b055630 | ||
|
|
c7c72efeac | ||
|
|
6489d2f27e | ||
|
|
2d8a239d6d | ||
|
|
71a1f81b4d | ||
|
|
8574d028c0 | ||
|
|
0be0628d33 | ||
|
|
782e119add | ||
|
|
9079ffcdb1 | ||
|
|
a7c1ad1b0d | ||
|
|
091757973f | ||
|
|
762ac24f47 | ||
|
|
6f8ac2b796 | ||
|
|
5157b3f599 | ||
|
|
760879a183 | ||
|
|
63e3dda268 | ||
|
|
284d38f3c3 | ||
|
|
cc72964343 | ||
|
|
de4984cef6 | ||
|
|
dba56de614 | ||
|
|
bb102ba986 | ||
|
|
199bbcd6ae | ||
|
|
c8ee036dd2 | ||
|
|
dfff42074d | ||
|
|
565893307e | ||
|
|
ff32270464 | ||
|
|
98a91830f2 | ||
|
|
f72707ead5 | ||
|
|
963cf945cb | ||
|
|
d5aa809ff9 | ||
|
|
1dd9a0e04e | ||
|
|
dbf16ba433 | ||
|
|
d421c1e106 | ||
|
|
50cf25d949 | ||
|
|
1aeec5329e | ||
|
|
80120b0455 | ||
|
|
3f5ee4664a | ||
|
|
659003d8e2 | ||
|
|
2f48b4b33e | ||
|
|
1830e0285a | ||
|
|
d31b8ebc8b | ||
|
|
acb431e77f | ||
|
|
bfd3665d70 | ||
|
|
a181eb13f5 | ||
|
|
43dd396a64 | ||
|
|
921d4fa1d3 | ||
|
|
91aec645ba | ||
|
|
c209e338d4 | ||
|
|
0ba3e4f9e1 | ||
|
|
253b0cc38b | ||
|
|
8c7c7a34e5 | ||
|
|
8d6b57ec21 | ||
|
|
8994a9c663 | ||
|
|
c048372928 | ||
|
|
0271582c0d | ||
|
|
a4b71e0805 | ||
|
|
66ac595850 | ||
|
|
d4f037f2a5 | ||
|
|
c1fe9e0ace | ||
|
|
a8a108e3fd | ||
|
|
0d1c6bb062 | ||
|
|
071ec219ce | ||
|
|
626eab5258 | ||
|
|
f683678224 | ||
|
|
2d88bdb27f | ||
|
|
0b9481b7c4 | ||
|
|
603bd2a740 | ||
|
|
576b7387be | ||
|
|
e6662a9b18 | ||
|
|
f2d2234c51 | ||
|
|
24f20bd54f | ||
|
|
bd6821347f | ||
|
|
15d4f5b262 | ||
|
|
eb2c1fc9fe | ||
|
|
b00e473e40 | ||
|
|
b70d554756 | ||
|
|
50d3fbfaf5 | ||
|
|
1fb58ea9fa | ||
|
|
01bce79e6f | ||
|
|
0b4388107e | ||
|
|
d8ef8aa560 | ||
|
|
373f6de51e | ||
|
|
58e87f57ec | ||
|
|
63ed992ef6 | ||
|
|
977f006a74 | ||
|
|
f81fe96997 | ||
|
|
d77b4a09e6 | ||
|
|
86ab1a0cf3 | ||
|
|
b3139ae853 | ||
|
|
c9c0a4c207 | ||
|
|
ead12b3838 | ||
|
|
2849954f71 | ||
|
|
06305385f7 | ||
|
|
e25c29e654 | ||
|
|
1a037e84e6 | ||
|
|
45f091eeee | ||
|
|
41a9e4f38c | ||
|
|
b50c7e4e80 | ||
|
|
74fbdac607 | ||
|
|
f86d24669e | ||
|
|
9cb9e64aba | ||
|
|
5866048be8 | ||
|
|
b18e75d5a4 | ||
|
|
0e433b5f5c | ||
|
|
6274cc442d | ||
|
|
f2e468893f | ||
|
|
f3817a4aa7 | ||
|
|
cb99998c53 | ||
|
|
f6a6f99954 | ||
|
|
248bd1ea88 | ||
|
|
13a79c4206 | ||
|
|
425e3ca0e4 | ||
|
|
d1c4eff4fc | ||
|
|
e6bd4e926d | ||
|
|
35422e9793 | ||
|
|
e106fef923 | ||
|
|
41ef404699 | ||
|
|
45e31c0b3f | ||
|
|
0df4c97819 | ||
|
|
77b448a7f7 | ||
|
|
4e520f7e52 | ||
|
|
df0f01eae2 | ||
|
|
82626a0d57 | ||
|
|
0c36c4166f | ||
|
|
57c6bcd5bb | ||
|
|
16d8665cd2 | ||
|
|
34373dc841 | ||
|
|
8410315920 | ||
|
|
78db8d339a | ||
|
|
39cd086024 | ||
|
|
82c379d273 | ||
|
|
20e27bb652 | ||
|
|
a7e74a8937 | ||
|
|
77f5a92901 | ||
|
|
6256bf0dd5 | ||
|
|
ecc5a58d2b | ||
|
|
2cccdceda7 | ||
|
|
f7993152b7 | ||
|
|
cf0bbfd93f | ||
|
|
803a19ff3b | ||
|
|
44e4010e15 | ||
|
|
5cbcf00595 | ||
|
|
49e75e15e5 | ||
|
|
1a3fcc0041 |
+2
-1
@@ -1,3 +1,4 @@
|
||||
[codespell]
|
||||
skip = .git,*.pdf,*.svg,package-lock.json,*.prisma
|
||||
ignore-words-list = afterall
|
||||
ignore-words-list = afterall,vertx
|
||||
|
||||
|
||||
@@ -16,8 +16,17 @@ NEXTAUTH_SECRET="secret"
|
||||
|
||||
# Langfuse experimental features
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
|
||||
|
||||
# Salt for API key hashing
|
||||
SALT="salt"
|
||||
|
||||
# Email
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
# S3 storage
|
||||
S3_ENDPOINT=
|
||||
S3_ACCESS_KEY_ID=
|
||||
S3_SECRET_ACCESS_KEY=
|
||||
S3_BUCKET_NAME=
|
||||
S3_REGION=
|
||||
+1
-8
@@ -1,21 +1,14 @@
|
||||
# When adding additional environment variables, the schema in "/src/env.mjs"
|
||||
# should be updated accordingly.
|
||||
|
||||
# Prisma
|
||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
||||
DIRECT_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
DATABASE_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
# Next Auth
|
||||
# You can generate a new secret on the command line with:
|
||||
# openssl rand -base64 32
|
||||
# https://next-auth.js.org/configuration/options#secret
|
||||
NEXTAUTH_SECRET="secret"
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# feature flag to enable experimental features locally
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
|
||||
SALT="salt"
|
||||
|
||||
|
||||
# Email
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
+67
-27
@@ -1,31 +1,72 @@
|
||||
# Assuming deployment on Vercel with Postgres database on Supabase
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="US"
|
||||
NEXTAUTH_COOKIE_DOMAIN=".langfuse.com"
|
||||
# More information: https://langfuse.com/docs/deployment/self-host
|
||||
|
||||
# When adding additional environment variables, the schema in "/src/env.mjs"
|
||||
# should be updated accordingly.
|
||||
|
||||
# Prisma
|
||||
|
||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
||||
DIRECT_URL="postgresql://postgres:[pw]@db.[db_id].supabase.co:5432/postgres"
|
||||
DATABASE_URL="postgres://postgres:[pw]@db.[db_id].supabase.co:6543/postgres?pgbouncer=true&connection_limit=1"
|
||||
# DATABASE_URL supports pooled connections, but then you need to set DIRECT_URL
|
||||
DATABASE_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
# DIRECT_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
# SHADOW_DATABASE_URL=
|
||||
|
||||
# Next Auth
|
||||
|
||||
# NEXTAUTH_URL does not need to be set when deploying on Vercel
|
||||
# NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# AUTH_REDIRECT_PROXY_URL used to proxy oauth callbacks on e.g. preview deployments. optional.
|
||||
# AUTH_REDIRECT_PROXY_URL="https://example.com/api/auth"
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# You can generate a new secret on the command line with:
|
||||
# openssl rand -base64 32
|
||||
# https://next-auth.js.org/configuration/options#secret
|
||||
NEXTAUTH_SECRET="secret"
|
||||
SALT="salt"
|
||||
# Sentry; set via Vercel integration
|
||||
# NEXT_PUBLIC_SENTRY_DSN=
|
||||
# NEXT_SENTRY_ORG=
|
||||
# NEXT_SENTRY_PROJECT=
|
||||
# SENTRY_AUTH_TOKEN=
|
||||
|
||||
# Docker only, optional
|
||||
# PORT=3000
|
||||
# HOSTNAME=localhost
|
||||
|
||||
# Default project, optional
|
||||
# LANGFUSE_DEFAULT_PROJECT_ID=
|
||||
# LANGFUSE_DEFAULT_PROJECT_ROLE=
|
||||
|
||||
# Enable experimental features, optional
|
||||
# NEXT_PUBLIC_ENABLE_EXPERIMENTAL_FEATURES
|
||||
|
||||
# Auth, optional configuration
|
||||
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
|
||||
# AUTH_DISABLE_USERNAME_PASSWORD=true
|
||||
|
||||
# SSO, each group is optional
|
||||
# AUTH_GOOGLE_CLIENT_ID=
|
||||
# AUTH_GOOGLE_CLIENT_SECRET=
|
||||
# AUTH_GITHUB_CLIENT_ID=
|
||||
# AUTH_GITHUB_CLIENT_SECRET=
|
||||
# AUTH_AZURE_AD_CLIENT_ID=
|
||||
# AUTH_AZURE_AD_CLIENT_SECRET=
|
||||
# AUTH_AZURE_AD_TENANT_ID=
|
||||
|
||||
# Transactional email, optional
|
||||
# Defines the email address to use as the from address.
|
||||
# EMAIL_FROM_ADDRESS=
|
||||
# Defines the connection url for smtp server.
|
||||
# SMTP_CONNECTION_URL=
|
||||
|
||||
# S3 storage, optional, used for exports from the UI
|
||||
# S3_ENDPOINT=
|
||||
# S3_ACCESS_KEY_ID=
|
||||
# S3_SECRET_ACCESS_KEY=
|
||||
# S3_BUCKET_NAME=
|
||||
# S3_REGION=
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### START Langfuse Cloud Config
|
||||
# Used for Langfuse Cloud deployments
|
||||
# Not recommended for self-hosted deployments as these are NOT COVERED BY SEMVER
|
||||
|
||||
# NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="US"
|
||||
# NEXTAUTH_COOKIE_DOMAIN=".langfuse.com"
|
||||
|
||||
# LANGFUSE_TEAM_SLACK_WEBHOOK=
|
||||
# LANGFUSE_NEW_USER_SIGNUP_WEBHOOK=
|
||||
@@ -34,17 +75,16 @@ SALT="salt"
|
||||
# NEXT_PUBLIC_POSTHOG_HOST=
|
||||
# NEXT_PUBLIC_POSTHOG_KEY=
|
||||
|
||||
# Id of demo project to automatically assign new users to
|
||||
# Sentry
|
||||
# NEXT_PUBLIC_SENTRY_DSN=
|
||||
# NEXT_SENTRY_ORG=
|
||||
# NEXT_SENTRY_PROJECT=
|
||||
# SENTRY_AUTH_TOKEN=
|
||||
|
||||
# Demo project that users can use to try the platform
|
||||
# NEXT_PUBLIC_DEMO_PROJECT_ID=
|
||||
|
||||
# Auth, each group is optional
|
||||
AUTH_GOOGLE_CLIENT_ID=
|
||||
AUTH_GOOGLE_CLIENT_SECRET=
|
||||
AUTH_GITHUB_CLIENT_ID=
|
||||
AUTH_GITHUB_CLIENT_SECRET=
|
||||
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
|
||||
# AUTH_DISABLE_USERNAME_PASSWORD=true
|
||||
# Crisp chat
|
||||
# NEXT_PUBLIC_CRISP_WEBSITE_ID=
|
||||
|
||||
# Email
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
### END Langfuse Cloud Config
|
||||
|
||||
+14
-2
@@ -6,12 +6,17 @@ const config = {
|
||||
overrides: [
|
||||
{
|
||||
extends: [
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/strict-type-checked",
|
||||
],
|
||||
files: ["*.ts", "*.tsx"],
|
||||
parserOptions: {
|
||||
project: path.join(__dirname, "tsconfig.json"),
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-confusing-void-expression": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
@@ -19,7 +24,8 @@ const config = {
|
||||
project: path.join(__dirname, "tsconfig.json"),
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
|
||||
extends: ["next/core-web-vitals"],
|
||||
ignorePatterns: ["generated/**/*"],
|
||||
rules: {
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"warn",
|
||||
@@ -29,6 +35,12 @@ const config = {
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
||||
"react/jsx-key": [
|
||||
"error",
|
||||
{
|
||||
warnOnDuplicates: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature or potential improvement
|
||||
description: Please describe the change as clear and concise as possible. Remember to add context as to why you believe this is needed.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the change here. If your idea is related to any issues or discussions, link them here.
|
||||
- type: checkboxes
|
||||
id: contribute
|
||||
attributes:
|
||||
label: Contribute
|
||||
description: Are you willing to contribute to the implementation of this idea?
|
||||
options:
|
||||
- label: Yes, I can implement this and raise a PR
|
||||
@@ -0,0 +1,7 @@
|
||||
contact_links:
|
||||
- name: 💡 Feature Request
|
||||
url: https://github.com/orgs/langfuse/discussions/new?category=ideas
|
||||
about: Suggest any ideas you have using our discussion forums.
|
||||
- name: 🤗 Get Help
|
||||
url: https://github.com/orgs/langfuse/discussions/new?category=support
|
||||
about: If you can’t get something to work the way you expect, open a question in our discussion forums.
|
||||
@@ -1,36 +0,0 @@
|
||||
# This template is heavily inspired by the Next.js's template:
|
||||
# See here: https://github.com/vercel/next.js/blob/canary/.github/ISSUE_TEMPLATE/3.feature_request.yml
|
||||
|
||||
name: 🛠 Feature Request
|
||||
description: Create a feature request for the core packages
|
||||
title: "feat: "
|
||||
labels: ["✨ enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to file a feature request. Please fill out this form as completely as possible.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature you'd like to request
|
||||
description: Please describe the feature as clear and concise as possible. Remember to add context as to why you believe this feature is needed.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like to see
|
||||
description: Please describe the solution you would like to see. Adding example usage is a good way to provide context.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here.
|
||||
- type: checkboxes
|
||||
id: contribute
|
||||
attributes:
|
||||
label: Contribute
|
||||
description: Are you willing to contribute to the implementation of this feature?
|
||||
options:
|
||||
- label: Yes, I can implement this and raise a PR
|
||||
|
||||
@@ -27,7 +27,7 @@ Fixes # (issue)
|
||||
|
||||
<!-- Remove bullet points below that don't apply to you -->
|
||||
|
||||
- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
|
||||
- I haven't read the [contributing guide](https://github.com/langfuse/langfuse/blob/main/CONTRIBUTING.md)
|
||||
- My code doesn't follow the style guidelines of this project (`npm run prettier`)
|
||||
- I haven't commented my code, particularly in hard-to-understand areas
|
||||
- I haven't checked if my PR needs changes to the documentation
|
||||
|
||||
@@ -18,6 +18,9 @@ updates:
|
||||
- dependency-name: "@types/node"
|
||||
- dependency-name: "@trpc/*"
|
||||
groups:
|
||||
sentry:
|
||||
patterns:
|
||||
- "@sentry/*"
|
||||
prisma:
|
||||
patterns:
|
||||
- "prisma"
|
||||
|
||||
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
merge_group:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
- "main"
|
||||
tags:
|
||||
- "v*"
|
||||
merge_group:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
@@ -29,12 +30,47 @@ jobs:
|
||||
- name: lint
|
||||
run: npm run lint
|
||||
|
||||
test-docker-build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
NEXTAUTH_SECRET: "secret"
|
||||
SALT: "salt"
|
||||
NEXTAUTH_URL: "http://localhost:3030"
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
steps:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
build-args: |
|
||||
DATABASE_URL=${{ env.DATABASE_URL }}
|
||||
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
|
||||
NEXTAUTH_URL=${{ env.NEXTAUTH_URL }}
|
||||
SALT=${{ env.SALT }}
|
||||
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18, 20]
|
||||
steps:
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@master
|
||||
with:
|
||||
swap-size-gb: 10
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
@@ -100,7 +136,7 @@ jobs:
|
||||
all-ci-passed:
|
||||
# This allows us to have a branch protection rule for tests and deploys with matrix
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, tests, e2e-tests]
|
||||
needs: [lint, tests, e2e-tests, test-docker-build]
|
||||
if: always()
|
||||
steps:
|
||||
- name: Successful deploy
|
||||
|
||||
Vendored
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
@@ -28,5 +28,6 @@
|
||||
"docwriter.style": "JSDoc",
|
||||
"[prisma]": {
|
||||
"editor.defaultFormatter": "Prisma.prisma"
|
||||
}
|
||||
},
|
||||
"eslint.lintTask.enable": true
|
||||
}
|
||||
|
||||
+81
-9
@@ -21,19 +21,86 @@ _Before making any significant changes, please [open an issue](https://github.co
|
||||
|
||||
Once we've discussed your changes and you've got your code ready, make sure that tests are passing and open your pull request.
|
||||
|
||||
## Getting started
|
||||
|
||||
A good first step is to search for open [issues](https://github.com/langfuse/langfuse/issues). Issues are labeled, and some good issues to start with are labeled: [good first issue](https://github.com/langfuse/langfuse/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
|
||||
|
||||
## Project Overview
|
||||
|
||||
### Technologies we use
|
||||
|
||||
- NextJS, pages router
|
||||
- NextAuth.js / Auth.js
|
||||
- TRPC
|
||||
- Prisma ORM
|
||||
- Zod
|
||||
- Tailwind CSS
|
||||
- shadcn/ui tailwind components (using Radix and tanstack)
|
||||
- Application (this repository)
|
||||
- NextJS 14, pages router
|
||||
- NextAuth.js / Auth.js
|
||||
- tRPC: Frontend APIs
|
||||
- Prisma ORM
|
||||
- Zod
|
||||
- Tailwind CSS
|
||||
- shadcn/ui tailwind components (using Radix and tanstack)
|
||||
- Fern: generate OpenAPI spec and Pydantic models
|
||||
- JS SDK ([langfuse/langfuse-js](https://github.com/langfuse/langfuse-js))
|
||||
- openapi-typescript to generated types based on OpenAPI spec
|
||||
- Python SDK ([langfuse/langfuse-python](https://github.com/langfuse/langfuse-python))
|
||||
- Pydantic for input validation, models generated by fern
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph s4["Clients"]
|
||||
subgraph s2["langfuse/langfuse-python"]
|
||||
Python["Python SDK"]
|
||||
OAI["OpenAI drop-in replacement"] -->|extends| Python
|
||||
LCPYTHON["Langchain Python Integration"] -->|extends| Python
|
||||
Langflow -->|uses| LCPYTHON
|
||||
LiteLLM -->|uses| Python
|
||||
end
|
||||
subgraph s3["langfuse/langfuse-js"]
|
||||
JS["JS SDK"]
|
||||
LCJS["Langchain JS Integration"] -->|extends| JS
|
||||
Flowise -->|uses| LCJS
|
||||
end
|
||||
end
|
||||
|
||||
DB[Postgres Database]
|
||||
subgraph s1["Application (langfuse/langfuse)"]
|
||||
API[Public HTTP API]
|
||||
G[TRPC API]
|
||||
I[NextAuth]
|
||||
H[React Frontend]
|
||||
Prisma[Prisma ORM]
|
||||
H --> G
|
||||
H --> I
|
||||
G --> I
|
||||
G --- Prisma
|
||||
API --- Prisma
|
||||
I --- Prisma
|
||||
end
|
||||
Prisma --- DB
|
||||
JS --- API
|
||||
Python --- API
|
||||
```
|
||||
|
||||
### Database Overview
|
||||
|
||||
The diagram below may not show all relationships if the foreign key is not defined in the database schema. For instance, `trace_id` in the `observation` table is not defined as a foreign key to the `trace` table to allow unordered ingestion of these objects, but it is still a foreign key in the application code.
|
||||
|
||||
Full database schema: [prisma/schema.prisma](prisma/schema.prisma)
|
||||
|
||||
<img src="./prisma/database.svg">
|
||||
|
||||
### Infrastructure & Network Overview
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Browser ---|Web UI & TRPC API| App
|
||||
Integrations/SDKs ---|Public HTTP API| App
|
||||
subgraph i1["Application Network"]
|
||||
App["Langfuse Application (Docker or Serverless)"]
|
||||
end
|
||||
subgraph i2["Database Network"]
|
||||
DB["Postgres Database"]
|
||||
end
|
||||
App --- DB
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
|
||||
@@ -79,6 +146,9 @@ Requirements
|
||||
npm run dev
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If you frequently switch branches, use `npm run dx` instead of `npm run dev`. This command will install dependencies, reset the database (wipe and apply all migrations), and run the database seeder with example data before starting the development server.
|
||||
|
||||
## Commit messages
|
||||
|
||||
On the main branch, we adhere to the best practices of [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). All pull requests and branches are squash-merged to maintain a clean and readable history. This approach ensures the addition of a conventional commit message when merging contributions.
|
||||
@@ -136,3 +206,5 @@ When a new release is tagged on the `main` branch (excluding prereleases), it tr
|
||||
## License
|
||||
|
||||
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
When contributing to the Langfuse codebase, you need to agree to the [Contributor License Agreement](https://cla-assistant.io/langfuse/langfuse). You only need to do this once and the CLA bot will remind you if you haven't signed it yet.
|
||||
|
||||
@@ -13,6 +13,14 @@
|
||||
</div>
|
||||
</br>
|
||||
<div>
|
||||
<a href="https://cloud.langfuse.com">
|
||||
<strong>Langfuse Cloud »</strong>
|
||||
</a>
|
||||
</br>
|
||||
<a href="https://langfuse.com/docs/deployment/self-host">
|
||||
<strong>Self Host Langfuse »</strong>
|
||||
</a>
|
||||
</br>
|
||||
<a href="https://discord.gg/7NXusRtqYU">
|
||||
<strong>Join the Langfuse Discord »</strong>
|
||||
</a>
|
||||
@@ -26,11 +34,12 @@
|
||||
<a href="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+">
|
||||
<strong>Report Bug</strong>
|
||||
</a> ·
|
||||
<a href="https://github.com/langfuse/langfuse/issues/new?assignees=&labels=%E2%9C%A8+enhancement&projects=&template=feature_request.yml&title=feat%3A+">
|
||||
<a href="https://github.com/orgs/langfuse/discussions/new?category=ideas">
|
||||
<strong>Feature Request</strong>
|
||||
</a>
|
||||
</div>
|
||||
</br>
|
||||
</br>
|
||||
<div>
|
||||
<img src="https://img.shields.io/badge/License-MIT-red.svg?style=flat-square" alt="MIT License">
|
||||
<a href="https://discord.gg/7NXusRtqYU"><img src="https://img.shields.io/discord/1111061815649124414?style=flat-square&logo=Discord&logoColor=white&label=Discord&color=%23434EE4" alt="Discord"></a>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
## Security Policy
|
||||
We strongly recommend using the latest version of Langfuse to receive all security updates.
|
||||
|
||||
For more information, please refer to the [Data Security & Privacy](https://langfuse.com/docs/data-security-privacy) page in the documentation or contact security@langfuse.com.
|
||||
+17
-2
@@ -1,7 +1,22 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Set DIRECT_URL to the value of DATABASE_URL, required for migrations
|
||||
export DIRECT_URL=$DATABASE_URL
|
||||
# Check if DATABASE_URL is not set
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
# Check if all required variables are provided
|
||||
if [ -n "$DATABASE_HOST" ] && [ -n "$DATABASE_USERNAME" ] && [ -n "$DATABASE_PASSWORD" ] && [ -n "$DATABASE_NAME" ]; then
|
||||
# Construct DATABASE_URL from the provided variables
|
||||
DATABASE_URL="postgresql://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@${DATABASE_HOST}/${DATABASE_NAME}"
|
||||
export DATABASE_URL
|
||||
else
|
||||
echo "Error: Required database environment variables are not set. Provide a postgres url for DATABASE_URL."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set DIRECT_URL to the value of DATABASE_URL if it is not set, required for migrations
|
||||
if [ -z "$DIRECT_URL" ]; then
|
||||
export DIRECT_URL=$DATABASE_URL
|
||||
fi
|
||||
|
||||
# Apply migrations
|
||||
prisma migrate deploy
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
name: langfuse
|
||||
docs: |
|
||||
## Authentication
|
||||
Authenticate with the API using Basic Auth, get API keys in the project settings:
|
||||
|
||||
- username: Langfuse Public Key
|
||||
- password: Langfuse Secret Key
|
||||
|
||||
error-discrimination:
|
||||
strategy: status-code
|
||||
auth: basic
|
||||
|
||||
@@ -1,32 +1,4 @@
|
||||
types:
|
||||
# Create Bodies
|
||||
CreateEventRequest:
|
||||
properties:
|
||||
id: optional<string>
|
||||
traceId: optional<string>
|
||||
name: optional<string>
|
||||
startTime: optional<datetime>
|
||||
metadata: optional<unknown>
|
||||
input: optional<unknown>
|
||||
output: optional<unknown>
|
||||
level: optional<ObservationLevel>
|
||||
statusMessage: optional<string>
|
||||
parentObservationId: optional<string>
|
||||
version: optional<string>
|
||||
CreateSpanRequest:
|
||||
extends: CreateEventRequest
|
||||
properties:
|
||||
endTime: optional<datetime>
|
||||
CreateGenerationRequest:
|
||||
extends: CreateSpanRequest
|
||||
properties:
|
||||
completionStartTime: optional<datetime>
|
||||
model: optional<string>
|
||||
modelParameters: optional<map<string, MapValue>>
|
||||
prompt: optional<unknown>
|
||||
completion: optional<unknown>
|
||||
usage: optional<LLMUsage>
|
||||
|
||||
# Objects
|
||||
Trace:
|
||||
properties:
|
||||
@@ -35,10 +7,14 @@ types:
|
||||
docs: The unique identifier of a trace
|
||||
timestamp: datetime
|
||||
name: optional<string>
|
||||
input: optional<unknown>
|
||||
output: optional<unknown>
|
||||
sessionId: optional<string>
|
||||
release: optional<string>
|
||||
version: optional<string>
|
||||
userId: optional<string>
|
||||
metadata: optional<unknown>
|
||||
tags: optional<list<string>>
|
||||
public:
|
||||
type: optional<boolean>
|
||||
docs: Public traces are accessible via url without login
|
||||
@@ -54,8 +30,17 @@ types:
|
||||
TraceWithFullDetails:
|
||||
extends: Trace
|
||||
properties:
|
||||
observations: list<Observation>
|
||||
observations: list<ObservationsView>
|
||||
scores: list<Score>
|
||||
Session:
|
||||
properties:
|
||||
id: string
|
||||
createdAt: datetime
|
||||
projectId: string
|
||||
SessionWithTraces:
|
||||
extends: Session
|
||||
properties:
|
||||
traces: list<Trace>
|
||||
Observation:
|
||||
properties:
|
||||
id: string
|
||||
@@ -71,12 +56,32 @@ types:
|
||||
version: optional<string>
|
||||
metadata: optional<unknown>
|
||||
output: optional<unknown>
|
||||
promptTokens: integer
|
||||
completionTokens: integer
|
||||
totalTokens: integer
|
||||
usage: optional<Usage>
|
||||
level: ObservationLevel
|
||||
statusMessage: optional<string>
|
||||
parentObservationId: optional<string>
|
||||
promptId: optional<string>
|
||||
|
||||
ObservationsView:
|
||||
extends: Observation
|
||||
properties:
|
||||
modelId: optional<string>
|
||||
inputPrice: optional<double>
|
||||
outputPrice: optional<double>
|
||||
totalPrice: optional<double>
|
||||
calculatedInputCost: optional<double>
|
||||
calculatedOutputCost: optional<double>
|
||||
calculatedTotalCost: optional<double>
|
||||
|
||||
Usage:
|
||||
properties:
|
||||
input: optional<integer>
|
||||
output: optional<integer>
|
||||
total: optional<integer>
|
||||
unit: optional<ModelUsageUnit>
|
||||
inputCost: optional<double>
|
||||
outputCost: optional<double>
|
||||
totalCost: optional<double>
|
||||
Score:
|
||||
properties:
|
||||
id: string
|
||||
@@ -90,7 +95,6 @@ types:
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
status: DatasetStatus
|
||||
projectId: string
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
@@ -124,6 +128,13 @@ types:
|
||||
datasetRunItems: list<DatasetRunItem>
|
||||
|
||||
# Utilities
|
||||
ModelUsageUnit:
|
||||
enum:
|
||||
- CHARACTERS
|
||||
- TOKENS
|
||||
- MILLISECONDS
|
||||
- SECONDS
|
||||
- IMAGES
|
||||
ObservationLevel:
|
||||
enum:
|
||||
- DEBUG
|
||||
@@ -136,11 +147,7 @@ types:
|
||||
- optional<string>
|
||||
- optional<integer>
|
||||
- optional<boolean>
|
||||
LLMUsage:
|
||||
properties:
|
||||
promptTokens: optional<integer>
|
||||
completionTokens: optional<integer>
|
||||
totalTokens: optional<integer>
|
||||
- optional<list<string>>
|
||||
DatasetStatus:
|
||||
enum:
|
||||
- ACTIVE
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
create:
|
||||
docs: Add an event to the database
|
||||
method: POST
|
||||
path: /events
|
||||
request: commons.CreateEventRequest
|
||||
response: commons.Observation
|
||||
@@ -1,37 +0,0 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
pagination: ./utils/pagination.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
log:
|
||||
method: POST
|
||||
path: /generations
|
||||
request: commons.CreateGenerationRequest
|
||||
response: commons.Observation
|
||||
update:
|
||||
method: PATCH
|
||||
path: /generations
|
||||
request: UpdateGenerationRequest
|
||||
response: commons.Observation
|
||||
|
||||
types:
|
||||
UpdateGenerationRequest:
|
||||
properties:
|
||||
generationId: string
|
||||
traceId: optional<string>
|
||||
name: optional<string>
|
||||
startTime: optional<datetime>
|
||||
endTime: optional<datetime>
|
||||
completionStartTime: optional<datetime>
|
||||
model: optional<string>
|
||||
modelParameters: optional<map<string, commons.MapValue>>
|
||||
prompt: optional<unknown>
|
||||
version: optional<string>
|
||||
metadata: optional<unknown>
|
||||
completion: optional<unknown>
|
||||
usage: optional<commons.LLMUsage>
|
||||
level: optional<commons.ObservationLevel>
|
||||
statusMessage: optional<string>
|
||||
@@ -7,9 +7,22 @@ service:
|
||||
docs: Check health of API and database
|
||||
method: GET
|
||||
path: /health
|
||||
response: HealthResponse
|
||||
errors:
|
||||
- ServiceUnavailableError
|
||||
|
||||
types:
|
||||
HealthResponse:
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
docs: Langfuse server version
|
||||
status: string
|
||||
examples:
|
||||
- value:
|
||||
version: 1.25.0
|
||||
status: OK
|
||||
|
||||
errors:
|
||||
ServiceUnavailableError:
|
||||
status-code: 503
|
||||
|
||||
@@ -15,7 +15,7 @@ service:
|
||||
body:
|
||||
properties:
|
||||
batch: list<IngestionEvent>
|
||||
response: commons.Score
|
||||
response: IngestionResponse # will be reportet as 200 response, but endpoint returns 207
|
||||
|
||||
types:
|
||||
IngestionEvent:
|
||||
@@ -23,26 +23,206 @@ types:
|
||||
union:
|
||||
trace-create: TraceEvent
|
||||
score-create: ScoreEvent
|
||||
observation-create: ObservationCreateEvent
|
||||
observation-update: ObservationUpdateEvent
|
||||
event-create: CreateEventEvent
|
||||
generation-create: CreateGenerationEvent
|
||||
generation-update: UpdateGenerationEvent
|
||||
span-create: CreateSpanEvent
|
||||
span-update: UpdateSpanEvent
|
||||
sdk-log: SDKLogEvent
|
||||
|
||||
# both are legacy
|
||||
observation-create: CreateObservationEvent
|
||||
observation-update: UpdateObservationEvent
|
||||
|
||||
ObservationType:
|
||||
enum:
|
||||
- SPAN
|
||||
- GENERATION
|
||||
- EVENT
|
||||
|
||||
IngestionUsage:
|
||||
discriminated: false
|
||||
union:
|
||||
- commons.Usage
|
||||
- OpenAIUsage
|
||||
|
||||
OpenAIUsage:
|
||||
properties:
|
||||
promptTokens: optional<integer>
|
||||
completionTokens: optional<integer>
|
||||
totalTokens: optional<integer>
|
||||
|
||||
OptionalObservationBody:
|
||||
properties:
|
||||
traceId: optional<string>
|
||||
name: optional<string>
|
||||
startTime: optional<datetime>
|
||||
metadata: optional<unknown>
|
||||
input: optional<unknown>
|
||||
output: optional<unknown>
|
||||
level: optional<commons.ObservationLevel>
|
||||
statusMessage: optional<string>
|
||||
parentObservationId: optional<string>
|
||||
version: optional<string>
|
||||
|
||||
CreateEventBody:
|
||||
extends: OptionalObservationBody
|
||||
properties:
|
||||
id: optional<string>
|
||||
|
||||
UpdateEventBody:
|
||||
extends: OptionalObservationBody
|
||||
properties:
|
||||
id: string
|
||||
|
||||
CreateSpanBody:
|
||||
extends: CreateEventBody
|
||||
properties:
|
||||
endTime: optional<datetime>
|
||||
|
||||
UpdateSpanBody:
|
||||
extends: UpdateEventBody
|
||||
properties:
|
||||
endTime: optional<datetime>
|
||||
|
||||
CreateGenerationBody:
|
||||
extends: CreateSpanBody
|
||||
properties:
|
||||
completionStartTime: optional<datetime>
|
||||
model: optional<string>
|
||||
modelParameters: optional<map<string, commons.MapValue>>
|
||||
usage: optional<IngestionUsage>
|
||||
promptName: optional<string>
|
||||
promptVersion: optional<integer>
|
||||
|
||||
UpdateGenerationBody:
|
||||
extends: UpdateSpanBody
|
||||
properties:
|
||||
completionStartTime: optional<datetime>
|
||||
model: optional<string>
|
||||
modelParameters: optional<map<string, commons.MapValue>>
|
||||
usage: optional<IngestionUsage>
|
||||
promptName: optional<string>
|
||||
promptVersion: optional<integer>
|
||||
|
||||
ObservationBody:
|
||||
properties:
|
||||
id: optional<string>
|
||||
traceId: optional<string>
|
||||
type: ObservationType
|
||||
name: optional<string>
|
||||
startTime: optional<datetime>
|
||||
endTime: optional<datetime>
|
||||
completionStartTime: optional<datetime>
|
||||
model: optional<string>
|
||||
modelParameters: optional<map<string, commons.MapValue>>
|
||||
input: optional<unknown>
|
||||
version: optional<string>
|
||||
metadata: optional<unknown>
|
||||
output: optional<unknown>
|
||||
usage: optional<commons.Usage>
|
||||
level: optional<commons.ObservationLevel>
|
||||
statusMessage: optional<string>
|
||||
parentObservationId: optional<string>
|
||||
|
||||
TraceBody:
|
||||
properties:
|
||||
id: optional<string>
|
||||
name: optional<string>
|
||||
userId: optional<string>
|
||||
input: optional<unknown>
|
||||
output: optional<unknown>
|
||||
sessionId: optional<string>
|
||||
release: optional<string>
|
||||
version: optional<string>
|
||||
metadata: optional<unknown>
|
||||
tags: optional<list<string>>
|
||||
public:
|
||||
type: optional<boolean>
|
||||
docs: Make trace publicly accessible via url
|
||||
|
||||
SDKLogBody:
|
||||
properties:
|
||||
log: unknown
|
||||
|
||||
ScoreBody:
|
||||
properties:
|
||||
id: optional<string>
|
||||
traceId: string
|
||||
name: string
|
||||
value: double
|
||||
observationId: optional<string>
|
||||
comment: optional<string>
|
||||
|
||||
BaseEvent:
|
||||
properties:
|
||||
id: string
|
||||
timestamp: string
|
||||
metadata: unknown
|
||||
|
||||
TraceEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
id: string
|
||||
timestamp: string
|
||||
body: commons.Trace
|
||||
ObservationCreateEvent:
|
||||
body: TraceBody
|
||||
|
||||
CreateObservationEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
id: string
|
||||
timestamp: string
|
||||
body: commons.Observation
|
||||
ObservationUpdateEvent:
|
||||
body: ObservationBody
|
||||
|
||||
UpdateObservationEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
id: string
|
||||
timestamp: string
|
||||
body: commons.Observation
|
||||
body: ObservationBody
|
||||
|
||||
ScoreEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
body: ScoreBody
|
||||
|
||||
SDKLogEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
body: SDKLogBody
|
||||
|
||||
CreateGenerationEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
body: CreateGenerationBody
|
||||
|
||||
UpdateGenerationEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
body: UpdateGenerationBody
|
||||
|
||||
CreateSpanEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
body: CreateSpanBody
|
||||
|
||||
UpdateSpanEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
body: UpdateSpanBody
|
||||
|
||||
CreateEventEvent:
|
||||
extends: BaseEvent
|
||||
properties:
|
||||
body: CreateEventBody
|
||||
|
||||
IngestionSuccess:
|
||||
properties:
|
||||
id: string
|
||||
timestamp: string
|
||||
body: commons.Score
|
||||
status: integer
|
||||
|
||||
IngestionError:
|
||||
properties:
|
||||
id: string
|
||||
status: integer
|
||||
message: optional<string>
|
||||
error: optional<unknown>
|
||||
|
||||
IngestionResponse:
|
||||
properties:
|
||||
successes: list<IngestionSuccess>
|
||||
errors: list<IngestionError>
|
||||
|
||||
@@ -14,7 +14,7 @@ service:
|
||||
observationId:
|
||||
type: string
|
||||
docs: The unique langfuse identifier of an observation, can be an event, span or generation
|
||||
response: commons.Observation
|
||||
response: commons.ObservationsView
|
||||
getMany:
|
||||
docs: Get a list of observations
|
||||
method: GET
|
||||
@@ -29,10 +29,15 @@ service:
|
||||
type: optional<string>
|
||||
traceId: optional<string>
|
||||
parentObservationId: optional<string>
|
||||
response: Observations
|
||||
response: ObservationsViews
|
||||
|
||||
types:
|
||||
Observations:
|
||||
properties:
|
||||
data: list<commons.Observation>
|
||||
meta: pagination.MetaResponse
|
||||
|
||||
ObservationsViews:
|
||||
properties:
|
||||
data: list<commons.ObservationsView>
|
||||
meta: pagination.MetaResponse
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
pagination: ./utils/pagination.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
get:
|
||||
docs: Get a specific prompt
|
||||
method: GET
|
||||
path: /prompts
|
||||
request:
|
||||
name: GetParameterRequest
|
||||
query-parameters:
|
||||
name: string
|
||||
version: optional<integer>
|
||||
response: Prompt
|
||||
create:
|
||||
docs: Create a specific prompt
|
||||
method: POST
|
||||
path: /prompts
|
||||
request: CreatePromptRequest
|
||||
response: Prompt
|
||||
|
||||
types:
|
||||
CreatePromptRequest:
|
||||
properties:
|
||||
name: string
|
||||
isActive: boolean
|
||||
prompt: string
|
||||
Prompt:
|
||||
properties:
|
||||
name: string
|
||||
version: integer
|
||||
prompt: string
|
||||
@@ -0,0 +1,17 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
pagination: ./utils/pagination.yml
|
||||
commons: ./commons.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
get:
|
||||
docs: Get a session
|
||||
method: GET
|
||||
path: /sessions/{sessionId}
|
||||
path-parameters:
|
||||
sessionId:
|
||||
type: string
|
||||
docs: The unique id of a session
|
||||
response: commons.SessionWithTraces
|
||||
@@ -1,34 +0,0 @@
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json
|
||||
imports:
|
||||
commons: ./commons.yml
|
||||
service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
create:
|
||||
docs: Add a span to the database
|
||||
method: POST
|
||||
path: /spans
|
||||
request: commons.CreateSpanRequest
|
||||
response: commons.Observation
|
||||
update:
|
||||
docs: Update a span to the database
|
||||
method: PATCH
|
||||
path: /spans
|
||||
request: UpdateSpanRequest
|
||||
response: commons.Observation
|
||||
|
||||
types:
|
||||
UpdateSpanRequest:
|
||||
properties:
|
||||
spanId: string
|
||||
traceId: optional<string>
|
||||
startTime: optional<datetime>
|
||||
endTime: optional<datetime>
|
||||
name: optional<string>
|
||||
metadata: optional<unknown>
|
||||
input: optional<unknown>
|
||||
output: optional<unknown>
|
||||
level: optional<commons.ObservationLevel>
|
||||
version: optional<string>
|
||||
statusMessage: optional<string>
|
||||
@@ -6,12 +6,6 @@ service:
|
||||
auth: true
|
||||
base-path: /api/public
|
||||
endpoints:
|
||||
create:
|
||||
docs: Add a trace to the database
|
||||
method: POST
|
||||
path: /traces
|
||||
request: CreateTraceRequest
|
||||
response: commons.Trace
|
||||
get:
|
||||
docs: Get a specific trace
|
||||
method: GET
|
||||
@@ -32,22 +26,20 @@ service:
|
||||
limit: optional<integer>
|
||||
userId: optional<string>
|
||||
name: optional<string>
|
||||
orderBy:
|
||||
type: string
|
||||
docs: Format of the string sort_by=timestamp.asc (id, timestamp, name, userId, release, version, public, bookmarked, sessionId)
|
||||
tags:
|
||||
type: optional<string>
|
||||
allow-multiple: true
|
||||
docs: Only traces that include all of these tags will be returned.
|
||||
response: Traces
|
||||
|
||||
types:
|
||||
CreateTraceRequest:
|
||||
properties:
|
||||
id: optional<string>
|
||||
name: optional<string>
|
||||
userId: optional<string>
|
||||
externalId: optional<string>
|
||||
release: optional<string>
|
||||
version: optional<string>
|
||||
metadata: optional<unknown>
|
||||
public:
|
||||
type: optional<boolean>
|
||||
docs: Make trace publicly accessible via url
|
||||
Traces:
|
||||
properties:
|
||||
data: list<commons.TraceWithDetails>
|
||||
meta: pagination.MetaResponse
|
||||
Sort:
|
||||
properties:
|
||||
id: string
|
||||
|
||||
@@ -11,13 +11,14 @@ groups:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
- name: fernapi/fern-python-sdk
|
||||
version: 0.6.6
|
||||
version: 0.7.3
|
||||
output:
|
||||
location: local-file-system
|
||||
path: ../../../generated/python
|
||||
config:
|
||||
namespaceExport: Langfuse
|
||||
allowCustomFetcher: true
|
||||
client_class_name: FernLangfuse
|
||||
pydantic_config:
|
||||
require_optional_fields: false
|
||||
- name: fernapi/fern-typescript-node-sdk
|
||||
version: 0.7.1
|
||||
output:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"organization": "finto",
|
||||
"version": "0.15.3"
|
||||
"version": "0.16.36"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+104
-198
@@ -2,7 +2,7 @@
|
||||
"info": {
|
||||
"name": "Langfuse",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||
"description": null
|
||||
"description": "## Authentication\n\nAuthenticate with the API using Basic Auth, get API keys in the project settings:\n\n- username: Langfuse Public Key\n- password: Langfuse Secret Key"
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
@@ -260,119 +260,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Event",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"description": "Add an event to the database",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/events",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"events"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "POST",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"id\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"startTime\": \"1994-11-05T13:15:30Z\",\n \"metadata\": \"UNKNOWN\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"level\": \"DEBUG\",\n \"statusMessage\": \"example\",\n \"parentObservationId\": \"example\",\n \"version\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Generations",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Log",
|
||||
"request": {
|
||||
"description": null,
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/generations",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"generations"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "POST",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"completionStartTime\": \"1994-11-05T13:15:30Z\",\n \"model\": \"example\",\n \"modelParameters\": {\n \"example\": \"example\"\n },\n \"prompt\": \"UNKNOWN\",\n \"completion\": \"UNKNOWN\",\n \"usage\": {\n \"promptTokens\": 0,\n \"completionTokens\": 0,\n \"totalTokens\": 0\n },\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"id\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"startTime\": \"1994-11-05T13:15:30Z\",\n \"metadata\": \"UNKNOWN\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"level\": \"DEBUG\",\n \"statusMessage\": \"example\",\n \"parentObservationId\": \"example\",\n \"version\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Update",
|
||||
"request": {
|
||||
"description": null,
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/generations",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"generations"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "PATCH",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"generationId\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"startTime\": \"1994-11-05T13:15:30Z\",\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"completionStartTime\": \"1994-11-05T13:15:30Z\",\n \"model\": \"example\",\n \"modelParameters\": {\n \"example\": \"example\"\n },\n \"prompt\": \"UNKNOWN\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"completion\": \"UNKNOWN\",\n \"usage\": {\n \"promptTokens\": 0,\n \"completionTokens\": 0,\n \"totalTokens\": 0\n },\n \"level\": \"DEBUG\",\n \"statusMessage\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
@@ -433,7 +320,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"body\": {\n \"id\": \"example\",\n \"timestamp\": \"1994-11-05T13:15:30Z\",\n \"name\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"userId\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"public\": true\n }\n }\n ]\n}",
|
||||
"raw": "{\n \"batch\": [\n {\n \"type\": \"trace-create\",\n \"body\": {\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"sessionId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"tags\": [\n \"example\"\n ],\n \"public\": true\n },\n \"id\": \"example\",\n \"timestamp\": \"example\",\n \"metadata\": \"UNKNOWN\"\n }\n ]\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -577,6 +464,82 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Prompts",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": "Get a specific prompt",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/prompts?name=&version=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"prompts"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "name",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "version",
|
||||
"value": "",
|
||||
"description": null
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"description": "Create a specific prompt",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/prompts",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"prompts"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "POST",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"name\": \"example\",\n \"isActive\": true,\n \"prompt\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
@@ -666,71 +629,37 @@
|
||||
{
|
||||
"_type": "container",
|
||||
"description": null,
|
||||
"name": "Span",
|
||||
"name": "Sessions",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Create",
|
||||
"name": "Get",
|
||||
"request": {
|
||||
"description": "Add a span to the database",
|
||||
"description": "Get a session",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/spans",
|
||||
"raw": "{{baseUrl}}/api/public/sessions/:sessionId",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"spans"
|
||||
"sessions",
|
||||
":sessionId"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
"variable": [
|
||||
{
|
||||
"key": "sessionId",
|
||||
"value": "",
|
||||
"description": "The unique id of a session"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": [],
|
||||
"method": "POST",
|
||||
"method": "GET",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"id\": \"example\",\n \"traceId\": \"example\",\n \"name\": \"example\",\n \"startTime\": \"1994-11-05T13:15:30Z\",\n \"metadata\": \"UNKNOWN\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"level\": \"DEBUG\",\n \"statusMessage\": \"example\",\n \"parentObservationId\": \"example\",\n \"version\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Update",
|
||||
"request": {
|
||||
"description": "Update a span to the database",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/spans",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"spans"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "PATCH",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"spanId\": \"example\",\n \"traceId\": \"example\",\n \"startTime\": \"1994-11-05T13:15:30Z\",\n \"endTime\": \"1994-11-05T13:15:30Z\",\n \"name\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"input\": \"UNKNOWN\",\n \"output\": \"UNKNOWN\",\n \"level\": \"DEBUG\",\n \"version\": \"example\",\n \"statusMessage\": \"example\"\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
"body": null
|
||||
},
|
||||
"response": []
|
||||
}
|
||||
@@ -741,39 +670,6 @@
|
||||
"description": null,
|
||||
"name": "Trace",
|
||||
"item": [
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Create",
|
||||
"request": {
|
||||
"description": "Add a trace to the database",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/traces",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
"path": [
|
||||
"api",
|
||||
"public",
|
||||
"traces"
|
||||
],
|
||||
"query": [],
|
||||
"variable": []
|
||||
},
|
||||
"header": [],
|
||||
"method": "POST",
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"id\": \"example\",\n \"name\": \"example\",\n \"userId\": \"example\",\n \"externalId\": \"example\",\n \"release\": \"example\",\n \"version\": \"example\",\n \"metadata\": \"UNKNOWN\",\n \"public\": true\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"response": []
|
||||
},
|
||||
{
|
||||
"_type": "endpoint",
|
||||
"name": "Get",
|
||||
@@ -812,7 +708,7 @@
|
||||
"request": {
|
||||
"description": "Get list of traces",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=",
|
||||
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&orderBy=&tags=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
@@ -841,6 +737,16 @@
|
||||
"key": "name",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "orderBy",
|
||||
"value": "",
|
||||
"description": "Format of the string sort_by=timestamp.asc (id, timestamp, name, userId, release, version, public, bookmarked, sessionId)"
|
||||
},
|
||||
{
|
||||
"key": "tags",
|
||||
"value": "",
|
||||
"description": "Only traces that include all of these tags will be returned."
|
||||
}
|
||||
],
|
||||
"variable": []
|
||||
|
||||
+6
-1
@@ -18,6 +18,8 @@ const serverTestConfig = {
|
||||
testEnvironment: "jest-environment-node",
|
||||
};
|
||||
|
||||
// To avoid the "Cannot use import statement outside a module" errors while transforming ESM.
|
||||
const esModules = ["superjson"];
|
||||
// Add any custom config to be passed to Jest
|
||||
/** @type {import('jest').Config} */
|
||||
const config = {
|
||||
@@ -26,7 +28,10 @@ const config = {
|
||||
verbose: true,
|
||||
projects: [
|
||||
await createJestConfig(clientTestConfig)(),
|
||||
await createJestConfig(serverTestConfig)(),
|
||||
{
|
||||
...(await createJestConfig(serverTestConfig)()),
|
||||
transformIgnorePatterns: [`/node_modules/(?!(${esModules.join("|")})/)`],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
+31
-21
@@ -23,27 +23,37 @@ const nextConfig = {
|
||||
output: "standalone",
|
||||
|
||||
async headers() {
|
||||
// Required to check authentication status from langfuse.com
|
||||
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined)
|
||||
return [
|
||||
{
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return [];
|
||||
return [
|
||||
{
|
||||
source: "/:path*",
|
||||
headers: [
|
||||
{
|
||||
key: "x-frame-options",
|
||||
value: "SAMEORIGIN",
|
||||
},
|
||||
],
|
||||
},
|
||||
// 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",
|
||||
},
|
||||
]
|
||||
},
|
||||
] : []
|
||||
)
|
||||
]
|
||||
},
|
||||
|
||||
// webassembly support for @dqbd/tiktoken
|
||||
|
||||
Generated
+3725
-2247
File diff suppressed because it is too large
Load Diff
+77
-64
@@ -1,38 +1,46 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "1.13.2",
|
||||
"version": "2.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"dx": "npm i && npm run db:reset && npm run db:seed:examples && npm run dev",
|
||||
"postinstall": "prisma generate",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
"prettier": "prettier --write ./src *.{ts,js}",
|
||||
"clean": "rm -rf node_modules",
|
||||
"start": "next start",
|
||||
"test": "jest --runInBand",
|
||||
"test:watch": "jest --watch --runInBand",
|
||||
"test:e2e": "playwright test",
|
||||
"db:migrate": "npx prisma migrate dev",
|
||||
"infra:dev:up": "docker-compose -f docker-compose.dev.yml up -d",
|
||||
"infra:dev:down": "docker-compose -f docker-compose.dev.yml down",
|
||||
"db:migrate": "DISABLE_ERD=false npx prisma migrate dev",
|
||||
"db:reset": "npx prisma migrate reset",
|
||||
"db:seed": "npx prisma db seed",
|
||||
"db:seed:examples": "npx prisma db seed -- --environment examples",
|
||||
"release": "release-it"
|
||||
"release": "release-it",
|
||||
"models:migrate": "tsx scripts/model-match.ts"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "ts-node -r tsconfig-paths/register --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@formkit/auto-animate": "^0.8.1",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@hookform/resolvers": "^3.3.2",
|
||||
"@aws-sdk/client-s3": "^3.507.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.507.0",
|
||||
"@headlessui/react": "^1.7.18",
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^5.6.0",
|
||||
"@prisma/client": "^5.9.1",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
@@ -40,6 +48,7 @@
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
@@ -48,90 +57,89 @@
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@react-email/components": "^0.0.12",
|
||||
"@react-email/render": "^0.0.10",
|
||||
"@sentry/nextjs": "^7.83.0",
|
||||
"@sentry/profiling-node": "^1.2.6",
|
||||
"@t3-oss/env-nextjs": "^0.7.1",
|
||||
"@react-email/components": "^0.0.14",
|
||||
"@react-email/render": "^0.0.12",
|
||||
"@sentry/nextjs": "^7.99.0",
|
||||
"@sentry/profiling-node": "^1.3.5",
|
||||
"@sentry/types": "^7.88.0",
|
||||
"@t3-oss/env-nextjs": "^0.8.0",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.10.7",
|
||||
"@tanstack/react-table": "^8.11.8",
|
||||
"@tremor/react": "^3.11.1",
|
||||
"@trpc/client": "^10.44.1",
|
||||
"@trpc/next": "^10.44.1",
|
||||
"@trpc/react-query": "^10.44.1",
|
||||
"@trpc/server": "^10.44.1",
|
||||
"@trpc/client": "^10.45.0",
|
||||
"@trpc/next": "^10.45.0",
|
||||
"@trpc/react-query": "^10.45.0",
|
||||
"@trpc/server": "^10.45.0",
|
||||
"@vercel/edge-config": "^0.4.1",
|
||||
"axios": "^1.6.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^0.2.1",
|
||||
"core-js": "^3.35.1",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^2.30.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"js-tiktoken": "^1.0.10",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.294.0",
|
||||
"next": "^14.0.3",
|
||||
"lucide-react": "^0.323.0",
|
||||
"next": "^14.1.0",
|
||||
"next-auth": "^4.24.5",
|
||||
"next-query-params": "^5.0.0",
|
||||
"nodemailer": "^6.9.7",
|
||||
"posthog-js": "^1.93.6",
|
||||
"posthog-node": "^3.1.3",
|
||||
"nodemailer": "^6.9.9",
|
||||
"posthog-js": "^1.104.4",
|
||||
"posthog-node": "^3.6.2",
|
||||
"react": "18.2.0",
|
||||
"react-day-picker": "^8.9.1",
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.48.2",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-hook-form": "^7.50.1",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react18-json-view": "^0.2.7-canary.2",
|
||||
"superagent": "^8.1.2",
|
||||
"react18-json-view": "^0.2.7",
|
||||
"superjson": "2.2.1",
|
||||
"tailwind-merge": "^2.1.0",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tiktoken": "^1.0.11",
|
||||
"ts-pattern": "^5.0.6",
|
||||
"use-query-params": "^2.2.1",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@jedmao/location": "^3.0.0",
|
||||
"@mermaid-js/mermaid-cli": "^10.7.0",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@release-it/bumper": "^6.0.1",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/react": "^14.2.1",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/eslint": "^8.44.8",
|
||||
"@types/jest": "^29.5.10",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/eslint": "^8.56.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "20.10.0",
|
||||
"@types/node": "20.10.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/react": "^18.2.42",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
||||
"@typescript-eslint/parser": "^6.13.2",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-next": "^14.0.3",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"postcss": "^8.4.32",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.8",
|
||||
"prisma": "^5.6.0",
|
||||
"release-it": "^17.0.0",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"ts-node": "^10.9.1",
|
||||
"postcss": "^8.4.34",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"prisma": "^5.9.1",
|
||||
"prisma-erd-generator": "^1.11.2",
|
||||
"release-it": "^17.0.3",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.3.2"
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.13.0"
|
||||
@@ -148,7 +156,12 @@
|
||||
"release": true,
|
||||
"web": true,
|
||||
"autoGenerate": true,
|
||||
"releaseName": "v${version}"
|
||||
"releaseName": "v${version}",
|
||||
"comments": {
|
||||
"submit": true,
|
||||
"issue": ":rocket: _This issue has been resolved in v${version}. See [${releaseName}](${releaseUrl}) for release notes._",
|
||||
"pr": ":rocket: _This pull request is included in v${version}. See [${releaseName}](${releaseUrl}) for release notes._"
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it/bumper": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 194 KiB |
@@ -0,0 +1,34 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "traces" ADD COLUMN "input" JSONB,
|
||||
ADD COLUMN "output" JSONB,
|
||||
ADD COLUMN "session_id" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "trace_sessions" (
|
||||
"id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"project_id" TEXT NOT NULL,
|
||||
"bookmarked" BOOLEAN NOT NULL DEFAULT false,
|
||||
"public" BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
CONSTRAINT "trace_sessions_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "trace_sessions_project_id_idx" ON "trace_sessions"("project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "trace_sessions_created_at_idx" ON "trace_sessions"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "trace_sessions_id_project_id_key" ON "trace_sessions"("id", "project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "traces_session_id_idx" ON "traces"("session_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "trace_sessions" ADD CONSTRAINT "trace_sessions_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "traces" ADD CONSTRAINT "traces_session_id_fkey" FOREIGN KEY ("session_id") REFERENCES "trace_sessions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- AlterTable
|
||||
-- Adding this migration does not lock the postgres table. For non volaitile data, the default value is stored in the
|
||||
-- table metadata and accessed on read query time. No table re-write is required (https://www.postgresql.org/docs/current/sql-altertable.html#:~:text=When%20a%20column%20is%20added,is%20specified%2C%20NULL%20is%20used.)
|
||||
ALTER TABLE "observations" ADD COLUMN "unit" TEXT NOT NULL DEFAULT 'TOKENS';
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "projects" ADD COLUMN "cloud_config" JSONB;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Account" ADD COLUMN "expires_in" INTEGER,
|
||||
ADD COLUMN "ext_expires_in" INTEGER;
|
||||
@@ -0,0 +1,29 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "observations" ADD COLUMN "prompt_id" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "prompts" (
|
||||
"id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"project_id" TEXT NOT NULL,
|
||||
"created_by" TEXT NOT NULL,
|
||||
"prompt" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"version" INTEGER NOT NULL,
|
||||
"is_active" BOOLEAN NOT NULL,
|
||||
|
||||
CONSTRAINT "prompts_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "prompts_project_id_name_version_idx" ON "prompts"("project_id", "name", "version");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "prompts_project_id_name_version_key" ON "prompts"("project_id", "name", "version");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "observations" ADD CONSTRAINT "observations_prompt_id_fkey" FOREIGN KEY ("prompt_id") REFERENCES "prompts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "prompts" ADD CONSTRAINT "prompts_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,30 @@
|
||||
-- Migration to add gpt-3.5-turbo-1106 and gpt-3.5-turbo-instruct models
|
||||
|
||||
INSERT INTO pricings (
|
||||
id,
|
||||
model_name,
|
||||
pricing_unit,
|
||||
price,
|
||||
currency,
|
||||
token_type
|
||||
)
|
||||
VALUES
|
||||
('clqqpc2pr000008l3hvy63gxy','gpt-3.5-turbo-1106', 'PER_1000_TOKENS', 0.001, 'USD', 'PROMPT'),
|
||||
('clqqpcb6d000208l3atrfbmou','gpt-3.5-turbo-1106', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION'),
|
||||
('clqqpdh45000008lfgrnx76cv','gpt-3.5-turbo-instruct', 'PER_1000_TOKENS', 0.0015, 'USD', 'PROMPT'),
|
||||
('clqqpdjya000108lf3s4b4c4m','gpt-3.5-turbo-instruct', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION'),
|
||||
('clnnuiuq6000008l4dxp43wc6', 'claude-instant-1.2', 'PER_1000_TOKENS', 0.00163, 'USD', 'PROMPT'),
|
||||
('clnnujlmj000108l48wne4ii9', 'claude-instant-1.2', 'PER_1000_TOKENS', 0.00551, 'USD', 'COMPLETION'),
|
||||
('clnnukutg000208l49qqt9lyr', 'claude-instant-1.1', 'PER_1000_TOKENS', 0.00163, 'USD', 'PROMPT'),
|
||||
('clnnun3x3000408l490qz9uv0', 'claude-instant-1.1', 'PER_1000_TOKENS', 0.00551, 'USD', 'COMPLETION'),
|
||||
('clnnuosvy000508l4gsjy2pp4', 'claude-2.0', 'PER_1000_TOKENS', 0.01102, 'USD', 'PROMPT'),
|
||||
('clnnuqcns000608l40qaz8trt', 'claude-2.0', 'PER_1000_TOKENS', 0.03268, 'USD', 'COMPLETION'),
|
||||
('clnnuuif8000808l4gal4fjq4', 'claude-1.0', 'PER_1000_TOKENS', 0.01102, 'USD', 'PROMPT'),
|
||||
('clnnutptp000708l47r091vvd', 'claude-1.0', 'PER_1000_TOKENS', 0.03268, 'USD', 'COMPLETION'),
|
||||
('clnon8riv000308mlgfr1agiv', 'text-embedding-ada-002', 'PER_1000_TOKENS', 0.0001, 'USD', 'PROMPT'),
|
||||
('clnon9kfz000408ml1bg81o6z', 'text-embedding-ada-002', 'PER_1000_TOKENS', 0.0001, 'USD', 'COMPLETION'),
|
||||
('clqwniv8a000d08l2frjl7mmw', 'codechat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'PROMPT'),
|
||||
('clqwnj044000e08l2dfjm5g90', 'chat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'PROMPT'),
|
||||
('clqwnj47r000f08l2a16fekls', 'chat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'COMPLETION'),
|
||||
('clqwnj863000g08l2bwxgdapm', 'codechat-bison-32k', 'PER_1000_CHARS', 0.0005, 'USD', 'COMPLETION')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "observations_model_idx" ON "observations"("model");
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "pricings_model_name_idx" ON "pricings"("model_name");
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "traces" ADD COLUMN "tags" TEXT[] DEFAULT ARRAY[]::TEXT[];
|
||||
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "traces_tags_idx" ON "traces" USING GIN ("tags" array_ops);
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `status` on the `datasets` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "datasets" DROP COLUMN "status";
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Migration to add gpt-35 spelling
|
||||
|
||||
INSERT INTO pricings (
|
||||
id,
|
||||
model_name,
|
||||
pricing_unit,
|
||||
price,
|
||||
currency,
|
||||
token_type
|
||||
)
|
||||
VALUES
|
||||
('clqqpc2pr000008l3hvy63gxy1','gpt-35-turbo-1106', 'PER_1000_TOKENS', 0.001, 'USD', 'PROMPT'),
|
||||
('clqqpcb6d000208l3atrfbmou1','gpt-35-turbo-1106', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION'),
|
||||
('clqqpdh45000008lfgrnx76cv1','gpt-35-turbo-instruct', 'PER_1000_TOKENS', 0.0015, 'USD', 'PROMPT'),
|
||||
('clqqpdjya000108lf3s4b4c4m1','gpt-35-turbo-instruct', 'PER_1000_TOKENS', 0.002, 'USD', 'COMPLETION')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "traces_project_id_external_id_key";
|
||||
@@ -0,0 +1,4 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "observations" ADD COLUMN "input_cost" DECIMAL(65,30),
|
||||
ADD COLUMN "output_cost" DECIMAL(65,30),
|
||||
ADD COLUMN "total_cost" DECIMAL(65,30);
|
||||
@@ -0,0 +1,20 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "models" (
|
||||
"id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"project_id" TEXT,
|
||||
"model_name" TEXT NOT NULL,
|
||||
"match_pattern" TEXT NOT NULL,
|
||||
"start_date" TIMESTAMP(3),
|
||||
"input_price" DECIMAL(65,30),
|
||||
"output_price" DECIMAL(65,30),
|
||||
"total_price" DECIMAL(65,30),
|
||||
"unit" TEXT NOT NULL DEFAULT 'TOKENS',
|
||||
"tokenizer_config" JSONB NOT NULL,
|
||||
|
||||
CONSTRAINT "models_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "models" ADD CONSTRAINT "models_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "observations" ADD COLUMN "internal_model" TEXT;
|
||||
@@ -0,0 +1,49 @@
|
||||
CREATE VIEW "observations_view" AS
|
||||
SELECT
|
||||
o.*,
|
||||
m.id AS "model_id",
|
||||
m.start_date AS "model_start_date",
|
||||
m.input_price,
|
||||
m.output_price,
|
||||
m.total_price,
|
||||
m.tokenizer_config AS "tokenizer_config",
|
||||
CASE
|
||||
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
|
||||
o.prompt_tokens::decimal * m.input_price
|
||||
ELSE
|
||||
o.input_cost
|
||||
END AS "calculated_input_cost",
|
||||
CASE
|
||||
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
|
||||
o.completion_tokens::decimal * m.output_price
|
||||
ELSE
|
||||
o.output_cost
|
||||
END AS "calculated_output_cost",
|
||||
CASE
|
||||
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
|
||||
CASE
|
||||
WHEN m.total_price IS NOT NULL AND o.total_tokens IS NOT NULL THEN
|
||||
m.total_price * o.total_tokens
|
||||
ELSE
|
||||
o.prompt_tokens::decimal * m.input_price +
|
||||
o.completion_tokens::decimal * m.output_price
|
||||
END
|
||||
ELSE
|
||||
o.total_cost
|
||||
END AS "calculated_total_cost"
|
||||
FROM
|
||||
observations o
|
||||
LEFT JOIN models m ON m.id = (
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
models
|
||||
WHERE (project_id = o.project_id OR project_id IS NULL)
|
||||
AND model_name = o.internal_model
|
||||
AND (start_date < o.start_time OR start_date is NULL)
|
||||
AND o.unit::TEXT = unit
|
||||
ORDER BY
|
||||
project_id ASC, -- in postgres, NULLs are sorted first
|
||||
start_date DESC
|
||||
LIMIT 1
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- A unique constraint covering the columns `[project_id,model_name,start_date,unit]` on the table `models` will be added. If there are existing duplicate values, this will fail.
|
||||
|
||||
*/
|
||||
-- CreateIndex
|
||||
CREATE INDEX "models_project_id_model_name_idx" ON "models"("project_id", "model_name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "models_project_id_model_name_start_date_unit_key" ON "models"("project_id", "model_name", "start_date", "unit");
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "models" ADD COLUMN "tokenizer_id" TEXT;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "models" ALTER COLUMN "tokenizer_config" DROP NOT NULL;
|
||||
@@ -0,0 +1,84 @@
|
||||
-- This is an empty migration.
|
||||
|
||||
|
||||
INSERT INTO models (
|
||||
id,
|
||||
project_id,
|
||||
model_name,
|
||||
match_pattern,
|
||||
start_date,
|
||||
input_price,
|
||||
output_price,
|
||||
total_price,
|
||||
unit,
|
||||
tokenizer_id,
|
||||
tokenizer_config
|
||||
)
|
||||
VALUES
|
||||
--project_id, model_name, match_pattern, start_date, input_price, output_price, total_price, unit, tokenizer_id, tokenizer_config
|
||||
--https://openai.com/pricing
|
||||
-- GPT-4 Turbo
|
||||
('clrkvq6iq000008ju6c16gynt', NULL, 'gpt-4-turbo', '(?i)^(gpt-4-1106-preview)$', NULL, 0.00001, 0.00003, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4-1106-preview" }'),
|
||||
('clrkvx5gp000108juaogs54ea', NULL, 'gpt-4-turbo-vision', '(?i)^(gpt-4-vision-preview)$', NULL, 0.00001, 0.00003, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4-vision-preview" }'),
|
||||
|
||||
-- GPT-4
|
||||
('clrntkjgy000f08jx79v9g1xj', NULL, 'gpt-4', '(?i)^(gpt-4)$', NULL, 0.00003, 0.00006, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4" }'),
|
||||
('clrkwk4cc000908l537kl0rx3', NULL, 'gpt-4-0613', '(?i)^(gpt-4-0613)$', NULL, 0.00003, 0.00006, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4-0613" }'),
|
||||
('clrntkjgy000e08jx4x6uawoo', NULL, 'gpt-4-0314', '(?i)^(gpt-4-0314)$', NULL, 0.00003, 0.00006, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4-0314" }'),
|
||||
|
||||
('clrkvyzgw000308jue4hse4j9', NULL, 'gpt-4-32k', '(?i)^(gpt-4-32k)$', NULL, 0.00006, 0.00012, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4-32k" }'),
|
||||
('clrkwk4cb000108l5hwwh3zdi', NULL, 'gpt-4-32k-0613', '(?i)^(gpt-4-32k-0613)$', NULL, 0.00006, 0.00012, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4-32k-0613" }'),
|
||||
('clrntkjgy000d08jx0p4y9h4l', NULL, 'gpt-4-32k-0314', '(?i)^(gpt-4-32k-0314)$', NULL, 0.00006, 0.00012, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4-32k-0314" }'),
|
||||
|
||||
-- GPT 3
|
||||
|
||||
('clrkwk4cc000a08l562uc3s9g', NULL, 'gpt-3.5-turbo-instruct', '(?i)^(gpt-)(35|3.5)(-turbo-instruct)$', NULL, 0.0000015, 0.000002, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-3.5-turbo" }'),
|
||||
('clrkwk4cb000408l576jl7koo', NULL, 'gpt-3.5-turbo', '(?i)^(gpt-)(35|3.5)(-turbo)$', '2023-11-06', 0.000001, 0.000002, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-3.5-turbo" }'),
|
||||
('clrkwk4cb000208l59yvb9yq8', NULL, 'gpt-3.5-turbo-1106', '(?i)^(gpt-)(35|3.5)(-turbo-1106)$', NULL, 0.000001, 0.000002, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-3.5-turbo-1106" }'),
|
||||
|
||||
('clrntkjgy000c08jxesb30p3f', NULL, 'gpt-3.5-turbo', '(?i)^(gpt-)(35|3.5)(-turbo)$', '2023-06-27', 0.0000015, 0.000002, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-3.5-turbo" }'),
|
||||
('clrkwk4cc000808l51xmk4uic', NULL, 'gpt-3.5-turbo-0613', '(?i)^(gpt-)(35|3.5)(-turbo-0613)$', NULL, 0.0000015, 0.000002, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-3.5-turbo-0613" }'),
|
||||
|
||||
('clrntkjgy000b08jx769q1bah', NULL, 'gpt-3.5-turbo', '(?i)^(gpt-)(35|3.5)(-turbo)$', NULL, 0.000002, 0.000002, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 4, "tokensPerName": -1, "tokenizerModel": "gpt-3.5-turbo" }'),
|
||||
('clrntkjgy000a08jx4e062mr0', NULL, 'gpt-3.5-turbo-0301', '(?i)^(gpt-)(35|3.5)(-turbo-0301)$', NULL, 0.000002, 0.000002, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 4, "tokensPerName": -1, "tokenizerModel": "gpt-3.5-turbo-0301" }'),
|
||||
|
||||
|
||||
('clrkwk4cb000308l5go4b6otm', NULL, 'gpt-3.5-turbo-16k', '(?i)^(gpt-)(35|3.5)(-turbo-16k)$', NULL, 0.00003, 0.00004, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-3.5-turbo-16k" }'),
|
||||
('clrntjt89000a08jw0gcdbd5a', NULL, 'gpt-3.5-turbo-16k-0613', '(?i)^(gpt-)(35|3.5)(-turbo-16k-0613)$', NULL, 0.00003, 0.00004, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-3.5-turbo-16k-0613" }'),
|
||||
|
||||
|
||||
|
||||
|
||||
-- nothing earlier required for ada
|
||||
('clrntjt89000908jwhvkz5crm', NULL, 'text-embedding-ada-002', '(?i)^(text-embedding-ada-002)$', '2022-12-06', NULL, NULL, 0.0000001, 'TOKENS', 'openai', NULL),
|
||||
('clrntjt89000908jwhvkz5crg', NULL, 'text-embedding-ada-002-v2', '(?i)^(text-embedding-ada-002-v2)$', '2022-12-06', NULL, NULL, 0.0000001, 'TOKENS', 'openai', NULL),
|
||||
|
||||
|
||||
|
||||
|
||||
-- legacy price 2023-08-22 https://platform.openai.com/docs/deprecations/2023-07-06-gpt-and-embeddings
|
||||
('clrntjt89000108jwcou1af71', NULL, 'text-ada-001', '(?i)^(text-ada-001)$', NULL, NULL, NULL, 0.000004, 'TOKENS', 'openai', NULL),
|
||||
('clrntjt89000208jwawjr894q', NULL, 'text-babbage-001', '(?i)^(text-babbage-001)$', NULL, NULL, NULL, 0.0000005, 'TOKENS', 'openai', NULL),
|
||||
('clrp1wopz000708l079w02hkc', NULL, 'text-babbage-002', '(?i)^(text-babbage-002)$', NULL, NULL, NULL, 0.0000005, 'TOKENS', 'openai', NULL),
|
||||
('clrntjt89000308jw0jtfa4rs', NULL, 'text-curie-001', '(?i)^(text-curie-001)$', NULL, NULL, NULL, 0.00002, 'TOKENS', 'openai', NULL),
|
||||
('clrntjt89000408jwc2c93h6i', NULL, 'text-davinci-001', '(?i)^(text-davinci-001)$', NULL, NULL, NULL, 0.00002, 'TOKENS', 'openai', NULL),
|
||||
('clrntjt89000508jw192m64qi', NULL, 'text-davinci-002', '(?i)^(text-davinci-002)$', NULL, NULL, NULL, 0.00002, 'TOKENS', 'openai', NULL),
|
||||
('clrntjt89000608jw4m3x5s55', NULL, 'text-davinci-003', '(?i)^(text-davinci-003)$', NULL, NULL, NULL, 0.00002, 'TOKENS', 'openai', NULL),
|
||||
|
||||
|
||||
-- claude
|
||||
('clrnwbota000908jsgg9mb1ml', NULL, 'claude-instant-1', '(?i)^(claude-instant-1)$', NULL, 0.00000163, 0.00000551, NULL, 'CHARACTERS', 'claude', NULL),
|
||||
('clrnwb41q000308jsfrac9uh6', NULL, 'claude-instant-1.2', '(?i)^(claude-instant-1.2)$', NULL, 0.00000163, 0.00000551, NULL, 'CHARACTERS', 'claude', NULL),
|
||||
('clrnwbd1m000508js4hxu6o7n', NULL, 'claude-2.1', '(?i)^(claude-2.1)$', NULL, 0.000008, 0.000024, NULL, 'CHARACTERS', 'claude', NULL),
|
||||
('clrnwb836000408jsallr6u11', NULL, 'claude-2.0', '(?i)^(claude-2.0)$', NULL, 0.000008, 0.000024, NULL, 'CHARACTERS', 'claude', NULL),
|
||||
('clrnwbg2b000608jse2pp4q2d', NULL, 'claude-1.3', '(?i)^(claude-1.3)$', NULL, 0.000008, 0.000024, NULL, 'CHARACTERS', 'claude', NULL),
|
||||
('clrnwbi9d000708jseiy44k26', NULL, 'claude-1.2', '(?i)^(claude-1.2)$', NULL, 0.000008, 0.000024, NULL, 'CHARACTERS', 'claude', NULL),
|
||||
('clrnwblo0000808jsc1385hdp', NULL, 'claude-1.1', '(?i)^(claude-1.1)$', NULL, 0.000008, 0.000024, NULL, 'CHARACTERS', 'claude', NULL),
|
||||
|
||||
|
||||
-- vertex
|
||||
('clrp1wopz000808l09nwy32xh', NULL, 'codechat-bison-32k', '(?i)^(codechat-bison-32k)$', NULL, 0.0000005, 0.0000025, NULL, 'TOKENS', 'vertex', NULL),
|
||||
('clrp1wopz000408l05xcycki1', NULL, 'chat-bison-32k', '(?i)^(chat-bison-32k)$', NULL, 0.0000005, 0.0000025, NULL, 'TOKENS', 'vertex', NULL)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- The primary key for the `trace_sessions` table will be changed. If it partially fails, the table could be left without primary key constraint.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "traces" DROP CONSTRAINT "traces_session_id_fkey";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "trace_sessions_id_project_id_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "trace_sessions" DROP CONSTRAINT "trace_sessions_pkey",
|
||||
ADD CONSTRAINT "trace_sessions_pkey" PRIMARY KEY ("id", "project_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "traces" ADD CONSTRAINT "traces_session_id_project_id_fkey" FOREIGN KEY ("session_id", "project_id") REFERENCES "trace_sessions"("id", "project_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,42 @@
|
||||
-- This is an empty migration.
|
||||
|
||||
DELETE FROM models
|
||||
WHERE id in ('clrntjt89000908jwhvkz5crm', 'clrntjt89000908jwhvkz5crg', 'clrntjt89000108jwcou1af71', 'clrntjt89000208jwawjr894q', 'clrntjt89000308jw0jtfa4rs', 'clrntjt89000408jwc2c93h6i', 'clrntjt89000508jw192m64qi', 'clrntjt89000608jw4m3x5s55', 'clrp1wopz000708l079w02hkc');
|
||||
|
||||
|
||||
|
||||
INSERT INTO models (
|
||||
id,
|
||||
project_id,
|
||||
model_name,
|
||||
match_pattern,
|
||||
start_date,
|
||||
input_price,
|
||||
output_price,
|
||||
total_price,
|
||||
unit,
|
||||
tokenizer_id,
|
||||
tokenizer_config
|
||||
)
|
||||
VALUES
|
||||
-- nothing earlier required for ada
|
||||
('clrntjt89000908jwhvkz5crm', NULL, 'text-embedding-ada-002', '(?i)^(text-embedding-ada-002)$', '2022-12-06', NULL, NULL, 0.0000001, 'TOKENS', 'openai', '{"tokenizerModel": "text-embedding-ada-002"}'),
|
||||
('clrntjt89000908jwhvkz5crg', NULL, 'text-embedding-ada-002-v2', '(?i)^(text-embedding-ada-002-v2)$', '2022-12-06', NULL, NULL, 0.0000001, 'TOKENS', 'openai', '{"tokenizerModel": "text-embedding-ada-002"}'),
|
||||
|
||||
|
||||
|
||||
|
||||
-- -- legacy price 2023-08-22 https://platform.openai.com/docs/deprecations/2023-07-06-gpt-and-embeddings
|
||||
('clrntjt89000108jwcou1af71', NULL, 'text-ada-001', '(?i)^(text-ada-001)$', NULL, NULL, NULL, 0.000004, 'TOKENS', 'openai', '{"tokenizerModel": "text-ada-001"}'),
|
||||
('clrntjt89000208jwawjr894q', NULL, 'text-babbage-001', '(?i)^(text-babbage-001)$', NULL, NULL, NULL, 0.0000005, 'TOKENS', 'openai', '{"tokenizerModel": "text-babbage-001"}'),
|
||||
('clrntjt89000308jw0jtfa4rs', NULL, 'text-curie-001', '(?i)^(text-curie-001)$', NULL, NULL, NULL, 0.00002, 'TOKENS', 'openai', '{"tokenizerModel": "text-curie-001"}'),
|
||||
('clrntjt89000408jwc2c93h6i', NULL, 'text-davinci-001', '(?i)^(text-davinci-001)$', NULL, NULL, NULL, 0.00002, 'TOKENS', 'openai', '{"tokenizerModel": "text-davinci-001"}'),
|
||||
('clrntjt89000508jw192m64qi', NULL, 'text-davinci-002', '(?i)^(text-davinci-002)$', NULL, NULL, NULL, 0.00002, 'TOKENS', 'openai', '{"tokenizerModel": "text-davinci-002"}'),
|
||||
('clrntjt89000608jw4m3x5s55', NULL, 'text-davinci-003', '(?i)^(text-davinci-003)$', NULL, NULL, NULL, 0.00002, 'TOKENS', 'openai', '{"tokenizerModel": "text-davinci-003"}'),
|
||||
|
||||
('clrs2dnql000108l46vo0gp2t', NULL, 'babbage-002', '(?i)^(babbage-002)$', NULL, 0.0000004, 0.0000016, 0.0000005, 'TOKENS', 'openai', '{"tokenizerModel": "babbage-002"}'),
|
||||
('clrs2ds35000208l4g4b0hi3u', NULL, 'davinci-002', '(?i)^(davinci-002)$', NULL, 0.0000060, 0.0000120, 0.0000005, 'TOKENS', 'openai', '{"tokenizerModel": "davinci-002"}')
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
-- This is an empty migration.
|
||||
|
||||
DELETE FROM models
|
||||
WHERE id in ('clrp1wopz000808l09nwy32xh', 'clrp1wopz000408l05xcycki1','clrs2dnql000108l46vo0gp2t', 'clrs2ds35000208l4g4b0hi3u');
|
||||
|
||||
|
||||
|
||||
INSERT INTO models (
|
||||
id,
|
||||
project_id,
|
||||
model_name,
|
||||
match_pattern,
|
||||
start_date,
|
||||
input_price,
|
||||
output_price,
|
||||
total_price,
|
||||
unit,
|
||||
tokenizer_id,
|
||||
tokenizer_config
|
||||
)
|
||||
VALUES
|
||||
-- https://openai.com/blog/new-embedding-models-and-api-updates
|
||||
-- tokenizers are best guess for embedding models
|
||||
('clruwn3pc00010al7bl611c8o', NULL, 'text-embedding-3-small', '(?i)^(text-embedding-3-small)$', NULL, NULL, NULL, 0.00000002, 'TOKENS', 'openai', '{"tokenizerModel": "text-embedding-ada-002"}'),
|
||||
('clruwn76700020al7gp8e4g4l', NULL, 'text-embedding-ada-002-v2', '(?i)^(text-embedding-3-large)$', NULL, NULL, NULL, 0.00000013, 'TOKENS', 'openai', '{"tokenizerModel": "text-embedding-ada-002"}'),
|
||||
|
||||
('clruwnahl00030al7ab9rark7', NULL, 'gpt-3.5-turbo-0125', '(?i)^(gpt-)(35|3.5)(-turbo-0125)$', NULL, 0.0000005, 0.0000015, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-3.5-turbo" }'),
|
||||
('clruwnahl00040al78f1lb0at', NULL, 'gpt-3.5-turbo', '(?i)^(gpt-)(35|3.5)(-turbo)$', '2024-02-08', 0.0000005, 0.0000015, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-3.5-turbo" }'),
|
||||
|
||||
('clruwnahl00050al796ck3p44', NULL, 'gpt-4-0125-preview', '(?i)^(gpt-4-0125-preview)$', NULL, 0.00001, 0.00003, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4" }'),
|
||||
('clruwnahl00060al74fcfehas', NULL, 'gpt-4-turbo-preview', '(?i)^(gpt-4-turbo-preview)$', NULL, 0.00003, 0.00006, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-4" }'),
|
||||
|
||||
-- fix tokenizer for vertx
|
||||
('clrp1wopz000808l09nwy32xh', NULL, 'codechat-bison-32k', '(?i)^(codechat-bison-32k)$', NULL, 0.0000005, 0.0000025, NULL, 'TOKENS', NULL, NULL),
|
||||
('clrp1wopz000408l05xcycki1', NULL, 'chat-bison-32k', '(?i)^(chat-bison-32k)$', NULL, 0.0000005, 0.0000025, NULL, 'TOKENS', NULL, NULL),
|
||||
|
||||
-- fix prices
|
||||
('clrs2dnql000108l46vo0gp2t', NULL, 'babbage-002', '(?i)^(babbage-002)$', NULL, 0.0000004, 0.0000016, NULL, 'TOKENS', 'openai', '{"tokenizerModel": "babbage-002"}'),
|
||||
('clrs2ds35000208l4g4b0hi3u', NULL, 'davinci-002', '(?i)^(davinci-002)$', NULL, 0.0000060, 0.0000120, NULL, 'TOKENS', 'openai', '{"tokenizerModel": "davinci-002"}')
|
||||
@@ -0,0 +1,6 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "models" ALTER COLUMN "unit" DROP DEFAULT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "observations" ALTER COLUMN "unit" DROP NOT NULL,
|
||||
ALTER COLUMN "unit" DROP DEFAULT;
|
||||
@@ -0,0 +1,40 @@
|
||||
-- This is an empty migration.
|
||||
|
||||
DELETE FROM models
|
||||
WHERE id in (
|
||||
'clrnwbota000908jsgg9mb1ml',
|
||||
'clrnwb41q000308jsfrac9uh6',
|
||||
'clrnwbd1m000508js4hxu6o7n',
|
||||
'clrnwb836000408jsallr6u11',
|
||||
'clrnwbg2b000608jse2pp4q2d',
|
||||
'clrnwbi9d000708jseiy44k26',
|
||||
'clrnwblo0000808jsc1385hdp'
|
||||
);
|
||||
|
||||
|
||||
|
||||
INSERT INTO models (
|
||||
id,
|
||||
project_id,
|
||||
model_name,
|
||||
match_pattern,
|
||||
start_date,
|
||||
input_price,
|
||||
output_price,
|
||||
total_price,
|
||||
unit,
|
||||
tokenizer_id,
|
||||
tokenizer_config
|
||||
)
|
||||
VALUES
|
||||
-- nothing earlier required for ada
|
||||
('clrnwbota000908jsgg9mb1ml', NULL, 'claude-instant-1', '(?i)^(claude-instant-1)$', NULL, 0.00000163, 0.00000551, NULL, 'TOKENS', 'claude', NULL),
|
||||
('clrnwb41q000308jsfrac9uh6', NULL, 'claude-instant-1.2', '(?i)^(claude-instant-1.2)$', NULL, 0.00000163, 0.00000551, NULL, 'TOKENS', 'claude', NULL),
|
||||
('clrnwbd1m000508js4hxu6o7n', NULL, 'claude-2.1', '(?i)^(claude-2.1)$', NULL, 0.000008, 0.000024, NULL, 'TOKENS', 'claude', NULL),
|
||||
('clrnwb836000408jsallr6u11', NULL, 'claude-2.0', '(?i)^(claude-2.0)$', NULL, 0.000008, 0.000024, NULL, 'TOKENS', 'claude', NULL),
|
||||
('clrnwbg2b000608jse2pp4q2d', NULL, 'claude-1.3', '(?i)^(claude-1.3)$', NULL, 0.000008, 0.000024, NULL, 'TOKENS', 'claude', NULL),
|
||||
('clrnwbi9d000708jseiy44k26', NULL, 'claude-1.2', '(?i)^(claude-1.2)$', NULL, 0.000008, 0.000024, NULL, 'TOKENS', 'claude', NULL),
|
||||
('clrnwblo0000808jsc1385hdp', NULL, 'claude-1.1', '(?i)^(claude-1.1)$', NULL, 0.000008, 0.000024, NULL, 'TOKENS', 'claude', NULL)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
DELETE FROM models
|
||||
WHERE id in ('clrp1wopz000808l09nwy32xh', 'clrp1wopz000408l05xcycki1');
|
||||
|
||||
INSERT INTO models (
|
||||
id,
|
||||
project_id,
|
||||
model_name,
|
||||
match_pattern,
|
||||
start_date,
|
||||
input_price,
|
||||
output_price,
|
||||
total_price,
|
||||
unit,
|
||||
tokenizer_id,
|
||||
tokenizer_config
|
||||
)
|
||||
VALUES
|
||||
-- https://openai.com/blog/new-embedding-models-and-api-updates
|
||||
-- https://openai.com/blog/gpt-3-5-turbo-fine-tuning-and-api-updates
|
||||
-- ft model tokens are getting counted like the base model https://github.com/openai/tiktoken/blob/db5bda9fc93b3171db6c4afea329394e6b6d31ca/tiktoken/model.py
|
||||
|
||||
('cls08r8sq000308jq14ae96f0', NULL, 'ft:gpt-3.5-turbo-1106', '(?i)^(ft:)(gpt-3.5-turbo-1106:)(.+)(:)(.*)(:)(.+)$', NULL, 0.000003, 0.000006, NULL, 'TOKENS', 'openai', '{"tokensPerName": 1, "tokenizerModel": "gpt-3.5-turbo-1106", "tokensPerMessage": 3}'),
|
||||
('cls08rp99000408jqepxoakjv', NULL, 'ft:gpt-3.5-turbo-0613', '(?i)^(ft:)(gpt-3.5-turbo-0613:)(.+)(:)(.*)(:)(.+)$', NULL, 0.000012, 0.000016, NULL, 'TOKENS', 'openai', '{"tokensPerName": 1, "tokenizerModel": "gpt-3.5-turbo-0613", "tokensPerMessage": 3}'),
|
||||
('cls08rv9g000508jq5p4z4nlr', NULL, 'ft:davinci-002', '(?i)^(ft:)(davinci-002:)(.+)(:)(.*)(:)(.+)$$', NULL, 0.000012, 0.000012, NULL, 'TOKENS', 'openai', '{"tokenizerModel": "davinci-002"}'),
|
||||
('cls08s2bw000608jq57wj4un2', NULL, 'ft:babbage-002', '(?i)^(ft:)(babbage-002:)(.+)(:)(.*)(:)(.+)$$', NULL, 0.0000016, 0.0000016, NULL, 'TOKENS', 'openai', '{"tokenizerModel": "babbage-002"}'),
|
||||
|
||||
-- https://cloud.google.com/vertex-ai/docs/generative-ai/pricing
|
||||
('cls0k4lqt000008ky1o1s8wd5', NULL, 'gemini-pro', '(?i)^(gemini-pro)(@[a-zA-Z0-9]+)?$', NULL, 0.00000025, 0.0000005, NULL, 'CHARACTERS', NULL, NULL),
|
||||
('cls0jni4t000008jk3kyy803r', NULL, 'chat-bison-32k', '(?i)^(chat-bison-32k)(@[a-zA-Z0-9]+)?$', NULL, 0.00000025, 0.0000005, NULL, 'CHARACTERS', NULL, NULL),
|
||||
('cls0iv12d000108l251gf3038', NULL, 'chat-bison', '(?i)^(chat-bison)(@[a-zA-Z0-9]+)?$', NULL, 0.00000025, 0.0000005, NULL, 'CHARACTERS', NULL, NULL),
|
||||
('cls0jmjt3000108l83ix86w0d', NULL, 'text-bison-32k', '(?i)^(text-bison-32k)(@[a-zA-Z0-9]+)?$', NULL, 0.00000025, 0.0000005, NULL, 'CHARACTERS', NULL, NULL),
|
||||
('cls0juygp000308jk2a6x9my2', NULL, 'text-bison', '(?i)^(text-bison)(@[a-zA-Z0-9]+)?$', NULL, 0.00000025, 0.0000005, NULL, 'CHARACTERS', NULL, NULL),
|
||||
('cls0jungb000208jk12gm4gk1', NULL, 'text-unicorn', '(?i)^(text-unicorn)(@[a-zA-Z0-9]+)?$', NULL, 0.0000025, 0.0000075, NULL, 'CHARACTERS', NULL, NULL),
|
||||
('cls1nyj5q000208l33ne901d8', NULL, 'textembedding-gecko', '(?i)^(textembedding-gecko)(@[a-zA-Z0-9]+)?$', NULL, NULL, NULL, 0.0000001, 'CHARACTERS', NULL, NULL),
|
||||
('cls1nyyjp000308l31gxy1bih', NULL, 'textembedding-gecko-multilingual', '(?i)^(textembedding-gecko-multilingual)(@[a-zA-Z0-9]+)?$', NULL, NULL, NULL, 0.0000001, 'CHARACTERS', NULL, NULL),
|
||||
('cls1nzjt3000508l3dnwad3g0', NULL, 'code-gecko', '(?i)^(code-gecko)(@[a-zA-Z0-9]+)?$', NULL, 0.00000025, 0.0000005, NULL, 'CHARACTERS', NULL, NULL),
|
||||
('cls1nzwx4000608l38va7e4tv', NULL, 'code-bison', '(?i)^(code-bison)(@[a-zA-Z0-9]+)?$', NULL, 0.00000025, 0.0000005, NULL, 'CHARACTERS', NULL, NULL),
|
||||
('cls1o053j000708l39f8g4bgs', NULL, 'code-bison-32k', '(?i)^(code-bison-32k)(@[a-zA-Z0-9]+)?$', NULL, 0.00000025, 0.0000005, NULL, 'CHARACTERS', NULL, NULL),
|
||||
('cls0j33v1000008joagkc4lql', NULL, 'codechat-bison-32k', '(?i)^(codechat-bison-32k)(@[a-zA-Z0-9]+)?$', NULL, 0.00000025, 0.0000005, NULL, 'CHARACTERS', NULL, NULL),
|
||||
('cls0jmc9v000008l8ee6r3gsd', NULL, 'codechat-bison', '(?i)^(codechat-bison)(@[a-zA-Z0-9]+)?$', NULL, 0.00000025, 0.0000005, NULL, 'CHARACTERS', NULL, NULL)
|
||||
@@ -0,0 +1,27 @@
|
||||
-- This is an empty migration.
|
||||
|
||||
DELETE FROM models
|
||||
WHERE id in ('clrkwk4cb000308l5go4b6otm', 'clrntjt89000a08jw0gcdbd5a');
|
||||
|
||||
|
||||
|
||||
INSERT INTO models (
|
||||
id,
|
||||
project_id,
|
||||
model_name,
|
||||
match_pattern,
|
||||
start_date,
|
||||
input_price,
|
||||
output_price,
|
||||
total_price,
|
||||
unit,
|
||||
tokenizer_id,
|
||||
tokenizer_config
|
||||
)
|
||||
VALUES
|
||||
-- https://openai.com/blog/new-embedding-models-and-api-updates
|
||||
|
||||
|
||||
-- fix prices
|
||||
('clrkwk4cb000308l5go4b6otm', NULL, 'gpt-3.5-turbo-16k', '(?i)^(gpt-)(35|3.5)(-turbo-16k)$', NULL, 0.000003, 0.000004, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-3.5-turbo-16k" }'),
|
||||
('clrntjt89000a08jw0gcdbd5a', NULL, 'gpt-3.5-turbo-16k-0613', '(?i)^(gpt-)(35|3.5)(-turbo-16k-0613)$', NULL, 0.000003, 0.000004, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-3.5-turbo-16k-0613" }')
|
||||
+148
-14
@@ -3,13 +3,22 @@
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["tracing"]
|
||||
previewFeatures = ["tracing", "views"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
}
|
||||
|
||||
generator erd {
|
||||
provider = "prisma-erd-generator"
|
||||
ignoreTables = ["_prisma_migrations", "Session", "Account", "Example"]
|
||||
disabled = true
|
||||
ignoreEnums = true
|
||||
output = "database.svg"
|
||||
}
|
||||
|
||||
model Example {
|
||||
@@ -28,6 +37,8 @@ model Account {
|
||||
refresh_token String? // @db.Text
|
||||
access_token String? // @db.Text
|
||||
expires_at Int?
|
||||
expires_in Int?
|
||||
ext_expires_in Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? // @db.Text
|
||||
@@ -79,6 +90,7 @@ model Project {
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
name String
|
||||
cloudConfig Json? @map("cloud_config") // Langfuse Cloud, for zod schema see projectsRouter.ts
|
||||
members Membership[]
|
||||
traces Trace[]
|
||||
observations Observation[]
|
||||
@@ -86,6 +98,9 @@ model Project {
|
||||
dataset Dataset[]
|
||||
RawEvents Events[]
|
||||
invitations MembershipInvitation[]
|
||||
sessions TraceSession[]
|
||||
Prompt Prompt[]
|
||||
Model Model[]
|
||||
|
||||
@@map("projects")
|
||||
}
|
||||
@@ -147,29 +162,51 @@ enum MembershipRole {
|
||||
VIEWER
|
||||
}
|
||||
|
||||
model TraceSession {
|
||||
id String @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
bookmarked Boolean @default(false)
|
||||
public Boolean @default(false)
|
||||
traces Trace[]
|
||||
|
||||
@@id([id, projectId])
|
||||
@@index([projectId])
|
||||
@@index([createdAt])
|
||||
@@map("trace_sessions")
|
||||
}
|
||||
|
||||
model Trace {
|
||||
id String @id @default(cuid())
|
||||
externalId String? @map("external_id")
|
||||
timestamp DateTime @default(now())
|
||||
id String @id @default(cuid())
|
||||
externalId String? @map("external_id")
|
||||
timestamp DateTime @default(now())
|
||||
name String?
|
||||
userId String? @map("user_id")
|
||||
userId String? @map("user_id")
|
||||
metadata Json?
|
||||
release String?
|
||||
version String?
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
public Boolean @default(false)
|
||||
bookmarked Boolean @default(false)
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
public Boolean @default(false)
|
||||
bookmarked Boolean @default(false)
|
||||
tags String[] @default([])
|
||||
input Json?
|
||||
output Json?
|
||||
sessionId String? @map("session_id")
|
||||
session TraceSession? @relation(fields: [sessionId, projectId], references: [id, projectId])
|
||||
|
||||
scores Score[]
|
||||
|
||||
@@unique([projectId, externalId])
|
||||
@@index([projectId])
|
||||
@@index([sessionId])
|
||||
@@index([name])
|
||||
@@index([userId])
|
||||
@@index([externalId])
|
||||
@@index(timestamp)
|
||||
@@index(release)
|
||||
@@index([tags(ops: ArrayOps)], type: Gin)
|
||||
@@map("traces")
|
||||
}
|
||||
|
||||
@@ -190,18 +227,26 @@ model Observation {
|
||||
|
||||
// GENERATION ONLY
|
||||
model String?
|
||||
internalModel String? @map("internal_model")
|
||||
modelParameters Json?
|
||||
input Json?
|
||||
output Json?
|
||||
promptTokens Int @default(0) @map("prompt_tokens")
|
||||
completionTokens Int @default(0) @map("completion_tokens")
|
||||
totalTokens Int @default(0) @map("total_tokens")
|
||||
unit String?
|
||||
inputCost Decimal? @map("input_cost")
|
||||
outputCost Decimal? @map("output_cost")
|
||||
totalCost Decimal? @map("total_cost")
|
||||
completionStartTime DateTime? @map("completion_start_time")
|
||||
scores Score[]
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
derivedDatasetItems DatasetItem[]
|
||||
datasetRunItems DatasetRunItems[]
|
||||
|
||||
promptId String? @map("prompt_id")
|
||||
prompt Prompt? @relation(fields: [promptId], onDelete: SetNull, references: [id])
|
||||
|
||||
@@unique([id, projectId])
|
||||
@@index([traceId])
|
||||
@@index([type])
|
||||
@@ -209,6 +254,7 @@ model Observation {
|
||||
@@index(createdAt)
|
||||
@@index(projectId)
|
||||
@@index(parentObservationId)
|
||||
@@index(model)
|
||||
@@map("observations")
|
||||
}
|
||||
|
||||
@@ -262,6 +308,7 @@ model Pricing {
|
||||
currency String @default("USD")
|
||||
tokenType TokenType @map("token_type")
|
||||
|
||||
@@index(modelName)
|
||||
@@map("pricings")
|
||||
}
|
||||
|
||||
@@ -277,7 +324,6 @@ model CronJobs {
|
||||
model Dataset {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
status DatasetStatus @default(ACTIVE)
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
@@ -358,3 +404,91 @@ model Events {
|
||||
@@index(projectId)
|
||||
@@map("events")
|
||||
}
|
||||
|
||||
model Prompt {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdBy String @map("created_by")
|
||||
|
||||
prompt String
|
||||
name String
|
||||
version Int
|
||||
isActive Boolean @map("is_active")
|
||||
Observation Observation[]
|
||||
|
||||
@@unique([projectId, name, version])
|
||||
@@index([projectId, name, version])
|
||||
@@map("prompts")
|
||||
}
|
||||
|
||||
model Model {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
projectId String? @map("project_id")
|
||||
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
modelName String @map("model_name")
|
||||
matchPattern String @map("match_pattern")
|
||||
startDate DateTime? @map("start_date")
|
||||
inputPrice Decimal? @map("input_price")
|
||||
outputPrice Decimal? @map("output_price")
|
||||
totalPrice Decimal? @map("total_price")
|
||||
unit String // TOKENS, CHARACTERS, MILLISECONDS, SECONDS, or IMAGES
|
||||
tokenizerId String? @map("tokenizer_id")
|
||||
tokenizerConfig Json? @map("tokenizer_config")
|
||||
|
||||
@@unique([projectId, modelName, startDate, unit])
|
||||
@@index([projectId, modelName])
|
||||
@@map("models")
|
||||
}
|
||||
|
||||
// This view is a mix of the observation and model. Once prisma supports
|
||||
// inheritance, we should remove code duplication here.
|
||||
view ObservationView {
|
||||
id String @id @default(cuid())
|
||||
traceId String? @map("trace_id")
|
||||
projectId String @map("project_id")
|
||||
type ObservationType
|
||||
startTime DateTime @default(now()) @map("start_time")
|
||||
endTime DateTime? @map("end_time")
|
||||
name String?
|
||||
metadata Json?
|
||||
parentObservationId String? @map("parent_observation_id")
|
||||
level ObservationLevel @default(DEFAULT)
|
||||
statusMessage String? @map("status_message")
|
||||
version String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
// GENERATION ONLY
|
||||
model String?
|
||||
modelParameters Json?
|
||||
input Json?
|
||||
output Json?
|
||||
promptTokens Int @default(0) @map("prompt_tokens")
|
||||
completionTokens Int @default(0) @map("completion_tokens")
|
||||
totalTokens Int @default(0) @map("total_tokens")
|
||||
unit String?
|
||||
completionStartTime DateTime? @map("completion_start_time")
|
||||
|
||||
promptId String? @map("prompt_id")
|
||||
|
||||
// Model
|
||||
modelId String? @map("model_id")
|
||||
inputPrice Decimal? @map("input_price")
|
||||
outputPrice Decimal? @map("output_price")
|
||||
totalPrice Decimal? @map("total_price")
|
||||
|
||||
// calculated fields
|
||||
calculatedInputCost Decimal? @map("calculated_input_cost")
|
||||
calculatedOutputCost Decimal? @map("calculated_output_cost")
|
||||
calculatedTotalCost Decimal? @map("calculated_total_cost")
|
||||
|
||||
@@map("observations_view")
|
||||
}
|
||||
|
||||
+267
-73
@@ -5,6 +5,7 @@ import {
|
||||
} from "@/src/features/public-api/lib/apiKeys";
|
||||
import { hash } from "bcryptjs";
|
||||
import { parseArgs } from "node:util";
|
||||
import { ModelUsageUnit } from "@/src/constants";
|
||||
|
||||
const options = {
|
||||
environment: { type: "string" },
|
||||
@@ -53,6 +54,25 @@ async function main() {
|
||||
},
|
||||
});
|
||||
|
||||
const prompt = await prisma.prompt.upsert({
|
||||
where: {
|
||||
projectId_name_version: {
|
||||
projectId: seedProjectId,
|
||||
name: "summary-prompt",
|
||||
version: 1,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
name: "summary-prompt",
|
||||
project: { connect: { id: seedProjectId } },
|
||||
prompt: "prompt {{variable}} {{anotherVariable}}",
|
||||
isActive: true,
|
||||
version: 1,
|
||||
createdBy: "user-1",
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
const seedApiKey = {
|
||||
id: "seed-api-key",
|
||||
secret: process.env.SEED_SECRET_KEY ?? "sk-lf-1234567890",
|
||||
@@ -79,9 +99,9 @@ async function main() {
|
||||
|
||||
// Do not run the following for local docker compose setup
|
||||
if (environment === "examples") {
|
||||
const project2 = await prisma.project.create({
|
||||
data: {
|
||||
id: "239ad00f-562f-411d-af14-831c75ddd875",
|
||||
const project2 = await prisma.project.upsert({
|
||||
where: { id: "239ad00f-562f-411d-af14-831c75ddd875" },
|
||||
create: {
|
||||
name: "demo-app",
|
||||
apiKeys: {
|
||||
create: [
|
||||
@@ -100,16 +120,130 @@ async function main() {
|
||||
},
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
const promptIds: string[] = [];
|
||||
|
||||
const prompts = [
|
||||
{
|
||||
id: `prompt-${Math.floor(Math.random() * 1000000000)}`,
|
||||
projectId: project2.id,
|
||||
createdBy: "user-1",
|
||||
prompt: "Prompt 1 content",
|
||||
name: "Prompt 1",
|
||||
version: 1,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: `prompt-${Math.floor(Math.random() * 1000000000)}`,
|
||||
projectId: project2.id,
|
||||
createdBy: "user-1",
|
||||
prompt: "Prompt 2 content",
|
||||
name: "Prompt 2",
|
||||
version: 1,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: `prompt-${Math.floor(Math.random() * 1000000000)}`,
|
||||
projectId: project2.id,
|
||||
createdBy: "API",
|
||||
prompt: "Prompt 3 content",
|
||||
name: "Prompt 3 by API",
|
||||
version: 1,
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
|
||||
for (const prompt of prompts) {
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: prompt.id,
|
||||
projectId: prompt.projectId,
|
||||
createdBy: prompt.createdBy,
|
||||
prompt: prompt.prompt,
|
||||
name: prompt.name,
|
||||
version: prompt.version,
|
||||
isActive: prompt.isActive,
|
||||
},
|
||||
});
|
||||
promptIds.push(prompt.id);
|
||||
}
|
||||
|
||||
const promptVersionsWithVariables = [
|
||||
{
|
||||
id: `prompt-${Math.floor(Math.random() * 1000000000)}`,
|
||||
projectId: project2.id,
|
||||
createdBy: "user-1",
|
||||
prompt: "Prompt 4 version 1 content with {{variable}}",
|
||||
name: "Prompt 4 with variable",
|
||||
version: 1,
|
||||
isActive: false,
|
||||
},
|
||||
{
|
||||
id: `prompt-${Math.floor(Math.random() * 1000000000)}`,
|
||||
projectId: project2.id,
|
||||
createdBy: "user-1",
|
||||
prompt: "Prompt 4 version 2 content with {{variable}}",
|
||||
name: "Prompt 4 with variable",
|
||||
version: 2,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: `prompt-${Math.floor(Math.random() * 1000000000)}`,
|
||||
projectId: project2.id,
|
||||
createdBy: "user-1",
|
||||
prompt: "Prompt 4 version 3 content with {{variable}}",
|
||||
name: "Prompt 4 with variable",
|
||||
version: 3,
|
||||
isActive: false,
|
||||
},
|
||||
];
|
||||
|
||||
for (const version of promptVersionsWithVariables) {
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: version.id,
|
||||
projectId: version.projectId,
|
||||
createdBy: version.createdBy,
|
||||
prompt: version.prompt,
|
||||
name: version.name,
|
||||
version: version.version,
|
||||
isActive: version.isActive,
|
||||
},
|
||||
});
|
||||
promptIds.push(version.id);
|
||||
}
|
||||
const promptName = "Prompt with Longer Name";
|
||||
const projectId = project2.id;
|
||||
const createdBy = "user-1";
|
||||
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
const promptId = `prompt-${Math.floor(Math.random() * 1000000000)}`;
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
projectId: projectId,
|
||||
createdBy: createdBy,
|
||||
prompt: `${promptName} version ${i} content`,
|
||||
name: promptName,
|
||||
version: i,
|
||||
isActive: i === 20,
|
||||
},
|
||||
});
|
||||
promptIds.push(promptId);
|
||||
}
|
||||
|
||||
const generationIds: string[] = [];
|
||||
const envTags = [null, "development", "staging", "production"];
|
||||
const colorTags = [null, "red", "blue", "yellow"];
|
||||
|
||||
for (let i = 0; i < TRACE_VOLUME; i++) {
|
||||
// print progress to console with a progress bar that refreshes every 10 iterations
|
||||
if (i % 10 === 0) {
|
||||
if ((i + 1) % 10 === 0 || i === TRACE_VOLUME - 1) {
|
||||
process.stdout.clearLine(0);
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write(`Seeding ${i} of ${TRACE_VOLUME}`);
|
||||
process.stdout.write(`Seeding ${i + 1} of ${TRACE_VOLUME}`);
|
||||
}
|
||||
// random date within last 90 days, with a linear bias towards more recent dates
|
||||
const traceTs = new Date(
|
||||
@@ -117,6 +251,13 @@ async function main() {
|
||||
Math.floor(Math.random() ** 1.5 * 90 * 24 * 60 * 60 * 1000),
|
||||
);
|
||||
|
||||
const envTag = envTags[Math.floor(Math.random() * envTags.length)];
|
||||
const colorTag = colorTags[Math.floor(Math.random() * colorTags.length)];
|
||||
|
||||
const tags = [envTag, colorTag].filter((tag) => tag !== null);
|
||||
|
||||
const projectId = [project1.id, project2.id][i % 2] as string;
|
||||
|
||||
const trace = await prisma.trace.create({
|
||||
data: {
|
||||
id: `trace-${Math.floor(Math.random() * 1000000000)}`,
|
||||
@@ -127,25 +268,48 @@ async function main() {
|
||||
metadata: {
|
||||
user: `user-${i}@langfuse.com`,
|
||||
},
|
||||
tags: tags as string[],
|
||||
project: {
|
||||
connect: {
|
||||
id: [project1.id, project2.id][i % 2],
|
||||
},
|
||||
connect: { id: projectId },
|
||||
},
|
||||
userId: `user-${i % 10}`,
|
||||
session:
|
||||
Math.random() > 0.3
|
||||
? {
|
||||
connectOrCreate: {
|
||||
where: {
|
||||
id_projectId: {
|
||||
id: `session-${i % 10}`,
|
||||
projectId: projectId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
id: `session-${i % 10}`,
|
||||
projectId: projectId,
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
input:
|
||||
Math.random() > 0.3
|
||||
? "I'm looking for a React component"
|
||||
: undefined,
|
||||
output:
|
||||
Math.random() > 0.3
|
||||
? "What kind of component are you looking for?"
|
||||
: undefined,
|
||||
scores: {
|
||||
createMany: {
|
||||
data: [
|
||||
{
|
||||
name: "latency",
|
||||
value: Math.floor(Math.random() * 20),
|
||||
timestamp: traceTs,
|
||||
},
|
||||
{
|
||||
name: "feedback",
|
||||
value: Math.floor(Math.random() * 3) - 1,
|
||||
timestamp: traceTs,
|
||||
},
|
||||
...(Math.random() > 0.5
|
||||
? [
|
||||
{
|
||||
name: "feedback",
|
||||
value: Math.floor(Math.random() * 3) - 1,
|
||||
timestamp: traceTs,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(Math.random() > 0.7
|
||||
? [
|
||||
{
|
||||
@@ -218,6 +382,20 @@ async function main() {
|
||||
const promptTokens = Math.floor(Math.random() * 1000) + 300;
|
||||
const completionTokens = Math.floor(Math.random() * 500) + 100;
|
||||
|
||||
const models = [
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-4",
|
||||
"gpt-4-32k-0613",
|
||||
"gpt-3.5-turbo-16k-0613",
|
||||
"claude-instant-1",
|
||||
"claude-2.1",
|
||||
"gpt-4-vision-preview",
|
||||
"MIXTRAL-8X7B",
|
||||
];
|
||||
|
||||
const model = models[Math.floor(Math.random() * models.length)];
|
||||
const promptId =
|
||||
promptIds[Math.floor(Math.random() * promptIds.length)];
|
||||
const generation = await prisma.observation.create({
|
||||
data: {
|
||||
type: "GENERATION",
|
||||
@@ -226,6 +404,7 @@ async function main() {
|
||||
endTime: generationTsEnd,
|
||||
name: `generation-${i}-${j}-${k}`,
|
||||
project: { connect: { id: trace.projectId } },
|
||||
prompt: { connect: { id: promptId } },
|
||||
input:
|
||||
Math.random() > 0.5
|
||||
? [
|
||||
@@ -284,7 +463,8 @@ async function main() {
|
||||
|
||||
Remember to import React at the top of your file whenever you're creating a component, because JSX transpiles to 'React.createElement' calls under the hood.`,
|
||||
},
|
||||
model: Math.random() > 0.5 ? "gpt-3.5-turbo" : "gpt-4",
|
||||
model: model,
|
||||
internalModel: model,
|
||||
modelParameters: {
|
||||
temperature:
|
||||
Math.random() > 0.9 ? undefined : Math.random().toFixed(2),
|
||||
@@ -303,25 +483,32 @@ async function main() {
|
||||
totalTokens: promptTokens + completionTokens,
|
||||
parentObservationId: span.id,
|
||||
traceId: trace.id,
|
||||
...{
|
||||
...(Math.random() > 0.5 && trace.projectId === prompt.projectId
|
||||
? { prompt: { connect: { id: prompt.id } } }
|
||||
: {}),
|
||||
},
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.score.create({
|
||||
data: {
|
||||
name: "quality",
|
||||
value: Math.random() * 2 - 1,
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
},
|
||||
});
|
||||
await prisma.score.create({
|
||||
data: {
|
||||
name: "conciseness",
|
||||
value: Math.random() * 2 - 1,
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
},
|
||||
});
|
||||
if (Math.random() > 0.6)
|
||||
await prisma.score.create({
|
||||
data: {
|
||||
name: "quality",
|
||||
value: Math.random() * 2 - 1,
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
},
|
||||
});
|
||||
if (Math.random() > 0.6)
|
||||
await prisma.score.create({
|
||||
data: {
|
||||
name: "conciseness",
|
||||
value: Math.random() * 2 - 1,
|
||||
observationId: generation.id,
|
||||
traceId: trace.id,
|
||||
},
|
||||
});
|
||||
|
||||
generationIds.push(generation.id);
|
||||
|
||||
@@ -353,47 +540,54 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
const dataset = await prisma.dataset.create({
|
||||
data: {
|
||||
name: "demo-dataset",
|
||||
projectId: project2.id,
|
||||
},
|
||||
});
|
||||
|
||||
const datasetRun = await prisma.datasetRuns.create({
|
||||
data: {
|
||||
name: "demo-dataset-run",
|
||||
datasetId: dataset.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (let runNumber = 0; runNumber < 10; runNumber++) {
|
||||
//pick randomly from existingSpanIds
|
||||
const sourceObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
const runObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
|
||||
const datasetItem = await prisma.datasetItem.create({
|
||||
for (let datasetNumber = 0; datasetNumber < 2; datasetNumber++) {
|
||||
const dataset = await prisma.dataset.create({
|
||||
data: {
|
||||
datasetId: dataset.id,
|
||||
sourceObservationId:
|
||||
Math.random() > 0.5 ? sourceObservationId : undefined,
|
||||
input: [
|
||||
{ role: "user", content: "How can i create a React component?" },
|
||||
],
|
||||
expectedOutput:
|
||||
"Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both.",
|
||||
name: `demo-dataset-${datasetNumber}`,
|
||||
projectId: project2.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.datasetRunItems.create({
|
||||
data: {
|
||||
datasetItemId: datasetItem.id,
|
||||
observationId: runObservationId!,
|
||||
datasetRunId: datasetRun.id,
|
||||
},
|
||||
});
|
||||
for (let datasetRunNumber = 0; datasetRunNumber < 2; datasetRunNumber++) {
|
||||
const datasetRun = await prisma.datasetRuns.create({
|
||||
data: {
|
||||
name: `demo-dataset-run-${datasetRunNumber}`,
|
||||
datasetId: dataset.id,
|
||||
},
|
||||
});
|
||||
|
||||
for (let runNumber = 0; runNumber < 10; runNumber++) {
|
||||
//pick randomly from existingSpanIds
|
||||
const sourceObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
const runObservationId =
|
||||
generationIds[Math.floor(Math.random() * generationIds.length)];
|
||||
|
||||
const datasetItem = await prisma.datasetItem.create({
|
||||
data: {
|
||||
datasetId: dataset.id,
|
||||
sourceObservationId:
|
||||
Math.random() > 0.5 ? sourceObservationId : undefined,
|
||||
input: [
|
||||
{
|
||||
role: "user",
|
||||
content: "How can i create a React component?",
|
||||
},
|
||||
],
|
||||
expectedOutput:
|
||||
"Creating a React component can be done in two ways: as a functional component or as a class component. Let's start with a basic example of both.",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.datasetRunItems.create({
|
||||
data: {
|
||||
datasetItemId: datasetItem.id,
|
||||
observationId: runObservationId!,
|
||||
datasetRunId: datasetRun.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
// Description: New model definitions in Langfuse are automatically applied to new observations.
|
||||
// You can optionally run this script to apply new model definitions to existing observations.
|
||||
// See docs: https://langfuse.com/docs/deployment/self-host#migrate-models
|
||||
// Execute: `npm run models:migrate`
|
||||
|
||||
import "dotenv/config";
|
||||
|
||||
import { findModel } from "@/src/server/api/services/EventProcessor";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import lodash from "lodash";
|
||||
import { tokenCount } from "@/src/features/ingest/lib/usage";
|
||||
import { type Prisma } from "@prisma/client";
|
||||
|
||||
async function main() {
|
||||
return await modelMatch();
|
||||
}
|
||||
|
||||
// Call the function
|
||||
main().catch((err) => {
|
||||
console.error("An error occurred:", err);
|
||||
});
|
||||
|
||||
export async function modelMatch() {
|
||||
console.log("Starting model match");
|
||||
const start = Date.now();
|
||||
|
||||
const BATCH_SIZE = 10_000;
|
||||
let continueLoop = true;
|
||||
let index = 0;
|
||||
let totalObservations = 0;
|
||||
|
||||
while (continueLoop) {
|
||||
type ObservationSelect = {
|
||||
model: string | null;
|
||||
id: string;
|
||||
projectId: string;
|
||||
startTime: Date;
|
||||
unit: string | null;
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
input: Prisma.JsonValue;
|
||||
output: Prisma.JsonValue;
|
||||
};
|
||||
|
||||
const observations = await prisma.observation.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
startTime: true,
|
||||
model: true,
|
||||
unit: true,
|
||||
projectId: true,
|
||||
promptTokens: true,
|
||||
completionTokens: true,
|
||||
totalTokens: true,
|
||||
input: true,
|
||||
output: true,
|
||||
},
|
||||
orderBy: {
|
||||
startTime: "desc",
|
||||
},
|
||||
where: {
|
||||
internalModel: null,
|
||||
type: "GENERATION",
|
||||
},
|
||||
take: BATCH_SIZE,
|
||||
skip: index * BATCH_SIZE,
|
||||
});
|
||||
|
||||
console.log(`Found ${observations.length} observations to migrate`);
|
||||
|
||||
type Config = {
|
||||
startTime: Date;
|
||||
model: string;
|
||||
unit: string;
|
||||
projectId: string | null;
|
||||
};
|
||||
|
||||
interface GroupedObservations {
|
||||
[key: string]: ObservationSelect[];
|
||||
}
|
||||
|
||||
const groupedObservations = observations.reduce<GroupedObservations>(
|
||||
(acc, observation) => {
|
||||
const config = {
|
||||
startTime: observation.startTime.toISOString().slice(0, 10),
|
||||
model: observation.model,
|
||||
unit: observation.unit,
|
||||
projectId: observation.projectId,
|
||||
};
|
||||
|
||||
const key = JSON.stringify(config);
|
||||
|
||||
// Ensure the array is initialized before using it
|
||||
acc[key] = acc[key] ?? [];
|
||||
acc[key]?.push(observation);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
let updatedObservations = 0;
|
||||
const dbPromises = [];
|
||||
|
||||
for (const [key, observationsGroup] of Object.entries(
|
||||
groupedObservations,
|
||||
)) {
|
||||
const { startTime, model, unit, projectId } = JSON.parse(key) as Config;
|
||||
|
||||
console.log("Execute key: ", startTime, model, unit, projectId);
|
||||
|
||||
if (!projectId) {
|
||||
throw new Error("No project id");
|
||||
}
|
||||
|
||||
const foundModel = await findModel({
|
||||
event: { projectId, model, unit, startTime: startTime },
|
||||
});
|
||||
|
||||
console.log(
|
||||
"Found model: ",
|
||||
foundModel?.id,
|
||||
" for key: ",
|
||||
key,
|
||||
" with observations: ",
|
||||
observationsGroup.length,
|
||||
);
|
||||
|
||||
if (foundModel) {
|
||||
// find all the observations with all tokens 0 and tokenize them individually
|
||||
const observationsWithAllTokensZero = observationsGroup.filter(
|
||||
(observation) =>
|
||||
observation.promptTokens === 0 &&
|
||||
observation.completionTokens === 0 &&
|
||||
observation.totalTokens === 0,
|
||||
);
|
||||
|
||||
for (const observation of observationsWithAllTokensZero) {
|
||||
console.log("Tokenizing observation: ", observation.id);
|
||||
const newInputCount = tokenCount({
|
||||
model: foundModel,
|
||||
text: observation.input,
|
||||
});
|
||||
const newOutputCount = tokenCount({
|
||||
model: foundModel,
|
||||
text: observation.output,
|
||||
});
|
||||
|
||||
dbPromises.push(
|
||||
prisma.observation.update({
|
||||
where: {
|
||||
id: observation.id,
|
||||
},
|
||||
data: {
|
||||
promptTokens: newInputCount,
|
||||
completionTokens: newOutputCount,
|
||||
totalTokens: (newInputCount ?? 0) + (newOutputCount ?? 0),
|
||||
internalModel: foundModel.modelName,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// for all remaining observations, batch update them with the model id
|
||||
const observationsWithTokens = observationsGroup.filter(
|
||||
(observation) =>
|
||||
observation.promptTokens !== 0 ||
|
||||
observation.completionTokens !== 0 ||
|
||||
observation.totalTokens !== 0,
|
||||
);
|
||||
|
||||
// Push the promise for updating observations into the array
|
||||
lodash.chunk(observationsWithTokens, 32000).map((chunk) => {
|
||||
dbPromises.push(
|
||||
prisma.observation.updateMany({
|
||||
where: {
|
||||
id: {
|
||||
in: chunk.map((observation) => observation.id),
|
||||
},
|
||||
},
|
||||
data: {
|
||||
internalModel: foundModel.modelName,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
updatedObservations += observationsGroup.length;
|
||||
} else {
|
||||
lodash.chunk(observationsGroup, 32000).map((chunk) => {
|
||||
dbPromises.push(
|
||||
prisma.observation.updateMany({
|
||||
where: {
|
||||
id: {
|
||||
in: chunk.map((observation) => observation.id),
|
||||
},
|
||||
},
|
||||
data: {
|
||||
internalModel: "LANGFUSETMPNOMODEL",
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
updatedObservations += observationsGroup.length;
|
||||
}
|
||||
}
|
||||
|
||||
totalObservations += updatedObservations;
|
||||
// Wait for all update operations to complete
|
||||
const promiseChunk = lodash.chunk(dbPromises, 10);
|
||||
|
||||
for (const promises of promiseChunk) {
|
||||
console.log("Waiting for promises to complete", promises.length);
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
console.log(
|
||||
"Updated observations count: ",
|
||||
updatedObservations,
|
||||
" in total: ",
|
||||
totalObservations,
|
||||
);
|
||||
|
||||
console.log(updatedObservations, observations.length);
|
||||
|
||||
if (updatedObservations === 0) {
|
||||
index++;
|
||||
}
|
||||
|
||||
if (observations.length === 0) {
|
||||
console.log("No more observations to migrate");
|
||||
continueLoop = false;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.observation.updateMany({
|
||||
where: {
|
||||
internalModel: "LANGFUSETMPNOMODEL",
|
||||
},
|
||||
data: {
|
||||
internalModel: null,
|
||||
},
|
||||
});
|
||||
|
||||
const end = Date.now();
|
||||
|
||||
console.log(`Model match took ${end - start} ms`);
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We always recommend using the latest version of Langfuse to ensure you get all security updates.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security vulnerabilities to security@langfuse.com.
|
||||
|
||||
We currently do not operate a bug bounty program.
|
||||
@@ -9,7 +9,7 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 0.8,
|
||||
tracesSampleRate: 0.3,
|
||||
|
||||
// Capture Replay for 100% of all sessions,
|
||||
// plus for 100% of sessions with an error
|
||||
|
||||
@@ -7,7 +7,7 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 0.75,
|
||||
tracesSampleRate: 0.3,
|
||||
|
||||
// ...
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: 0.8,
|
||||
tracesSampleRate: 0.3,
|
||||
|
||||
profilesSampleRate: 0.8, // Profiling sample rate is relative to tracesSampleRate
|
||||
profilesSampleRate: 0.3, // Profiling sample rate is relative to tracesSampleRate
|
||||
integrations: [
|
||||
// Add profiling integration to list of integrations
|
||||
new ProfilingIntegration(),
|
||||
|
||||
@@ -2,6 +2,7 @@ import { test, expect } from "@playwright/test";
|
||||
|
||||
test("should redirect to sign-in if not signed in", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.waitForTimeout(2000);
|
||||
await expect(page).toHaveURL("/auth/sign-in");
|
||||
});
|
||||
|
||||
|
||||
@@ -3,28 +3,33 @@ import { test, expect } from "@playwright/test";
|
||||
test("should see new projects dialog open after clicking new project btn", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/auth/sign-in");
|
||||
await page.fill('input[name="email"]', "demo@langfuse.com");
|
||||
await page.goto("auth/sign-up");
|
||||
await page.fill('input[name="name"]', "demo user");
|
||||
await page.fill('input[name="email"]', randomEmailAddress());
|
||||
await page.fill('input[type="password"]', "password");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(2000);
|
||||
await page.waitForTimeout(2000);
|
||||
await page.isVisible("Create new project");
|
||||
expect(await page.innerHTML("data-testid=create-new-project-title")).toBe(
|
||||
"Create new project",
|
||||
);
|
||||
await page.click('[data-testid="create-project-btn"]');
|
||||
await page.waitForTimeout(2000);
|
||||
await page.isVisible('[data-testid="new-project-form"]');
|
||||
await expect(page.locator("data-testid=new-project-form")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Create a project with provided name", async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
|
||||
await page.goto("/auth/sign-in");
|
||||
await page.fill('input[name="email"]', "demo@langfuse.com");
|
||||
await page.fill('input[type="password"]', "password");
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForTimeout(2000);
|
||||
await page.isVisible("Create new project");
|
||||
await page.isVisible('[data-testid="new-project-form"]');
|
||||
await page.click('[data-testid="create-project-btn"]');
|
||||
await page.waitForTimeout(2000);
|
||||
await page.isVisible('[data-testid="new-project-form"]');
|
||||
await expect(page.locator("data-testid=new-project-form")).toBeVisible();
|
||||
await page.fill(
|
||||
'[data-testid="new-project-name-input"]',
|
||||
"my e2e demo project",
|
||||
@@ -33,7 +38,11 @@ test("Create a project with provided name", async ({ page }) => {
|
||||
await page.waitForTimeout(2000);
|
||||
expect(page.url()).toContain("/project/");
|
||||
await page.waitForTimeout(2000);
|
||||
expect(await page.getByTestId("project-title-span-1").textContent()).toBe(
|
||||
expect(await page.getByTestId("project-name").textContent()).toContain(
|
||||
"my e2e demo project",
|
||||
);
|
||||
});
|
||||
|
||||
// random email address to be used in tests
|
||||
const randomEmailAddress = () =>
|
||||
Math.random().toString(36).substring(2, 11) + "@example.com";
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { ModelUsageUnit } from "@/src/constants";
|
||||
import { prisma } from "@/src/server/db";
|
||||
|
||||
describe("cost retrieval tests", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
|
||||
[
|
||||
{
|
||||
testDescription: "prompt and completion tokens",
|
||||
promptTokens: 200,
|
||||
completionTokens: 3000,
|
||||
totalTokens: undefined,
|
||||
inputPrice: "0.0000010",
|
||||
outputPrice: "0.0000020",
|
||||
totalPrice: undefined,
|
||||
expectedPromptTokens: 200,
|
||||
expectedCompletionTokens: 3000,
|
||||
expectedTotalTokens: 0,
|
||||
expectedInputCost: "0.0002", // 200 / 1000 * 0.0010
|
||||
expectedOutputCost: "0.006", // 3000 / 1000 * 0.0020
|
||||
expectedTotalCost: "0.0062", // 0.0002 + 0.006
|
||||
},
|
||||
{
|
||||
testDescription: "missing completion tokens",
|
||||
promptTokens: 200,
|
||||
completionTokens: undefined,
|
||||
totalTokens: undefined,
|
||||
inputPrice: "0.0000010",
|
||||
outputPrice: "0.0000020",
|
||||
totalPrice: undefined,
|
||||
expectedPromptTokens: 200,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 0,
|
||||
expectedInputCost: "0.0002",
|
||||
expectedOutputCost: "0", // completionTokens are set to 0 when ingesting undefined, hence 0 cost
|
||||
expectedTotalCost: "0.0002",
|
||||
},
|
||||
{
|
||||
testDescription: "missing prompt tokens",
|
||||
promptTokens: undefined,
|
||||
completionTokens: 3000,
|
||||
totalTokens: undefined,
|
||||
inputPrice: "0.0000010",
|
||||
outputPrice: "0.0000020",
|
||||
totalPrice: undefined,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 3000,
|
||||
expectedTotalTokens: 0,
|
||||
expectedInputCost: "0", // promptTokens are set to 0 when ingesting undefined, hence 0 cost
|
||||
expectedOutputCost: "0.006",
|
||||
expectedTotalCost: "0.006",
|
||||
},
|
||||
{
|
||||
testDescription: "prompt and completion and total",
|
||||
promptTokens: 200,
|
||||
completionTokens: 3000,
|
||||
totalTokens: 3200,
|
||||
inputPrice: "0.0000010",
|
||||
outputPrice: "0.0000020",
|
||||
totalPrice: undefined,
|
||||
expectedPromptTokens: 200,
|
||||
expectedCompletionTokens: 3000,
|
||||
expectedTotalTokens: 3200,
|
||||
expectedInputCost: "0.0002", // 200 / 1000 * 0.0010
|
||||
expectedOutputCost: "0.006", // 3000 / 1000 * 0.0020
|
||||
expectedTotalCost: "0.0062", // 0.0002 + 0.006
|
||||
},
|
||||
{
|
||||
testDescription: "total only without price",
|
||||
promptTokens: undefined,
|
||||
completionTokens: undefined,
|
||||
totalTokens: 3200,
|
||||
inputPrice: "0.0000010",
|
||||
outputPrice: "0.0000020",
|
||||
totalPrice: undefined,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 3200,
|
||||
expectedInputCost: "0",
|
||||
expectedOutputCost: "0",
|
||||
expectedTotalCost: "0",
|
||||
},
|
||||
{
|
||||
testDescription: "total only",
|
||||
promptTokens: undefined,
|
||||
completionTokens: undefined,
|
||||
totalTokens: 3200,
|
||||
inputPrice: "0.0000010",
|
||||
outputPrice: "0.0000020",
|
||||
totalPrice: "0.1",
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 3200,
|
||||
expectedInputCost: "0",
|
||||
expectedOutputCost: "0",
|
||||
expectedTotalCost: "320",
|
||||
},
|
||||
].forEach((input) => {
|
||||
it(`should calculate cost correctly ${input.testDescription}`, async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: input.inputPrice,
|
||||
outputPrice: input.outputPrice,
|
||||
totalPrice: input.totalPrice,
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
projectId: null,
|
||||
startDate: new Date("2023-12-01"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
const dbTrace = await prisma.trace.create({
|
||||
data: {
|
||||
name: "trace-name",
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.observation.create({
|
||||
data: {
|
||||
traceId: dbTrace.id,
|
||||
type: "GENERATION",
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
model: "gpt-3.5-turbo",
|
||||
internalModel: "gpt-3.5-turbo",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
promptTokens: input.promptTokens,
|
||||
completionTokens: input.completionTokens,
|
||||
totalTokens: input.totalTokens,
|
||||
},
|
||||
});
|
||||
|
||||
const view = await prisma.observationView.findFirst({
|
||||
where: { traceId: dbTrace.id },
|
||||
});
|
||||
|
||||
expect(view?.promptTokens).toBe(input.expectedPromptTokens);
|
||||
expect(view?.completionTokens).toBe(input.expectedCompletionTokens);
|
||||
expect(view?.totalTokens).toBe(input.expectedTotalTokens);
|
||||
|
||||
// calculated cost fields
|
||||
expect(view?.calculatedInputCost?.toString()).toBe(
|
||||
input.expectedInputCost,
|
||||
);
|
||||
expect(view?.calculatedOutputCost?.toString()).toBe(
|
||||
input.expectedOutputCost,
|
||||
);
|
||||
expect(view?.calculatedTotalCost?.toString()).toBe(
|
||||
input.expectedTotalCost,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
[
|
||||
{
|
||||
testDescription: "overwriting project specific model",
|
||||
expectedInputCost: "0.0004", // 200 / 1000 * 0.0010
|
||||
expectedOutputCost: "0.012", // 3000 / 1000 * 0.0020
|
||||
expectedTotalCost: "0.0124", // 0.0002 + 0.006
|
||||
expectedModelId: "model-2",
|
||||
},
|
||||
].forEach((input) => {
|
||||
it(`should calculate cost correctly with multiple models ${input.testDescription}`, async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: "model-1",
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: "0.0000010",
|
||||
outputPrice: "0.0000020",
|
||||
totalPrice: "0.1",
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
projectId: null,
|
||||
startDate: new Date("2023-12-01"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: "model-2",
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: "0.0000020",
|
||||
outputPrice: "0.0000040",
|
||||
totalPrice: undefined,
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
startDate: new Date("2023-12-01"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
},
|
||||
});
|
||||
|
||||
const dbTrace = await prisma.trace.create({
|
||||
data: {
|
||||
name: "trace-name",
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.observation.create({
|
||||
data: {
|
||||
traceId: dbTrace.id,
|
||||
type: "GENERATION",
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
model: "gpt-3.5-turbo",
|
||||
internalModel: "gpt-3.5-turbo",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
promptTokens: 200,
|
||||
completionTokens: 3000,
|
||||
totalTokens: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const view = await prisma.observationView.findFirst({
|
||||
where: { traceId: dbTrace.id },
|
||||
});
|
||||
|
||||
// calculated cost fields
|
||||
expect(view?.modelId).toBe(input.expectedModelId);
|
||||
expect(view?.calculatedInputCost?.toString()).toBe(
|
||||
input.expectedInputCost,
|
||||
);
|
||||
expect(view?.calculatedOutputCost?.toString()).toBe(
|
||||
input.expectedOutputCost,
|
||||
);
|
||||
expect(view?.calculatedTotalCost?.toString()).toBe(
|
||||
input.expectedTotalCost,
|
||||
);
|
||||
});
|
||||
});
|
||||
it(`should prioritize latest models`, async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: "model-1",
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: "0.0000010",
|
||||
outputPrice: "0.0000020",
|
||||
totalPrice: "0.1",
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
projectId: null,
|
||||
startDate: new Date("2023-12-01"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: "model-2",
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: "0.0000020",
|
||||
outputPrice: "0.0000040",
|
||||
totalPrice: undefined,
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
startDate: new Date("2023-12-02"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
const dbTrace = await prisma.trace.create({
|
||||
data: {
|
||||
name: "trace-name",
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.observation.create({
|
||||
data: {
|
||||
traceId: dbTrace.id,
|
||||
type: "GENERATION",
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
model: "gpt-3.5-turbo",
|
||||
internalModel: "gpt-3.5-turbo",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
promptTokens: 200,
|
||||
completionTokens: 3000,
|
||||
totalTokens: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const view = await prisma.observationView.findFirst({
|
||||
where: { traceId: dbTrace.id },
|
||||
});
|
||||
|
||||
console.log(view);
|
||||
|
||||
// calculated cost fields
|
||||
expect(view?.modelId).toBe("model-2");
|
||||
expect(view?.calculatedInputCost?.toString()).toBe("0.0004");
|
||||
expect(view?.calculatedOutputCost?.toString()).toBe("0.012");
|
||||
expect(view?.calculatedTotalCost?.toString()).toBe("0.0124");
|
||||
});
|
||||
|
||||
it(`should prioritize old model if the latest model is not own one`, async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: "model-1",
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: "0.0010",
|
||||
outputPrice: "0.0020",
|
||||
totalPrice: "0.1",
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
startDate: new Date("2023-12-02"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: "model-2",
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: "0.0000020",
|
||||
outputPrice: "0.0000040",
|
||||
totalPrice: undefined,
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
startDate: new Date("2023-12-01"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
const dbTrace = await prisma.trace.create({
|
||||
data: {
|
||||
name: "trace-name",
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.observation.create({
|
||||
data: {
|
||||
traceId: dbTrace.id,
|
||||
type: "GENERATION",
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
model: "gpt-3.5-turbo",
|
||||
internalModel: "gpt-3.5-turbo",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
promptTokens: 200,
|
||||
completionTokens: 3000,
|
||||
totalTokens: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const view = await prisma.observationView.findFirst({
|
||||
where: { traceId: dbTrace.id },
|
||||
});
|
||||
|
||||
console.log(view);
|
||||
|
||||
// calculated cost fields
|
||||
expect(view?.modelId).toBe("model-2");
|
||||
expect(view?.calculatedInputCost?.toString()).toBe("0.0004");
|
||||
expect(view?.calculatedOutputCost?.toString()).toBe("0.012");
|
||||
expect(view?.calculatedTotalCost?.toString()).toBe("0.0124");
|
||||
});
|
||||
|
||||
it(`should prioritize user provided cost`, async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: "model-1",
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: "0.0010",
|
||||
outputPrice: "0.0020",
|
||||
totalPrice: "0.1",
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
startDate: new Date("2023-12-02"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: "model-2",
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: "0.0020",
|
||||
outputPrice: "0.0040",
|
||||
totalPrice: undefined,
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
startDate: new Date("2023-12-01"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
const dbTrace = await prisma.trace.create({
|
||||
data: {
|
||||
name: "trace-name",
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.observation.create({
|
||||
data: {
|
||||
traceId: dbTrace.id,
|
||||
type: "GENERATION",
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
model: "gpt-3.5-turbo",
|
||||
internalModel: "gpt-3.5-turbo",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
promptTokens: 200,
|
||||
completionTokens: 3000,
|
||||
totalTokens: undefined,
|
||||
inputCost: "1",
|
||||
outputCost: "2",
|
||||
totalCost: "3",
|
||||
},
|
||||
});
|
||||
|
||||
const view = await prisma.observationView.findFirst({
|
||||
where: { traceId: dbTrace.id },
|
||||
});
|
||||
|
||||
console.log(view);
|
||||
|
||||
// calculated cost fields
|
||||
expect(view?.modelId).toBe("model-2");
|
||||
expect(view?.calculatedInputCost?.toString()).toBe("1");
|
||||
expect(view?.calculatedOutputCost?.toString()).toBe("2");
|
||||
expect(view?.calculatedTotalCost?.toString()).toBe("3");
|
||||
});
|
||||
});
|
||||
@@ -241,12 +241,12 @@ describe("/api/public/events API Endpoint", () => {
|
||||
|
||||
expect(createSpan.status).toBe(200);
|
||||
|
||||
expect(dbEvent?.id).toBe(spanId);
|
||||
expect(dbEvent?.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbEvent?.name).toBe(generationName);
|
||||
expect(dbEvent?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbEvent?.input).toEqual({ key: "value" });
|
||||
expect(dbEvent?.metadata).toEqual({ key: "value" });
|
||||
expect(dbEvent?.version).toBe("2.0.0");
|
||||
expect(dbEvent.id).toBe(spanId);
|
||||
expect(dbEvent.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbEvent.name).toBe(generationName);
|
||||
expect(dbEvent.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbEvent.input).toEqual({ key: "value" });
|
||||
expect(dbEvent.metadata).toEqual({ key: "value" });
|
||||
expect(dbEvent.version).toBe("2.0.0");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
|
||||
import { TestRouter } from "@/src/__tests__/fixtures/TestRouter";
|
||||
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
|
||||
import { LocationMock } from "@jedmao/location";
|
||||
import NextAdapterPages from "next-query-params/pages";
|
||||
import { useRouter } from "next/router";
|
||||
import { QueryParamProvider } from "use-query-params";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
jest.mock("next/router", () => ({
|
||||
useRouter: jest.fn(),
|
||||
}));
|
||||
|
||||
const { location: savedLocation } = window;
|
||||
|
||||
// The test for the useOrderByState hook
|
||||
describe("useOrderByState hook", () => {
|
||||
let testRouter: TestRouter;
|
||||
let locationMock: LocationMock;
|
||||
|
||||
beforeAll(() => {
|
||||
// @ts-expect-error - TS only allows to delete optional params; but here we want to delete it
|
||||
// as it will be set in beforeEach
|
||||
delete window.location;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
locationMock = new LocationMock("https://langfuse.com");
|
||||
testRouter = new TestRouter(locationMock);
|
||||
window.location = locationMock;
|
||||
|
||||
(useRouter as jest.Mock).mockReturnValue(testRouter);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
window.location = savedLocation;
|
||||
});
|
||||
|
||||
test("orderBy takes the default value if no url param is given", () => {
|
||||
const result = render(
|
||||
<QueryParamProvider adapter={NextAdapterPages}>
|
||||
<UseOrderByStateExample />
|
||||
</QueryParamProvider>,
|
||||
);
|
||||
|
||||
expect(result.queryByText(/column Column 1, order DESC/)).toBeTruthy();
|
||||
});
|
||||
|
||||
test("setOrderBy updates orderBy query param", () => {
|
||||
const result = render(
|
||||
<QueryParamProvider adapter={NextAdapterPages}>
|
||||
<UseOrderByStateExample />
|
||||
</QueryParamProvider>,
|
||||
);
|
||||
|
||||
result.queryByText(/Set Order/)!.click();
|
||||
result.rerender(
|
||||
<QueryParamProvider adapter={NextAdapterPages}>
|
||||
<UseOrderByStateExample />
|
||||
</QueryParamProvider>,
|
||||
);
|
||||
|
||||
expect(result.queryByText(/column Column 2, order ASC/)).toBeTruthy();
|
||||
expect(locationMock.searchParams.get("orderBy")).toBe(
|
||||
"column-Column 2_order-ASC",
|
||||
);
|
||||
});
|
||||
|
||||
test("orderBy reads the given param from the url", () => {
|
||||
locationMock.replace(
|
||||
"https://langfuse.com?orderBy=column-Column 3_order-DESC",
|
||||
);
|
||||
|
||||
const result = render(
|
||||
<QueryParamProvider adapter={NextAdapterPages}>
|
||||
<UseOrderByStateExample />
|
||||
</QueryParamProvider>,
|
||||
);
|
||||
|
||||
expect(result.queryByText(/column Column 3, order DESC/)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
const UseOrderByStateExample = () => {
|
||||
const [orderBy, setOrderBy] = useOrderByState({
|
||||
column: "Column 1",
|
||||
order: "DESC",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>{`column ${orderBy?.column}, order ${orderBy?.order}`}</h1>
|
||||
<button
|
||||
onClick={() => {
|
||||
setOrderBy({
|
||||
column: "Column 2",
|
||||
order: "ASC",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Set Order
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
|
||||
import { tracesTableCols } from "@/src/server/api/definitions/tracesTable";
|
||||
|
||||
// The test for the orderByToPrisma function
|
||||
describe("orderByToPrisma (Convert orderBy to Prisma.sql)", () => {
|
||||
test("orderByToPrisma throws error for orderBy column not included in column defs", () => {
|
||||
expect(() =>
|
||||
orderByToPrismaSql(
|
||||
{
|
||||
column: "InvalidCol",
|
||||
order: "ASC",
|
||||
},
|
||||
tracesTableCols,
|
||||
),
|
||||
).toThrow(/Invalid filter column: InvalidCol/);
|
||||
});
|
||||
|
||||
test("orderByToPrisma throws error for orderBy order that is not valid", () => {
|
||||
expect(() =>
|
||||
orderByToPrismaSql(
|
||||
{
|
||||
column: "latency",
|
||||
order: "test" as "ASC" | "DESC",
|
||||
},
|
||||
tracesTableCols,
|
||||
),
|
||||
).toThrow(/Invalid order: test/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
import { type Router } from "next/router";
|
||||
import type { UrlObject } from "url";
|
||||
import { type LocationMock } from "@jedmao/location";
|
||||
|
||||
type PartialRouter = Partial<Router>;
|
||||
|
||||
export const BASE_URL = "https://langfuse.com";
|
||||
|
||||
/**
|
||||
* A Router to be used for testing which provides the bare minimum needed
|
||||
* for the useQueryParam(s) hook and NextAdapter to work.
|
||||
*/
|
||||
export class TestRouter implements PartialRouter {
|
||||
isReady = true;
|
||||
pathname = "/";
|
||||
private currentUrl = "";
|
||||
private history: string[] = [];
|
||||
|
||||
constructor(private locationMock: LocationMock) {}
|
||||
|
||||
replace = (url: string | UrlObject) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
|
||||
this.locationMock.assign(`${BASE_URL}${url}`);
|
||||
this.currentUrl = TestRouter.getURLString(url);
|
||||
this.locationMock.assign(`${BASE_URL}${this.currentUrl}`);
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
push = (url: string | UrlObject) => {
|
||||
this.history.push(this.currentUrl);
|
||||
this.currentUrl = TestRouter.getURLString(url);
|
||||
this.locationMock.assign(`${BASE_URL}${this.currentUrl}`);
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
setIsReady = (isReady: boolean) => {
|
||||
this.isReady = isReady;
|
||||
};
|
||||
|
||||
get asPath() {
|
||||
return this.pathname;
|
||||
}
|
||||
|
||||
static getURLString(url: string | UrlObject): string {
|
||||
if (typeof url === "string") {
|
||||
return url;
|
||||
}
|
||||
return `${url.pathname}${url.search}`;
|
||||
}
|
||||
|
||||
getParams(): URLSearchParams {
|
||||
return new URL(`${BASE_URL}${this.currentUrl}`).searchParams;
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1,161 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { ModelUsageUnit } from "@/src/constants";
|
||||
import { prisma } from "@/src/server/db";
|
||||
|
||||
describe("/api/public/generations API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create generation after trace", async () => {
|
||||
await pruneDatabase();
|
||||
[
|
||||
{
|
||||
usage: {
|
||||
input: 100,
|
||||
output: 200,
|
||||
total: 100,
|
||||
unit: ModelUsageUnit.Characters,
|
||||
},
|
||||
expectedUnit: ModelUsageUnit.Characters,
|
||||
expectedPromptTokens: 100,
|
||||
expectedCompletionTokens: 200,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
total: 100,
|
||||
unit: ModelUsageUnit.Characters,
|
||||
},
|
||||
expectedUnit: ModelUsageUnit.Characters,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
total: 100,
|
||||
},
|
||||
expectedUnit: null,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
promptTokens: 100,
|
||||
completionTokens: 200,
|
||||
totalTokens: 100,
|
||||
},
|
||||
expectedPromptTokens: 100,
|
||||
expectedCompletionTokens: 200,
|
||||
expectedTotalTokens: 100,
|
||||
expectedUnit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
totalTokens: 100,
|
||||
},
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
expectedUnit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
{
|
||||
usage: undefined,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 0,
|
||||
expectedUnit: null,
|
||||
},
|
||||
{
|
||||
usage: null,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 0,
|
||||
expectedUnit: null,
|
||||
},
|
||||
{
|
||||
usage: {},
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 0,
|
||||
expectedUnit: null,
|
||||
},
|
||||
].forEach((testConfig) => {
|
||||
it(`should create generation after trace 1 ${JSON.stringify(
|
||||
testConfig,
|
||||
)}`, async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
const traceId = uuidv4();
|
||||
const traceId = uuidv4();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
});
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
expect(createGeneration.status).toBe(200);
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.id).toBe(traceId);
|
||||
|
||||
const generationId = uuidv4();
|
||||
const createGeneration = await makeAPICall(
|
||||
"POST",
|
||||
"/api/public/generations",
|
||||
{
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
prompt: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
usage: testConfig.usage,
|
||||
},
|
||||
);
|
||||
|
||||
expect(createGeneration.status).toBe(200);
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(traceId);
|
||||
expect(dbGeneration?.name).toBe("generation-name");
|
||||
expect(dbGeneration?.startTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.endTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.model).toBe("model-name");
|
||||
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.version).toBe("2.0.0");
|
||||
expect(dbGeneration?.unit).toBe(testConfig.expectedUnit);
|
||||
expect(dbGeneration?.promptTokens).toBe(testConfig.expectedPromptTokens);
|
||||
expect(dbGeneration?.completionTokens).toBe(
|
||||
testConfig.expectedCompletionTokens,
|
||||
);
|
||||
expect(dbGeneration?.totalTokens).toBe(testConfig.expectedTotalTokens);
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(traceId);
|
||||
expect(dbGeneration?.name).toBe("generation-name");
|
||||
expect(dbGeneration?.startTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbGeneration?.model).toBe("model-name");
|
||||
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should create generation before trace", async () => {
|
||||
@@ -339,8 +426,8 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
name: generationName,
|
||||
},
|
||||
});
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(externalTraceId);
|
||||
expect(dbGeneration.id).toBe(generationId);
|
||||
expect(dbGeneration.traceId).toBe(externalTraceId);
|
||||
|
||||
const dbTraces = await prisma.trace.findMany();
|
||||
expect(dbTraces.length).toBe(0);
|
||||
@@ -383,18 +470,18 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
|
||||
expect(createGeneration.status).toBe(200);
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbGeneration?.name).toBe(generationName);
|
||||
expect(dbGeneration?.startTime).toEqual(
|
||||
expect(dbGeneration.id).toBe(generationId);
|
||||
expect(dbGeneration.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbGeneration.name).toBe(generationName);
|
||||
expect(dbGeneration.startTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbGeneration?.model).toBe("model-name");
|
||||
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.version).toBe("2.0.0");
|
||||
expect(dbGeneration.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbGeneration.model).toBe("model-name");
|
||||
expect(dbGeneration.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should update generation", async () => {
|
||||
|
||||
@@ -1,19 +1,565 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { v4 } from "uuid";
|
||||
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { ModelUsageUnit } from "@/src/constants";
|
||||
import { cleanEvent } from "@/src/pages/api/public/ingestion";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
describe("/api/public/ingestion API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create trace and generation", async () => {
|
||||
[
|
||||
{
|
||||
usage: {
|
||||
input: 100,
|
||||
output: 200,
|
||||
total: 100,
|
||||
unit: ModelUsageUnit.Characters,
|
||||
inputCost: 123,
|
||||
outputCost: 456,
|
||||
totalCost: 789,
|
||||
},
|
||||
expectedUnit: ModelUsageUnit.Characters,
|
||||
expectedPromptTokens: 100,
|
||||
expectedCompletionTokens: 200,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
total: 100,
|
||||
unit: ModelUsageUnit.Characters,
|
||||
},
|
||||
expectedUnit: ModelUsageUnit.Characters,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
total: 100,
|
||||
unit: ModelUsageUnit.Milliseconds,
|
||||
},
|
||||
expectedUnit: ModelUsageUnit.Milliseconds,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
unit: ModelUsageUnit.Images,
|
||||
},
|
||||
expectedUnit: ModelUsageUnit.Images,
|
||||
expectedPromptTokens: 1,
|
||||
expectedCompletionTokens: 2,
|
||||
expectedTotalTokens: 3,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
input: 30,
|
||||
output: 10,
|
||||
unit: ModelUsageUnit.Seconds,
|
||||
},
|
||||
expectedUnit: ModelUsageUnit.Seconds,
|
||||
expectedPromptTokens: 30,
|
||||
expectedCompletionTokens: 10,
|
||||
expectedTotalTokens: 40,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
total: 100,
|
||||
},
|
||||
expectedUnit: null,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
promptTokens: 100,
|
||||
completionTokens: 200,
|
||||
totalTokens: 100,
|
||||
},
|
||||
expectedPromptTokens: 100,
|
||||
expectedCompletionTokens: 200,
|
||||
expectedTotalTokens: 100,
|
||||
expectedUnit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
totalTokens: 100,
|
||||
},
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
expectedUnit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
{
|
||||
usage: undefined,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 0,
|
||||
expectedUnit: null,
|
||||
},
|
||||
{
|
||||
usage: null,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 0,
|
||||
expectedUnit: null,
|
||||
},
|
||||
{
|
||||
usage: {},
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 0,
|
||||
expectedUnit: null,
|
||||
},
|
||||
].forEach((testConfig) => {
|
||||
it(`should create trace, generation and score without matching models ${JSON.stringify(
|
||||
testConfig,
|
||||
)}`, async () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
const spanId = v4();
|
||||
const scoreId = v4();
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
metadata: {
|
||||
sdk_verion: "1.0.0",
|
||||
sdk_name: "python",
|
||||
},
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
tags: ["tag-1", "tag-2"],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
modelParameters: { key: "value" },
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-update",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
type: "GENERATION",
|
||||
output: { key: "this is a great gpt output" },
|
||||
usage: testConfig.usage,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
type: "SPAN",
|
||||
name: "span-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "score-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: scoreId,
|
||||
name: "score-name",
|
||||
value: 100.5,
|
||||
traceId: traceId,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
console.log("response body", response.body);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.name).toBe("trace-name");
|
||||
expect(dbTrace[0]?.release).toBe("1.0.0");
|
||||
expect(dbTrace[0]?.externalId).toBeNull();
|
||||
expect(dbTrace[0]?.version).toBe("2.0.0");
|
||||
expect(dbTrace[0]?.projectId).toBe(
|
||||
"7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
);
|
||||
expect(dbTrace[0]?.tags).toEqual(["tag-1", "tag-2"]);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(traceId);
|
||||
expect(dbGeneration?.name).toBe("generation-name");
|
||||
expect(dbGeneration?.startTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.endTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.model).toBeNull();
|
||||
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.version).toBe("2.0.0");
|
||||
expect(dbGeneration?.internalModel).toBeNull();
|
||||
expect(dbGeneration?.promptTokens).toEqual(
|
||||
testConfig.expectedPromptTokens,
|
||||
);
|
||||
expect(dbGeneration?.completionTokens).toEqual(
|
||||
testConfig.expectedCompletionTokens,
|
||||
);
|
||||
expect(dbGeneration?.totalTokens).toEqual(testConfig.expectedTotalTokens);
|
||||
expect(dbGeneration?.unit).toEqual(testConfig.expectedUnit);
|
||||
expect(dbGeneration?.output).toEqual({
|
||||
key: "this is a great gpt output",
|
||||
});
|
||||
|
||||
const dbSpan = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: spanId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbSpan?.id).toBe(spanId);
|
||||
expect(dbSpan?.name).toBe("span-name");
|
||||
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan?.endTime).toEqual(new Date("2021-01-:00:00.000Z"));
|
||||
expect(dbSpan?.input).toEqual({ input: "value" });
|
||||
expect(dbSpan?.metadata).toEqual({ meta: "value" });
|
||||
expect(dbSpan?.version).toBe("2.0.0");
|
||||
|
||||
const dbScore = await prisma.score.findUnique({
|
||||
where: {
|
||||
id: scoreId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbScore?.id).toBe(scoreId);
|
||||
expect(dbScore?.traceId).toBe(traceId);
|
||||
expect(dbScore?.name).toBe("score-name");
|
||||
expect(dbScore?.value).toBe(100.5);
|
||||
expect(dbScore?.observationId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
[
|
||||
{
|
||||
observationExternalModel: "gpt-3.5",
|
||||
observationStartTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
modelUnit: ModelUsageUnit.Tokens,
|
||||
expectedInternalModel: "gpt-3.5-turbo",
|
||||
expectedPromptTokens: 5,
|
||||
expectedCompletionTokens: 7,
|
||||
models: [
|
||||
{
|
||||
modelName: "gpt-3.5-turbo",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
observationExternalModel: "gpt-3.5",
|
||||
observationStartTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
modelUnit: ModelUsageUnit.Tokens,
|
||||
expectedInternalModel: "gpt-3.5-turbo",
|
||||
expectedPromptTokens: 5,
|
||||
expectedCompletionTokens: 7,
|
||||
models: [
|
||||
{
|
||||
modelName: "gpt-3.5-turbo",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: null,
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
observationExternalModel: "GPT-3.5",
|
||||
observationStartTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
modelUnit: ModelUsageUnit.Tokens,
|
||||
expectedInternalModel: "gpt-3.5-turbo",
|
||||
expectedPromptTokens: 5,
|
||||
expectedCompletionTokens: 7,
|
||||
models: [
|
||||
{
|
||||
modelName: "gpt-3.5-turbo",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
observationExternalModel: "GPT-3.5",
|
||||
observationStartTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
modelUnit: ModelUsageUnit.Tokens,
|
||||
expectedInternalModel: "gpt-3.5-turbo",
|
||||
expectedPromptTokens: 5,
|
||||
expectedCompletionTokens: 7,
|
||||
models: [
|
||||
{
|
||||
modelName: "gpt-3.5-turbo",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
{
|
||||
modelName: "gpt-3.5-turbo-new",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T10:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
observationExternalModel: "GPT-3.5",
|
||||
observationStartTime: new Date("2021-01-02T00:00:00.000Z"),
|
||||
modelUnit: ModelUsageUnit.Tokens,
|
||||
expectedInternalModel: "gpt-3.5-turbo",
|
||||
expectedPromptTokens: 5,
|
||||
expectedCompletionTokens: 7,
|
||||
models: [
|
||||
{
|
||||
modelName: "gpt-3.5-turbo-new",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
{
|
||||
modelName: "gpt-3.5-turbo",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T10:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
tokenizerModel: "gpt-3.5-turbo",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
observationExternalModel: "ft:gpt-3.5-turbo-1106:my-org:custom_suffix:id",
|
||||
observationStartTime: new Date("2022-01-01T10:00:00.000Z"),
|
||||
modelUnit: ModelUsageUnit.Tokens,
|
||||
expectedInternalModel: "ft:gpt-3.5-turbo-1106",
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
models: [
|
||||
{
|
||||
modelName: "ft:gpt-3.5-turbo-1106",
|
||||
matchPattern: "(?i)^(ft:)(gpt-3.5-turbo-1106:)(.+)(:)(.*)(:)(.+)$",
|
||||
startDate: new Date("2022-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
observationExternalModel: "ft:babbage-002:my-org#2:custom_suffix-2:id",
|
||||
observationStartTime: new Date("2022-01-01T10:00:00.000Z"),
|
||||
modelUnit: ModelUsageUnit.Tokens,
|
||||
expectedInternalModel: "ft:babbage-002",
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
models: [
|
||||
{
|
||||
modelName: "ft:babbage-002",
|
||||
matchPattern: "(?i)^(ft:)(babbage-002:)(.+)(:)(.*)(:)(.+)$",
|
||||
startDate: new Date("2022-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
observationExternalModel: "GPT-4",
|
||||
observationStartTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
modelUnit: ModelUsageUnit.Tokens,
|
||||
expectedInternalModel: null,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
models: [
|
||||
{
|
||||
modelName: "gpt-3.5-turbo",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
observationExternalModel: "GPT-3",
|
||||
observationStartTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
modelUnit: ModelUsageUnit.Characters,
|
||||
expectedInternalModel: null,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
models: [
|
||||
{
|
||||
modelName: "gpt-3.5-turbo",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
],
|
||||
},
|
||||
].forEach((testConfig) => {
|
||||
it(`should match observations to internal models ${JSON.stringify(
|
||||
testConfig,
|
||||
)}`, async () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
|
||||
await Promise.all(
|
||||
testConfig.models.map(async (model) =>
|
||||
prisma.model.create({
|
||||
data: {
|
||||
modelName: model.modelName,
|
||||
matchPattern: model.matchPattern,
|
||||
startDate: model.startDate,
|
||||
unit: model.unit,
|
||||
tokenizerId: model.tokenizerId,
|
||||
tokenizerConfig: {
|
||||
tokensPerMessage: 3,
|
||||
tokensPerName: 1,
|
||||
tokenizerModel:
|
||||
"tokenizerModel" in model
|
||||
? model.tokenizerModel
|
||||
: model.modelName,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
metadata: {
|
||||
sdk_verion: "1.0.0",
|
||||
sdk_name: "python",
|
||||
},
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
startTime: testConfig.observationStartTime.toISOString(),
|
||||
model: testConfig.observationExternalModel,
|
||||
usage: {
|
||||
unit: testConfig.modelUnit,
|
||||
},
|
||||
input: "This is a great prompt",
|
||||
output: "This is a great gpt output",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
console.log("response body", response.body);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(traceId);
|
||||
expect(dbGeneration?.name).toBe("generation-name");
|
||||
expect(dbGeneration?.startTime).toEqual(testConfig.observationStartTime);
|
||||
expect(dbGeneration?.model).toBe(testConfig.observationExternalModel);
|
||||
expect(dbGeneration?.promptTokens).toBe(testConfig.expectedPromptTokens);
|
||||
expect(dbGeneration?.completionTokens).toBe(
|
||||
testConfig.expectedCompletionTokens,
|
||||
);
|
||||
expect(dbGeneration?.internalModel).toBe(
|
||||
testConfig.expectedInternalModel,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create and update all events", async () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
const spanId = v4();
|
||||
const eventId = v4();
|
||||
const scoreId = v4();
|
||||
|
||||
const exception = `
|
||||
ERROR langfuse:callback.py:677 'model_name'
|
||||
Traceback (most recent call last):
|
||||
File "/Users/maximiliandeichmann/development/github.com/langfuse/langfuse-python/langfuse/callback.py", line 674, in __on_llm_action
|
||||
model_name = kwargs["invocation_params"]["model_name"]
|
||||
KeyError: 'model_name'
|
||||
`;
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
@@ -22,55 +568,56 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
type: "span-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
modelParameters: { key: "value" },
|
||||
input: { key: "value" },
|
||||
metadata: { key: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-update",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
type: "GENERATION",
|
||||
output: { key: "this is a great gpt output" },
|
||||
usage: { promptTokens: 400, completionTokens: 1000 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "observation-create",
|
||||
type: "span-update",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: spanId,
|
||||
traceId: traceId,
|
||||
type: "SPAN",
|
||||
name: "span-name",
|
||||
startTime: "2021-01-01T00:00:00.000Z",
|
||||
endTime: "2021-01-01T00:00:00.000Z",
|
||||
input: { input: "value" },
|
||||
metadata: { meta: "value" },
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "generation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
parentObservationId: spanId,
|
||||
modelParameters: { someKey: ["user-1", "user-2"] },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "generation-update",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
name: "generation-name",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "event-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: eventId,
|
||||
traceId: traceId,
|
||||
name: "event-name",
|
||||
parentObservationId: generationId,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -80,8 +627,17 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
body: {
|
||||
id: scoreId,
|
||||
name: "score-name",
|
||||
value: 100.5,
|
||||
traceId: traceId,
|
||||
value: 100.5,
|
||||
observationId: generationId,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "sdk-log",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
log: exception,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -91,17 +647,23 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
name: "trace-name",
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toBeGreaterThan(0);
|
||||
expect(dbTrace[0]?.name).toBe("trace-name");
|
||||
expect(dbTrace[0]?.release).toBe("1.0.0");
|
||||
expect(dbTrace[0]?.externalId).toBeNull();
|
||||
expect(dbTrace[0]?.version).toBe("2.0.0");
|
||||
expect(dbTrace[0]?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
|
||||
|
||||
const dbSpan = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: spanId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbSpan?.id).toBe(spanId);
|
||||
expect(dbSpan?.name).toBe("span-name");
|
||||
expect(dbSpan?.traceId).toBe(traceId);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
@@ -111,34 +673,21 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.traceId).toBe(traceId);
|
||||
expect(dbGeneration?.name).toBe("generation-name");
|
||||
expect(dbGeneration?.startTime).toEqual(
|
||||
new Date("2021-01-01T00:00:00.000Z"),
|
||||
);
|
||||
expect(dbGeneration?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbGeneration?.model).toBeNull();
|
||||
expect(dbGeneration?.modelParameters).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.input).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.metadata).toEqual({ key: "value" });
|
||||
expect(dbGeneration?.version).toBe("2.0.0");
|
||||
expect(dbGeneration?.promptTokens).toEqual(400);
|
||||
expect(dbGeneration?.completionTokens).toEqual(1000);
|
||||
expect(dbGeneration?.output).toEqual({
|
||||
key: "this is a great gpt output",
|
||||
expect(dbGeneration?.parentObservationId).toBe(spanId);
|
||||
expect(dbGeneration?.modelParameters).toEqual({
|
||||
someKey: ["user-1", "user-2"],
|
||||
});
|
||||
|
||||
const dbSpan = await prisma.observation.findUnique({
|
||||
const dbEvent = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: spanId,
|
||||
id: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbSpan?.id).toBe(spanId);
|
||||
expect(dbSpan?.name).toBe("span-name");
|
||||
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan?.endTime).toEqual(new Date("2021-01-:00:00.000Z"));
|
||||
expect(dbSpan?.input).toEqual({ input: "value" });
|
||||
expect(dbSpan?.metadata).toEqual({ meta: "value" });
|
||||
expect(dbSpan?.version).toBe("2.0.0");
|
||||
expect(dbEvent?.id).toBe(eventId);
|
||||
expect(dbEvent?.traceId).toBe(traceId);
|
||||
expect(dbEvent?.name).toBe("event-name");
|
||||
expect(dbEvent?.parentObservationId).toBe(generationId);
|
||||
|
||||
const dbScore = await prisma.score.findUnique({
|
||||
where: {
|
||||
@@ -148,9 +697,21 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
|
||||
expect(dbScore?.id).toBe(scoreId);
|
||||
expect(dbScore?.traceId).toBe(traceId);
|
||||
expect(dbScore?.name).toBe("score-name");
|
||||
expect(dbScore?.observationId).toBe(generationId);
|
||||
expect(dbScore?.value).toBe(100.5);
|
||||
expect(dbScore?.observationId).toBeNull();
|
||||
|
||||
const logEvent = await prisma.events.findFirst({
|
||||
where: {
|
||||
data: {
|
||||
path: ["body", "log"],
|
||||
string_contains: "ERROR",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(logEvent).toBeDefined();
|
||||
expect(logEvent).not.toBeFalsy();
|
||||
expect(JSON.stringify(logEvent?.data)).toContain("KeyError: 'model_name'");
|
||||
});
|
||||
|
||||
it("should upsert threats", async () => {
|
||||
@@ -354,6 +915,21 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
modelName: "gpt-3.5",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
tokenizerConfig: {
|
||||
tokensPerMessage: 3,
|
||||
tokensPerName: 1,
|
||||
tokenizerModel: "gpt-3.5-turbo",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
@@ -421,6 +997,21 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
modelName: "gpt-3.5",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
tokenizerConfig: {
|
||||
tokensPerMessage: 3,
|
||||
tokensPerName: 1,
|
||||
tokenizerModel: "gpt-3.5-turbo",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
@@ -530,6 +1121,66 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbTrace[0]?.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should not override a trace from a different project", async () => {
|
||||
const traceId = v4();
|
||||
const newProjectId = v4();
|
||||
|
||||
await prisma.project.create({
|
||||
data: {
|
||||
id: newProjectId,
|
||||
name: "another-project",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.trace.create({
|
||||
data: {
|
||||
id: traceId,
|
||||
project: { connect: { id: newProjectId } },
|
||||
},
|
||||
});
|
||||
|
||||
const responseOne = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(responseOne.status).toBe(207);
|
||||
|
||||
console.log(responseOne.body);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
|
||||
const errors = responseOne.body.errors;
|
||||
|
||||
expect(errors).toBeDefined();
|
||||
console.log(errors);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
expect(errors.length).toBe(1);
|
||||
|
||||
const dbTrace = await prisma.trace.findMany({
|
||||
where: {
|
||||
id: traceId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbTrace.length).toEqual(1);
|
||||
expect(dbTrace[0]?.name).toBeNull();
|
||||
expect(dbTrace[0]?.release).toBeNull();
|
||||
expect(dbTrace[0]?.metadata).toBeNull();
|
||||
expect(dbTrace[0]?.version).toBeNull();
|
||||
});
|
||||
|
||||
[
|
||||
{
|
||||
inputs: [{ a: "a" }, { b: "b" }],
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { modelMatch } from "@/scripts/model-match";
|
||||
import { pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { ModelUsageUnit } from "@/src/constants";
|
||||
import { prisma } from "@/src/server/db";
|
||||
|
||||
describe("model match", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
|
||||
it("should match historic observations to models", async () => {
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: "model-1",
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: "0.0000010",
|
||||
outputPrice: "0.0000020",
|
||||
totalPrice: "0.1",
|
||||
matchPattern: "(.*)(gpt-3.5-turbo)?(.*)",
|
||||
projectId: null,
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerConfig: {
|
||||
tokensPerMessage: 3,
|
||||
tokensPerName: 1,
|
||||
tokenizerModel: "gpt-3.5-turbo",
|
||||
},
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
});
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: "model-2",
|
||||
modelName: "claude-1.3",
|
||||
inputPrice: "0.0000010",
|
||||
outputPrice: "0.0000020",
|
||||
totalPrice: "0.1",
|
||||
matchPattern: "(.*)(claude-1.3)?(.*)",
|
||||
projectId: null,
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "claude",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.observation.createMany({
|
||||
data: [
|
||||
{
|
||||
id: "observation-1",
|
||||
type: "GENERATION",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
model: "gpt-3.5-turbo",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
promptTokens: 200,
|
||||
completionTokens: 3000,
|
||||
input: "I am a prompt",
|
||||
output: "I am a completion",
|
||||
},
|
||||
{
|
||||
type: "GENERATION",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
model: "claude-1.3",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
input: "I am a prompt",
|
||||
output: "I am a completion",
|
||||
},
|
||||
{
|
||||
type: "GENERATION",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
model: "claude-1.3",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
input: "I am a prompt",
|
||||
output: "I am a completion",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await modelMatch();
|
||||
|
||||
const observations = await prisma.observation.findMany();
|
||||
|
||||
// check that tokens from observation-1 are not changed
|
||||
const observation1 = observations.find((o) => o.id === "observation-1");
|
||||
expect(observation1?.promptTokens).toEqual(200);
|
||||
expect(observation1?.completionTokens).toEqual(3000);
|
||||
expect(observation1?.totalTokens).toEqual(0);
|
||||
|
||||
expect(observations.length).toEqual(3);
|
||||
observations.forEach((observation) => {
|
||||
expect(observation.internalModel).toBeDefined();
|
||||
expect(observation.promptTokens).toBeGreaterThan(0);
|
||||
expect(observation.completionTokens).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,13 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { type Observation } from "@prisma/client";
|
||||
|
||||
describe("/api/public/generations API Endpoint", () => {
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { ModelUsageUnit } from "@/src/constants";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { type ObservationView } from "@prisma/client";
|
||||
|
||||
describe("/api/public/observations API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
@@ -26,6 +28,19 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const model = await prisma.model.create({
|
||||
data: {
|
||||
id: "model-1",
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: "0.0000010",
|
||||
outputPrice: "0.0000020",
|
||||
totalPrice: "0.1",
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
projectId: null,
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.observation.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
@@ -33,15 +48,20 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
name: "generation-name",
|
||||
startTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
endTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
model: "model-name",
|
||||
model: "gpt-3.5-turbo",
|
||||
modelParameters: { key: "value" },
|
||||
input: { key: "input" },
|
||||
output: { key: "output" },
|
||||
promptTokens: 10,
|
||||
completionTokens: 20,
|
||||
totalTokens: 30,
|
||||
version: "2.0.0",
|
||||
type: "GENERATION",
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
internalModel: "gpt-3.5-turbo",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -54,13 +74,27 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isObservationList(fetchedObservations.body)) {
|
||||
throw new Error("Expected body to be an array of observations");
|
||||
throw new Error(
|
||||
"Expected body to be an array of observations" +
|
||||
JSON.stringify(fetchedObservations.body),
|
||||
);
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.data.length).toBe(1);
|
||||
expect(fetchedObservations.body.data[0]?.traceId).toBe(traceId);
|
||||
expect(fetchedObservations.body.data[0]?.input).toEqual({ key: "input" });
|
||||
expect(fetchedObservations.body.data[0]?.output).toEqual({ key: "output" });
|
||||
expect(fetchedObservations.body.data[0]?.model).toEqual("gpt-3.5-turbo");
|
||||
expect(fetchedObservations.body.data[0]?.modelId).toEqual(model.id);
|
||||
expect(
|
||||
fetchedObservations.body.data[0]?.calculatedInputCost,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
fetchedObservations.body.data[0]?.calculatedOutputCost,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
fetchedObservations.body.data[0]?.calculatedTotalCost,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
it("should fetch all observations, filtered by generations", async () => {
|
||||
await pruneDatabase();
|
||||
@@ -79,6 +113,19 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.model.create({
|
||||
data: {
|
||||
id: "model-1",
|
||||
modelName: "gpt-3.5-turbo",
|
||||
inputPrice: "0.0000010",
|
||||
outputPrice: "0.0000020",
|
||||
totalPrice: "0.1",
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
projectId: null,
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.observation.create({
|
||||
data: {
|
||||
id: uuidv4(),
|
||||
@@ -86,11 +133,16 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
name: "generation-name",
|
||||
startTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
endTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
model: "model-name",
|
||||
model: "gpt-3.5-turbo",
|
||||
internalModel: "gpt-3.5-turbo",
|
||||
modelParameters: { key: "value" },
|
||||
input: { key: "input" },
|
||||
output: { key: "output" },
|
||||
promptTokens: 10,
|
||||
completionTokens: 20,
|
||||
totalTokens: 30,
|
||||
version: "2.0.0",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
type: "GENERATION",
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
@@ -105,7 +157,6 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
name: "generation-name",
|
||||
startTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
endTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
model: "model-name",
|
||||
modelParameters: { key: "value" },
|
||||
input: { key: "input" },
|
||||
output: { key: "output" },
|
||||
@@ -123,6 +174,8 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
undefined,
|
||||
);
|
||||
|
||||
console.log(fetchedObservations.body);
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isObservationList(fetchedObservations.body)) {
|
||||
@@ -156,11 +209,18 @@ const isObservationList = (val: unknown): val is ObservationResponse => {
|
||||
"input" in element &&
|
||||
"output" in element &&
|
||||
"metadata" in element &&
|
||||
"version" in element,
|
||||
"version" in element &&
|
||||
"modelId" in element &&
|
||||
"inputPrice" in element &&
|
||||
"outputPrice" in element &&
|
||||
"totalPrice" in element &&
|
||||
"calculatedInputCost" in element &&
|
||||
"calculatedOutputCost" in element &&
|
||||
"calculatedTotalCost" in element,
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
type ObservationResponse = {
|
||||
data: Observation[];
|
||||
data: ObservationView[];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { v4 as uuidv4, v4 } from "uuid";
|
||||
import { type Prompt } from "@prisma/client";
|
||||
|
||||
describe("/api/public/prompts API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
|
||||
it("should fetch a prompt", async () => {
|
||||
const promptId = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name&version=1",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isPrompt(fetchedObservations.body)) {
|
||||
throw new Error("Expected body to be a prompt");
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.id).toBe(promptId);
|
||||
expect(fetchedObservations.body.name).toBe("prompt-name");
|
||||
expect(fetchedObservations.body.prompt).toBe("prompt");
|
||||
expect(fetchedObservations.body.version).toBe(1);
|
||||
expect(fetchedObservations.body.isActive).toBe(true);
|
||||
expect(fetchedObservations.body.createdBy).toBe("user-1");
|
||||
});
|
||||
|
||||
it("should fetch active prompt only if no prompt version is given", async () => {
|
||||
const promptId = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: false,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(404);
|
||||
});
|
||||
|
||||
it("should fetch inactive prompt if prompt version is given", async () => {
|
||||
const promptId = uuidv4();
|
||||
const promptTwoId = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt-one",
|
||||
isActive: false,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptTwoId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 2,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name&version=1",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isPrompt(fetchedObservations.body)) {
|
||||
throw new Error("Expected body to be a prompt");
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.id).toBe(promptId);
|
||||
expect(fetchedObservations.body.name).toBe("prompt-name");
|
||||
expect(fetchedObservations.body.prompt).toBe("prompt-one");
|
||||
expect(fetchedObservations.body.version).toBe(1);
|
||||
expect(fetchedObservations.body.isActive).toBe(false);
|
||||
expect(fetchedObservations.body.createdBy).toBe("user-1");
|
||||
});
|
||||
|
||||
it("should fetch active prompt when multiple exist", async () => {
|
||||
const promptIdOne = uuidv4();
|
||||
const promptIdTwo = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptIdOne,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: false,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptIdTwo,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 2,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isPrompt(fetchedObservations.body)) {
|
||||
throw new Error("Expected body to be a prompt");
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.id).toBe(promptIdTwo);
|
||||
expect(fetchedObservations.body.name).toBe("prompt-name");
|
||||
expect(fetchedObservations.body.prompt).toBe("prompt");
|
||||
expect(fetchedObservations.body.version).toBe(2);
|
||||
expect(fetchedObservations.body.isActive).toBe(true);
|
||||
expect(fetchedObservations.body.createdBy).toBe("user-1");
|
||||
});
|
||||
|
||||
it("should create and fetch a prompt", async () => {
|
||||
await makeAPICall("POST", "/api/public/prompts", {
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
});
|
||||
|
||||
const fetchedObservations = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/prompts?name=prompt-name&version=1",
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(fetchedObservations.status).toBe(200);
|
||||
|
||||
if (!isPrompt(fetchedObservations.body)) {
|
||||
throw new Error("Expected body to be an array of observations");
|
||||
}
|
||||
|
||||
expect(fetchedObservations.body.name).toBe("prompt-name");
|
||||
expect(fetchedObservations.body.prompt).toBe("prompt");
|
||||
expect(fetchedObservations.body.version).toBe(1);
|
||||
expect(fetchedObservations.body.isActive).toBe(true);
|
||||
expect(fetchedObservations.body.createdBy).toBe("API");
|
||||
});
|
||||
|
||||
it("should relate generation to prompt", async () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
|
||||
const promptId = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
metadata: {
|
||||
sdk_verion: "1.0.0",
|
||||
sdk_name: "python",
|
||||
},
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "generation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
promptName: "prompt-name",
|
||||
promptVersion: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration?.id).toBe(generationId);
|
||||
expect(dbGeneration?.promptId).toBe(promptId);
|
||||
});
|
||||
|
||||
it("should fail if prompt version is missing", async () => {
|
||||
const traceId = v4();
|
||||
const generationId = v4();
|
||||
|
||||
const promptId = uuidv4();
|
||||
|
||||
await prisma.prompt.create({
|
||||
data: {
|
||||
id: promptId,
|
||||
name: "prompt-name",
|
||||
prompt: "prompt",
|
||||
isActive: true,
|
||||
version: 1,
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
createdBy: "user-1",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeAPICall("POST", "/api/public/ingestion", {
|
||||
metadata: {
|
||||
sdk_verion: "1.0.0",
|
||||
sdk_name: "python",
|
||||
},
|
||||
batch: [
|
||||
{
|
||||
id: v4(),
|
||||
type: "trace-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
type: "generation-create",
|
||||
timestamp: new Date().toISOString(),
|
||||
body: {
|
||||
id: generationId,
|
||||
traceId: traceId,
|
||||
type: "GENERATION",
|
||||
name: "generation-name",
|
||||
promptName: "prompt-name",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(response.status).toBe(207);
|
||||
|
||||
console.log("response body", response.body);
|
||||
|
||||
const dbGeneration = await prisma.observation.findUnique({
|
||||
where: {
|
||||
id: generationId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbGeneration).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
const isPrompt = (x: unknown): x is Prompt => {
|
||||
if (typeof x !== "object" || x === null) return false;
|
||||
const prompt = x as Prompt;
|
||||
return (
|
||||
typeof prompt.id === "string" &&
|
||||
typeof prompt.name === "string" &&
|
||||
typeof prompt.version === "number" &&
|
||||
typeof prompt.prompt === "string" &&
|
||||
typeof prompt.isActive === "boolean" &&
|
||||
typeof prompt.projectId === "string" &&
|
||||
typeof prompt.createdBy === "string"
|
||||
);
|
||||
};
|
||||
@@ -22,7 +22,7 @@ describe("Build valid SQL queries", () => {
|
||||
table: "traces_observations",
|
||||
values: ["project-id", "project-id"],
|
||||
strings: [
|
||||
' FROM traces t LEFT JOIN observations o ON t.id = o.trace_id WHERE t."project_id" = ',
|
||||
' FROM traces t LEFT JOIN observations_view o ON t.id = o.trace_id WHERE t."project_id" = ',
|
||||
' AND o."project_id" = ',
|
||||
" ;",
|
||||
],
|
||||
@@ -30,7 +30,7 @@ describe("Build valid SQL queries", () => {
|
||||
{
|
||||
table: "observations",
|
||||
values: ["project-id"],
|
||||
strings: [' FROM observations o WHERE o."project_id" = ', " ;"],
|
||||
strings: [' FROM observations_view o WHERE o."project_id" = ', " ;"],
|
||||
} as const,
|
||||
{
|
||||
table: "traces_scores",
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/** @jest-environment node */
|
||||
import { pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { appRouter } from "@/src/server/api/root";
|
||||
import { createInnerTRPCContext } from "@/src/server/api/trpc";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import type { Session } from "next-auth";
|
||||
|
||||
describe("Traces TRPC Router", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
const session: Session = {
|
||||
expires: "1",
|
||||
user: {
|
||||
id: "clgb17vnp000008jjere5g15i",
|
||||
name: "John Doe",
|
||||
projects: [
|
||||
{
|
||||
id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
role: "ADMIN",
|
||||
name: "test",
|
||||
},
|
||||
],
|
||||
featureFlags: {
|
||||
templateFlag: true,
|
||||
},
|
||||
admin: true,
|
||||
},
|
||||
};
|
||||
|
||||
const ctx = createInnerTRPCContext({ session });
|
||||
const caller = appRouter.createCaller({ ...ctx, prisma });
|
||||
|
||||
test("traces.all RPC returns an array of traces", async () => {
|
||||
const trace = {
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
};
|
||||
await prisma.trace.create({
|
||||
data: { ...trace, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
});
|
||||
|
||||
const traces = await caller.traces.all({
|
||||
page: 0,
|
||||
limit: 10,
|
||||
// projectId from `seed.ts`
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: null,
|
||||
searchQuery: "",
|
||||
orderBy: null,
|
||||
});
|
||||
expect(traces).toBeDefined();
|
||||
expect(traces).toMatchObject({ traces: [trace] });
|
||||
});
|
||||
|
||||
test("traces.all RPC orders traces by userId", async () => {
|
||||
const traceTmpl = {
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
};
|
||||
const trace1 = traceTmpl;
|
||||
const trace2 = { ...traceTmpl, userId: "user-2" };
|
||||
await prisma.trace.create({
|
||||
data: { ...traceTmpl, projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
});
|
||||
await prisma.trace.create({
|
||||
data: {
|
||||
...trace2,
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
},
|
||||
});
|
||||
|
||||
const tracesASC = await caller.traces.all({
|
||||
page: 0,
|
||||
limit: 10,
|
||||
// projectId from `seed.ts`
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: null,
|
||||
searchQuery: "",
|
||||
orderBy: {
|
||||
column: "userId",
|
||||
order: "ASC",
|
||||
},
|
||||
});
|
||||
expect(tracesASC).toMatchObject({ traces: [trace1, trace2] });
|
||||
|
||||
const tracesDESC = await caller.traces.all({
|
||||
page: 0,
|
||||
limit: 10,
|
||||
// projectId from `seed.ts`
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
filter: null,
|
||||
searchQuery: "",
|
||||
orderBy: {
|
||||
column: "userId",
|
||||
order: "DESC",
|
||||
},
|
||||
});
|
||||
expect(tracesDESC).toMatchObject({ traces: [trace2, trace1] });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { makeAPICall, pruneDatabase } from "@/src/__tests__/test-utils";
|
||||
import { prisma } from "@/src/server/db";
|
||||
|
||||
describe("/api/public/traces API Endpoint", () => {
|
||||
beforeEach(async () => await pruneDatabase());
|
||||
afterEach(async () => await pruneDatabase());
|
||||
|
||||
it("should create a session via a trace", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
name: "trace-name",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
sessionId: "session-id",
|
||||
});
|
||||
|
||||
const dbSession = await prisma.traceSession.findFirst({
|
||||
where: {
|
||||
id: "session-id",
|
||||
},
|
||||
});
|
||||
|
||||
expect(dbSession).not.toBeNull();
|
||||
expect(dbSession).toMatchObject({
|
||||
id: "session-id",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
});
|
||||
});
|
||||
|
||||
it("should get session including traces", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
name: "trace-name",
|
||||
id: "trace-id",
|
||||
input: { hello: "world" },
|
||||
output: "hi",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
sessionId: "session-id",
|
||||
});
|
||||
|
||||
const response = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/sessions/session-id",
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body).toMatchObject({
|
||||
id: "session-id",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
traces: [
|
||||
{
|
||||
id: "trace-id",
|
||||
input: { hello: "world" },
|
||||
output: "hi",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -140,8 +140,8 @@ describe("/api/public/spans API Endpoint", () => {
|
||||
name: "spanName",
|
||||
},
|
||||
});
|
||||
expect(dbSpan?.id).toBe(spanId);
|
||||
expect(dbSpan?.traceId).toBe(traceId);
|
||||
expect(dbSpan.id).toBe(spanId);
|
||||
expect(dbSpan.traceId).toBe(traceId);
|
||||
});
|
||||
|
||||
it("should create trace when creating span without existing trace", async () => {
|
||||
@@ -248,14 +248,14 @@ describe("/api/public/spans API Endpoint", () => {
|
||||
|
||||
expect(createSpan.status).toBe(200);
|
||||
|
||||
expect(dbSpan?.id).toBe(spanId);
|
||||
expect(dbSpan?.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbSpan?.name).toBe(generationName);
|
||||
expect(dbSpan?.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan?.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan?.input).toEqual({ key: "value" });
|
||||
expect(dbSpan?.metadata).toEqual({ key: "value" });
|
||||
expect(dbSpan?.version).toBe("2.0.0");
|
||||
expect(dbSpan.id).toBe(spanId);
|
||||
expect(dbSpan.traceId).toBe(dbTrace[0]?.id);
|
||||
expect(dbSpan.name).toBe(generationName);
|
||||
expect(dbSpan.startTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan.endTime).toEqual(new Date("2021-01-01T00:00:00.000Z"));
|
||||
expect(dbSpan.input).toEqual({ key: "value" });
|
||||
expect(dbSpan.metadata).toEqual({ key: "value" });
|
||||
expect(dbSpan.version).toBe("2.0.0");
|
||||
});
|
||||
|
||||
it("should update span", async () => {
|
||||
|
||||
@@ -12,6 +12,9 @@ export const pruneDatabase = async () => {
|
||||
await prisma.datasetItem.deleteMany();
|
||||
await prisma.dataset.deleteMany();
|
||||
await prisma.datasetRuns.deleteMany();
|
||||
await prisma.prompt.deleteMany();
|
||||
await prisma.events.deleteMany();
|
||||
await prisma.model.deleteMany();
|
||||
};
|
||||
|
||||
export function createBasicAuthHeader(
|
||||
|
||||
@@ -40,7 +40,6 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
id: "trace-id",
|
||||
name: "trace-name",
|
||||
userId: "user-1",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
@@ -65,7 +64,6 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-id",
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "5.0.0",
|
||||
@@ -88,4 +86,54 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
userId: "user-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("should use tags correctly on POST and GET", async () => {
|
||||
await pruneDatabase();
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-1",
|
||||
tags: ["tag-1", "tag-2", "tag-3"],
|
||||
});
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-2",
|
||||
tags: ["tag-1"],
|
||||
});
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-3",
|
||||
tags: ["tag-2", "tag-3"],
|
||||
});
|
||||
|
||||
// multiple tags
|
||||
const traces = await makeAPICall(
|
||||
"GET",
|
||||
"/api/public/traces?tags=tag-2&tags=tag-3",
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const traceIds = traces.body.data.map((t: { id: string }) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds).toEqual(["trace-3", "trace-1"]);
|
||||
|
||||
// single tag
|
||||
const traces2 = await makeAPICall("GET", "/api/public/traces?tags=tag-1");
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const traceIds2 = traces2.body.data.map((t: { id: string }) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds2).toEqual(["trace-2", "trace-1"]);
|
||||
|
||||
// wrong tag
|
||||
const traces3 = await makeAPICall("GET", "/api/public/traces?tags=tag-10");
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const traceIds3 = traces3.body.data.map((t: { id: string }) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds3).toEqual([]);
|
||||
|
||||
// no tag
|
||||
const traces4 = await makeAPICall("GET", "/api/public/traces?tags=");
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const traceIds4 = traces4.body.data.map((t: { id: string }) => t.id);
|
||||
// check for equality ok as ordered by timestamp
|
||||
expect(traceIds4).toEqual(["trace-3", "trace-2", "trace-1"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,42 @@
|
||||
import { ModelUsageUnit } from "@/src/constants";
|
||||
import { tokenCount } from "@/src/features/ingest/lib/usage";
|
||||
|
||||
describe("Token Count Functions", () => {
|
||||
const generateModel = (model: string, tokenizer: string) => {
|
||||
return {
|
||||
id: "1",
|
||||
modelName: model,
|
||||
tokenizerId: tokenizer,
|
||||
tokenizerConfig: {
|
||||
tokensPerMessage: 3,
|
||||
tokensPerName: 1,
|
||||
tokenizerModel: model,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
matchPattern: "",
|
||||
projectId: null,
|
||||
startDate: null,
|
||||
inputPrice: null,
|
||||
outputPrice: null,
|
||||
totalPrice: null,
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
};
|
||||
};
|
||||
|
||||
describe("token count for strings", () => {
|
||||
[
|
||||
{ model: "gpt-3.5", tokens: 114 },
|
||||
{ model: "gpt-4-1106-preview", tokens: 114 },
|
||||
{ model: "gpt-4-vision-preview", tokens: 114 },
|
||||
{ model: "claude", tokens: 118 },
|
||||
{ model: "claude-instant-1.2", tokens: 118 },
|
||||
].forEach(({ model, tokens }) => {
|
||||
{ model: "gpt-3.5-turbo", tokenizer: "openai", tokens: 114 },
|
||||
{ model: "text-embedding-ada-002", tokenizer: "openai", tokens: 114 },
|
||||
{ model: "gpt-4-1106-preview", tokenizer: "openai", tokens: 114 },
|
||||
{ model: "gpt-4-vision-preview", tokenizer: "openai", tokens: 114 },
|
||||
{ model: "claude", tokenizer: "claude", tokens: 118 },
|
||||
{ model: "claude-instant-1.2", tokenizer: "claude", tokens: 118 },
|
||||
{ model: "gpt-3.5-turbo-1106", tokenizer: "openai", tokens: 114 },
|
||||
].forEach(({ model, tokens, tokenizer }) => {
|
||||
it(`should return token count ${tokens} for ${model}`, () => {
|
||||
const result = tokenCount({
|
||||
model: model,
|
||||
model: generateModel(model, tokenizer),
|
||||
text: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
|
||||
});
|
||||
expect(result).toBeDefined();
|
||||
@@ -21,20 +46,67 @@ describe("Token Count Functions", () => {
|
||||
|
||||
it("should return undefined for unknown model", () => {
|
||||
const result = tokenCount({
|
||||
model: "unknown-model",
|
||||
model: generateModel("unknown-model", "unknown-tokenizer"),
|
||||
text: "Hello, World!",
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("check extensive openai chat message", () => {
|
||||
const result = tokenCount({
|
||||
model: generateModel("gpt-3.5-turbo", "openai"),
|
||||
text: [
|
||||
{
|
||||
role: "system",
|
||||
content: "some test",
|
||||
id: "some-id",
|
||||
isPersisted: true,
|
||||
},
|
||||
{
|
||||
id: "some-id",
|
||||
content: "some test",
|
||||
role: "user",
|
||||
timestamp: "2024-01-00:00:00.488Z",
|
||||
isPersisted: true,
|
||||
},
|
||||
{
|
||||
id: "some id",
|
||||
content:
|
||||
"Hey Simon! 😊 How's your day going? Have you been up to anything interesting lately?",
|
||||
role: "user",
|
||||
timestamp: "2024-01-24T10:00:00.929Z",
|
||||
isPersisted: true,
|
||||
},
|
||||
{
|
||||
content: true,
|
||||
role: "user",
|
||||
id: "some id",
|
||||
},
|
||||
{
|
||||
role: "system",
|
||||
content: "This is some content",
|
||||
},
|
||||
{
|
||||
id: "another id",
|
||||
role: "assistant",
|
||||
content: "This is some content",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(result).toBe(155);
|
||||
});
|
||||
|
||||
it("should return for invalid text type", () => {
|
||||
const result = tokenCount({ model: "gpt-4", text: 1234 });
|
||||
const result = tokenCount({
|
||||
model: generateModel("gpt-4", "openai"),
|
||||
text: 1234,
|
||||
});
|
||||
expect(result).toBe(2);
|
||||
});
|
||||
|
||||
it("should return correct token count for empty string", () => {
|
||||
const result = tokenCount({
|
||||
model: "gpt-4",
|
||||
model: generateModel("gpt-4", "openai"),
|
||||
text: "",
|
||||
});
|
||||
expect(result).toBe(0);
|
||||
@@ -43,7 +115,7 @@ describe("Token Count Functions", () => {
|
||||
it("should return correct token count for very long string", () => {
|
||||
const longString = "A".repeat(10000);
|
||||
const result = tokenCount({
|
||||
model: "gpt-4",
|
||||
model: generateModel("gpt-4", "openai"),
|
||||
text: longString,
|
||||
});
|
||||
expect(result).toBeDefined();
|
||||
@@ -52,14 +124,14 @@ describe("Token Count Functions", () => {
|
||||
|
||||
it("should return undefined for null text input", () => {
|
||||
const result = tokenCount({
|
||||
model: "gpt-4",
|
||||
model: generateModel("gpt-4", "openai"),
|
||||
text: null,
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
it("should return undefined for undefined text input", () => {
|
||||
const result = tokenCount({
|
||||
model: "gpt-4",
|
||||
model: generateModel("gpt-4", "openai"),
|
||||
text: undefined,
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
@@ -68,13 +140,14 @@ describe("Token Count Functions", () => {
|
||||
|
||||
describe("token count for chat messages", () => {
|
||||
[
|
||||
{ model: "gpt-4", tokens: 44 },
|
||||
{ model: "gpt-3.5-turbo-16k-0613", tokens: 44 },
|
||||
{ model: "claude-instant-1.2", tokens: 48 },
|
||||
].forEach(({ model, tokens }) => {
|
||||
{ model: "gpt-4", tokenizer: "openai", tokens: 44 },
|
||||
{ model: "gpt-3.5-turbo-16k-0613", tokenizer: "openai", tokens: 44 },
|
||||
{ model: "gpt-3.5-turbo-16k-0613", tokenizer: "openai", tokens: 44 },
|
||||
{ model: "claude-instant-1.2", tokenizer: "claude", tokens: 48 },
|
||||
].forEach(({ model, tokens, tokenizer }) => {
|
||||
it(`should return token count ${tokens} for ${model}`, () => {
|
||||
const result = tokenCount({
|
||||
model: model,
|
||||
model: generateModel(model, tokenizer),
|
||||
text: [
|
||||
{ role: "system", content: "You are a helpful assistant." },
|
||||
{ role: "user", content: "Who won the world series in 2020?" },
|
||||
@@ -91,7 +164,7 @@ describe("Token Count Functions", () => {
|
||||
|
||||
it("should return for non array", () => {
|
||||
const result = tokenCount({
|
||||
model: "gpt-4",
|
||||
model: generateModel("gpt-4", "openai"),
|
||||
text: { role: "Helo world" },
|
||||
});
|
||||
expect(result).toBe(7);
|
||||
@@ -99,7 +172,7 @@ describe("Token Count Functions", () => {
|
||||
|
||||
it("should return for empty array", () => {
|
||||
const result = tokenCount({
|
||||
model: "gpt-4",
|
||||
model: generateModel("gpt-4", "openai"),
|
||||
text: [],
|
||||
});
|
||||
expect(result).toBeUndefined();
|
||||
@@ -107,7 +180,7 @@ describe("Token Count Functions", () => {
|
||||
|
||||
it("should return for array of invalid object", () => {
|
||||
const result = tokenCount({
|
||||
model: "gpt-4",
|
||||
model: generateModel("gpt-4", "openai"),
|
||||
text: [{ role: "Helo world" }],
|
||||
});
|
||||
expect(result).toBe(9);
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
|
||||
import { VERSION } from "@/src/constants";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
export const LangfuseIcon = ({
|
||||
@@ -20,19 +24,55 @@ export const LangfuseIcon = ({
|
||||
export const LangfuseLogo = ({
|
||||
className,
|
||||
size = "sm",
|
||||
version = false,
|
||||
showEnvLabel = false,
|
||||
}: {
|
||||
size?: "sm" | "xl";
|
||||
className?: string;
|
||||
version?: boolean;
|
||||
showEnvLabel?: boolean;
|
||||
}) => (
|
||||
<div className={cn("flex items-center", className)}>
|
||||
<LangfuseIcon size={size === "sm" ? 16 : 20} />
|
||||
<span
|
||||
className={cn(
|
||||
"font-mono font-semibold",
|
||||
size === "sm" ? "ml-2 text-sm" : "ml-3 text-xl",
|
||||
<div className={cn("flex gap-4 xl:flex-col xl:items-start", className)}>
|
||||
{/* Environment Labeling for Langfuse Maintainers */}
|
||||
{showEnvLabel && env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-2 self-stretch rounded-md px-3 py-1 ring-1 xl:-mx-2",
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING"
|
||||
? "bg-blue-100 text-blue-500 ring-blue-500"
|
||||
: "bg-red-100 text-red-500 ring-red-500",
|
||||
)}
|
||||
>
|
||||
<AlertTriangle size={16} />
|
||||
<span className="whitespace-nowrap">
|
||||
{["EU", "US"].includes(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION)
|
||||
? `PRODUCTION-${env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}`
|
||||
: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Langfuse Logo */}
|
||||
<div className="flex items-center">
|
||||
<LangfuseIcon size={size === "sm" ? 16 : 20} />
|
||||
<span
|
||||
className={cn(
|
||||
"font-mono font-semibold",
|
||||
size === "sm" ? "ml-2 text-sm" : "ml-3 text-xl",
|
||||
)}
|
||||
>
|
||||
Langfuse
|
||||
</span>
|
||||
{version && (
|
||||
<a
|
||||
href="https://github.com/langfuse/langfuse/releases"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
title="View releases on GitHub"
|
||||
className="ml-2 text-xs text-gray-400"
|
||||
>
|
||||
{VERSION}
|
||||
</a>
|
||||
)}
|
||||
>
|
||||
Langfuse
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { StarIcon } from "lucide-react";
|
||||
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function BookmarkTrace({
|
||||
traceId,
|
||||
projectId,
|
||||
isBookmarked,
|
||||
size = "sm",
|
||||
}: {
|
||||
traceId: string;
|
||||
projectId: string;
|
||||
isBookmarked: boolean;
|
||||
size?: "sm" | "xs";
|
||||
}) {
|
||||
const utils = api.useUtils();
|
||||
const [cacheIsBookmarked, setCacheIsBookmarked] = useState<boolean | null>(
|
||||
null,
|
||||
);
|
||||
const localIsBookmarked = cacheIsBookmarked ?? isBookmarked;
|
||||
const hasAccess = useHasAccess({ projectId, scope: "traces:bookmark" });
|
||||
const mutBookmarkTrace = api.traces.bookmark.useMutation({
|
||||
onSuccess: () => {
|
||||
setCacheIsBookmarked(!localIsBookmarked);
|
||||
void utils.traces.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setCacheIsBookmarked(null);
|
||||
}, [isBookmarked]);
|
||||
|
||||
const handleBookmarkClick = () => {
|
||||
if (!hasAccess) return;
|
||||
void mutBookmarkTrace.mutateAsync({
|
||||
traceId,
|
||||
projectId,
|
||||
bookmarked: !localIsBookmarked,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size={size}
|
||||
onClick={handleBookmarkClick}
|
||||
disabled={!hasAccess}
|
||||
loading={mutBookmarkTrace.isLoading}
|
||||
>
|
||||
<StarIcon
|
||||
className={`h-4 w-4 ${
|
||||
localIsBookmarked ? "fill-current text-yellow-500" : "text-gray-500"
|
||||
}`}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Calendar as CalendarIcon } from "lucide-react";
|
||||
import { Calendar as CalendarIcon, X } from "lucide-react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { Calendar } from "@/src/components/ui/calendar";
|
||||
import {
|
||||
@@ -39,36 +39,50 @@ export type AvailableDateRangeSelections =
|
||||
export function DatePicker({
|
||||
date,
|
||||
onChange,
|
||||
clearable = false,
|
||||
className,
|
||||
}: {
|
||||
date?: Date | undefined;
|
||||
onChange: (date: Date | undefined) => void;
|
||||
clearable?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<div className="flex flex-row gap-2 align-middle">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"justify-start text-left font-normal",
|
||||
!date && "text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{date ? format(date, "PPP") : <span>Pick a date</span>}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={(d) => onChange(d)}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{date && clearable && (
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"justify-start text-left font-normal",
|
||||
!date && "text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onChange(undefined)}
|
||||
title="reset date"
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{date ? format(date, "PPP") : <span>Pick a date</span>}
|
||||
<X size={14} />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={(d) => onChange(d)}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,7 +205,7 @@ export function DatePickerWithRange({
|
||||
{DEFAULT_DATE_RANGE_SELECTION}
|
||||
</SelectItem>
|
||||
{dateTimeAggregationOptions.toReversed().map((item) => (
|
||||
<SelectItem key={item} value={`${item}`}>
|
||||
<SelectItem key={item} value={item}>
|
||||
{item}
|
||||
</SelectItem>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { api } from "@/src/utils/api";
|
||||
import {
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/src/components/ui/popover";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { useState } from "react";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
export function DeleteTrace({
|
||||
traceId,
|
||||
@@ -23,10 +23,11 @@ export function DeleteTrace({
|
||||
const [isDeleted, setIsDeleted] = useState(false);
|
||||
const router = useRouter();
|
||||
const utils = api.useUtils();
|
||||
const posthog = usePostHog();
|
||||
|
||||
const hasAccess = useHasAccess({ projectId, scope: "traces:delete" });
|
||||
|
||||
const mutDeleteTrace = api.traces.delete.useMutation({
|
||||
const mutDeleteTraces = api.traces.deleteMany.useMutation({
|
||||
onSuccess: () => {
|
||||
setIsDeleted(true);
|
||||
if (!isTableAction) {
|
||||
@@ -51,7 +52,7 @@ export function DeleteTrace({
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="outline" type="button">
|
||||
<Button variant="outline" type="button" size="icon">
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
@@ -66,10 +67,16 @@ export function DeleteTrace({
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
loading={mutDeleteTrace.isLoading || isDeleted}
|
||||
onClick={() =>
|
||||
void mutDeleteTrace.mutateAsync({ traceId, projectId })
|
||||
}
|
||||
loading={mutDeleteTraces.isLoading || isDeleted}
|
||||
onClick={() => {
|
||||
void mutDeleteTraces.mutateAsync({
|
||||
traceIds: [traceId],
|
||||
projectId,
|
||||
});
|
||||
posthog.capture("trace:delete", {
|
||||
source: isTableAction ? "table-single-row" : "trace",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete trace
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { Badge } from "@/src/components/ui/badge";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/src/components/ui/hover-card";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
import { MessageCircle } from "lucide-react";
|
||||
|
||||
type ScoreSimplified = {
|
||||
name: string;
|
||||
value: number;
|
||||
comment?: string | null;
|
||||
};
|
||||
|
||||
export const GroupedScoreBadges = ({
|
||||
@@ -12,7 +20,7 @@ export const GroupedScoreBadges = ({
|
||||
scores: ScoreSimplified[];
|
||||
variant?: "badge" | "headings";
|
||||
}) => {
|
||||
const groupedScores = scores.reduce(
|
||||
const groupedScores = scores.reduce<Record<string, ScoreSimplified[]>>(
|
||||
(acc, score) => {
|
||||
if (!acc[score.name] || !Array.isArray(acc[score.name])) {
|
||||
acc[score.name] = [score];
|
||||
@@ -21,7 +29,31 @@ export const GroupedScoreBadges = ({
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, ScoreSimplified[]>,
|
||||
{},
|
||||
);
|
||||
|
||||
const ScoresOfGroup = (props: {
|
||||
scores: ScoreSimplified[];
|
||||
className?: string;
|
||||
}) => (
|
||||
<div className={cn("text-xs", props.className)}>
|
||||
{props.scores.map((s, i) => (
|
||||
<span key={i} className="group/score ml-1 first:ml-0">
|
||||
{s.value.toFixed(2)}
|
||||
{s.comment && (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger className="ml-1 inline-block cursor-pointer">
|
||||
<MessageCircle size={12} />
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<p>{s.comment}</p>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)}
|
||||
<span className="group-last/score:hidden">,</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (variant === "headings")
|
||||
@@ -32,9 +64,7 @@ export const GroupedScoreBadges = ({
|
||||
.map(([name, scores]) => (
|
||||
<div key={name}>
|
||||
<div className="text-xs text-gray-500">{name}</div>
|
||||
<div className="text-xs">
|
||||
{scores.map((s) => s.value.toFixed(2)).join(", ")}
|
||||
</div>
|
||||
<ScoresOfGroup scores={scores} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -50,7 +80,7 @@ export const GroupedScoreBadges = ({
|
||||
key={name}
|
||||
className="break-all font-normal"
|
||||
>
|
||||
{name}: {scores.map((s) => s.value.toFixed(2)).join(", ")}
|
||||
{name}: <ScoresOfGroup scores={scores} className="ml-2" />
|
||||
</Badge>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -3,34 +3,65 @@ import {
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/src/components/ui/hover-card";
|
||||
import { HelpCircle } from "lucide-react";
|
||||
import { HelpCircle, Info } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
|
||||
export type DocPopupProps = {
|
||||
description: React.ReactNode;
|
||||
href: string;
|
||||
size?: "sm" | "md" | "lg";
|
||||
href?: string;
|
||||
style?: "question" | "info";
|
||||
size?: "xs" | "sm" | "md" | "lg";
|
||||
};
|
||||
|
||||
export default function DocPopup({
|
||||
description,
|
||||
href,
|
||||
style = "info",
|
||||
size = "sm",
|
||||
}: DocPopupProps) {
|
||||
const sizes = {
|
||||
xs: "w-3 h-3",
|
||||
sm: "w-4 h-4",
|
||||
md: "w-6 h-6",
|
||||
lg: "w-8 h-8",
|
||||
};
|
||||
const posthog = usePostHog();
|
||||
|
||||
return (
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger className="mx-2 cursor-pointer" asChild>
|
||||
<Link href={href} rel="noopener" target="_blank">
|
||||
<div className="whitespace-nowrap text-gray-500 sm:pl-0">
|
||||
<HelpCircle className={sizes[size]} />
|
||||
<HoverCard
|
||||
openDelay={200}
|
||||
onOpenChange={(open) => {
|
||||
if (open) {
|
||||
posthog.capture("help-popup:opened", { href, description });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<HoverCardTrigger className="mx-1 cursor-pointer" asChild>
|
||||
{href ? (
|
||||
<Link
|
||||
href={href}
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
className="inline-block whitespace-nowrap text-gray-500 sm:pl-0"
|
||||
>
|
||||
{
|
||||
{
|
||||
question: <HelpCircle className={sizes[size]} />,
|
||||
info: <Info className={sizes[size]} />,
|
||||
}[style]
|
||||
}
|
||||
</Link>
|
||||
) : (
|
||||
<div className="inline-block whitespace-nowrap text-gray-500 sm:pl-0">
|
||||
{
|
||||
{
|
||||
question: <HelpCircle className={sizes[size]} />,
|
||||
info: <Info className={sizes[size]} />,
|
||||
}[style]
|
||||
}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
{typeof description === "string" ? (
|
||||
|
||||
@@ -2,12 +2,20 @@ import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useSession } from "next-auth/react";
|
||||
import DocPopup from "@/src/components/layouts/doc-popup";
|
||||
import { type Status, StatusBadge } from "./status-badge";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
export default function Header(props: {
|
||||
export default function Header({
|
||||
level = "h2",
|
||||
...props
|
||||
}: {
|
||||
title: string;
|
||||
breadcrumb?: { name: string; href?: string }[];
|
||||
live?: boolean;
|
||||
status?: Status;
|
||||
help?: { description: string; href?: string };
|
||||
actionButtons?: React.ReactNode;
|
||||
level?: "h2" | "h3";
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const session = useSession();
|
||||
@@ -32,7 +40,7 @@ export default function Header(props: {
|
||||
[...props.breadcrumb.map((i) => i.href).filter(Boolean)].pop();
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className={cn(level === "h2" ? "mb-8" : "mb-1")}>
|
||||
<div>
|
||||
{backHref ? (
|
||||
<nav className="sm:hidden" aria-label="Back">
|
||||
@@ -48,7 +56,7 @@ export default function Header(props: {
|
||||
</Link>
|
||||
</nav>
|
||||
) : null}
|
||||
{breadcrumb ? (
|
||||
{(level === "h2" || props.breadcrumb) && breadcrumb.length ? (
|
||||
<nav className="hidden sm:flex" aria-label="Breadcrumb">
|
||||
<ol role="list" className="flex items-center space-x-4">
|
||||
{breadcrumb.map(({ name, href }, index) => (
|
||||
@@ -79,25 +87,31 @@ export default function Header(props: {
|
||||
</nav>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap items-center justify-between gap-5">
|
||||
<div className="mt-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-3 md:gap-5">
|
||||
<div className="min-w-0">
|
||||
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||
{props.title}
|
||||
</h2>
|
||||
<div className="flex min-w-0 flex-row">
|
||||
{level === "h2" ? (
|
||||
<h2 className="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
|
||||
{props.title}
|
||||
</h2>
|
||||
) : (
|
||||
<h3 className="text-lg font-bold leading-7 text-gray-900 sm:truncate sm:text-xl sm:tracking-tight">
|
||||
{props.title}
|
||||
</h3>
|
||||
)}
|
||||
{props.help ? (
|
||||
<DocPopup
|
||||
description={props.help.description}
|
||||
href={props.help.href}
|
||||
size="sm"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{props.live ? (
|
||||
<div className="flex items-center gap-2 rounded-sm bg-green-100 px-3 text-green-600">
|
||||
<span className="relative flex h-2 w-2 ">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-600"></span>
|
||||
</span>
|
||||
Live
|
||||
</div>
|
||||
) : null}
|
||||
{props.status && <StatusBadge type={props.status} />}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{props.actionButtons ?? null}
|
||||
</div>
|
||||
<div className="md:flex-1" />
|
||||
{props.actionButtons ?? null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+301
-168
@@ -1,12 +1,12 @@
|
||||
import { ROUTES } from "@/src/components/layouts/routes";
|
||||
import { ROUTES, type Route } from "@/src/components/layouts/routes";
|
||||
import { Fragment, type PropsWithChildren, useState } from "react";
|
||||
import { Dialog, Menu, Transition } from "@headlessui/react";
|
||||
import { Dialog, Disclosure, Menu, Transition } from "@headlessui/react";
|
||||
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import clsx from "clsx";
|
||||
import { Code, MessageSquarePlus, Info } from "lucide-react";
|
||||
import { Code, MessageSquarePlus, Info, ChevronRightIcon } from "lucide-react";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
@@ -23,6 +23,7 @@ import Head from "next/head";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { LangfuseLogo } from "@/src/components/LangfuseLogo";
|
||||
import { Spinner } from "@/src/components/layouts/spinner";
|
||||
import { hasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
|
||||
const userNavigation = [
|
||||
{
|
||||
@@ -34,8 +35,12 @@ const userNavigation = [
|
||||
},
|
||||
];
|
||||
|
||||
const pathsWithoutNavigation: string[] = [];
|
||||
const unauthenticatedPaths = ["/auth/sign-in", "/auth/sign-up"];
|
||||
const pathsWithoutNavigation: string[] = ["/onboarding"];
|
||||
const unauthenticatedPaths: string[] = ["/auth/sign-in", "/auth/sign-up"];
|
||||
const publishablePaths: string[] = [
|
||||
"/project/[projectId]/sessions/[sessionId]",
|
||||
"/project/[projectId]/traces/[traceId]",
|
||||
];
|
||||
|
||||
export default function Layout(props: PropsWithChildren) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
@@ -45,25 +50,47 @@ export default function Layout(props: PropsWithChildren) {
|
||||
api.environment.enableExperimentalFeatures.useQuery().data ?? false;
|
||||
|
||||
const projectId = router.query.projectId as string | undefined;
|
||||
const navigation = ROUTES.filter(
|
||||
({ pathname }) => projectId || !pathname.includes("[projectId]"),
|
||||
)
|
||||
.filter(
|
||||
({ featureFlag }) =>
|
||||
featureFlag === undefined ||
|
||||
|
||||
const mapNavigation = (route: Route): NavigationItem | null => {
|
||||
// Project-level routes
|
||||
if (!projectId && route.pathname?.includes("[projectId]")) return null;
|
||||
|
||||
// Feature Flags
|
||||
if (
|
||||
!(
|
||||
route.featureFlag === undefined ||
|
||||
enableExperimentalFeatures ||
|
||||
session.data?.user?.featureFlags[featureFlag],
|
||||
session.data?.user?.featureFlags[route.featureFlag]
|
||||
)
|
||||
)
|
||||
.map(({ pathname, ...rest }) => ({
|
||||
pathname,
|
||||
href: pathname.replace("[projectId]", projectId ?? ""),
|
||||
...rest,
|
||||
}))
|
||||
.map(({ pathname, ...rest }) => ({
|
||||
pathname,
|
||||
current: router.pathname === pathname,
|
||||
...rest,
|
||||
}));
|
||||
return null;
|
||||
|
||||
// RBAC
|
||||
if (
|
||||
route.rbacScope !== undefined &&
|
||||
(!projectId || !hasAccess({ projectId, scope: route.rbacScope, session }))
|
||||
)
|
||||
return null;
|
||||
|
||||
// apply to children as well
|
||||
const children: (NavigationItem | null)[] =
|
||||
route.children?.map((child) => mapNavigation(child)).filter(Boolean) ??
|
||||
[];
|
||||
return {
|
||||
...route,
|
||||
href: route.pathname?.replace("[projectId]", projectId ?? ""),
|
||||
current: router.pathname === route.pathname,
|
||||
children:
|
||||
children.length > 0
|
||||
? (children as NavigationItem[]) // does not include null due to filter
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const navigationMapped: (NavigationItem | null)[] = ROUTES.map((route) =>
|
||||
mapNavigation(route),
|
||||
).filter(Boolean);
|
||||
const navigation = navigationMapped.filter(Boolean) as NavigationItem[]; // does not include null due to filter
|
||||
|
||||
const currentPathName = navigation.find(({ current }) => current)?.name;
|
||||
|
||||
@@ -76,6 +103,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
session.data &&
|
||||
session.data.user === null &&
|
||||
!unauthenticatedPaths.includes(router.pathname) &&
|
||||
!publishablePaths.includes(router.pathname) &&
|
||||
!router.pathname.startsWith("/public/")
|
||||
) {
|
||||
void signOut({
|
||||
@@ -87,9 +115,10 @@ export default function Layout(props: PropsWithChildren) {
|
||||
if (
|
||||
session.status === "unauthenticated" &&
|
||||
!unauthenticatedPaths.includes(router.pathname) &&
|
||||
!publishablePaths.includes(router.pathname) &&
|
||||
!router.pathname.startsWith("/public/")
|
||||
) {
|
||||
void router.push("/auth/sign-in");
|
||||
void router.replace("/auth/sign-in");
|
||||
return <Spinner message="Redirecting" />;
|
||||
}
|
||||
|
||||
@@ -97,7 +126,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
session.status === "authenticated" &&
|
||||
unauthenticatedPaths.includes(router.pathname)
|
||||
) {
|
||||
void router.push("/");
|
||||
void router.replace("/");
|
||||
return <Spinner message="Redirecting" />;
|
||||
}
|
||||
|
||||
@@ -112,7 +141,6 @@ export default function Layout(props: PropsWithChildren) {
|
||||
{props.children}
|
||||
</main>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -192,46 +220,16 @@ export default function Layout(props: PropsWithChildren) {
|
||||
</Transition.Child>
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 py-4">
|
||||
<LangfuseLogo size="xl" />
|
||||
<LangfuseLogo
|
||||
version
|
||||
size="xl"
|
||||
showEnvLabel={session.data?.user?.email?.endsWith(
|
||||
"@langfuse.com",
|
||||
)}
|
||||
/>
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={clsx(
|
||||
item.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={clsx(
|
||||
item.current
|
||||
? "text-indigo-600"
|
||||
: "text-gray-400 group-hover:text-indigo-600",
|
||||
"h-6 w-6 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
<FeedbackButtonWrapper className="w-full">
|
||||
<li className="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
|
||||
<MessageSquarePlus
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Feedback
|
||||
</li>
|
||||
</FeedbackButtonWrapper>
|
||||
</ul>
|
||||
</li>
|
||||
<MainNavigation nav={navigation} />
|
||||
<li>
|
||||
<div className="flex flex-row place-content-between items-center">
|
||||
<div className="text-xs font-semibold leading-6 text-gray-400">
|
||||
@@ -267,7 +265,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
{project.role === "VIEWER" ? (
|
||||
<span
|
||||
className={cn(
|
||||
"whitespace-nowrap break-keep rounded-sm border p-1 text-xs",
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
projectId === project.id
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
@@ -294,47 +292,17 @@ export default function Layout(props: PropsWithChildren) {
|
||||
<div className="hidden xl:fixed xl:inset-y-0 xl:z-50 xl:flex xl:w-72 xl:flex-col">
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex h-screen grow flex-col gap-y-5 border-r border-gray-200 bg-white pt-7">
|
||||
<LangfuseLogo size="xl" className="mb-2 px-6" />
|
||||
<LangfuseLogo
|
||||
version
|
||||
size="xl"
|
||||
className="mb-2 px-6"
|
||||
showEnvLabel={session.data?.user?.email?.endsWith(
|
||||
"@langfuse.com",
|
||||
)}
|
||||
/>
|
||||
<nav className="flex h-full flex-1 flex-col overflow-y-auto px-6 pb-3">
|
||||
<ul role="list" className="flex h-full flex-col gap-y-4">
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={clsx(
|
||||
item.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={clsx(
|
||||
item.current
|
||||
? "text-indigo-600"
|
||||
: "text-gray-400 group-hover:text-indigo-600",
|
||||
"h-6 w-6 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
<FeedbackButtonWrapper className="w-full">
|
||||
<li className="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
|
||||
<MessageSquarePlus
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Feedback
|
||||
</li>
|
||||
</FeedbackButtonWrapper>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<MainNavigation nav={navigation} />
|
||||
<li className="mt-auto">
|
||||
<div className="flex flex-row place-content-between items-center">
|
||||
<div className="text-xs font-semibold leading-6 text-gray-400">
|
||||
@@ -373,7 +341,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
{project.role === "VIEWER" ? (
|
||||
<span
|
||||
className={cn(
|
||||
"whitespace-nowrap break-keep rounded-sm border p-1 text-xs",
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
projectId === project.id
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
@@ -387,64 +355,60 @@ export default function Layout(props: PropsWithChildren) {
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li className="-mx-6">
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button className="flex w-full items-center gap-x-4 p-1.5 px-6 py-3 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage
|
||||
src={session.data?.user?.image ?? undefined}
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{session.data?.user?.name
|
||||
? session.data.user.name
|
||||
.split(" ")
|
||||
.map((word) => word[0])
|
||||
.slice(0, 2)
|
||||
.concat("")
|
||||
: null}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="flex-shrink truncate text-sm font-semibold leading-6 text-gray-900">
|
||||
{session.data?.user?.name}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute -top-full right-0 z-10 mt-2.5 w-32 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||
{userNavigation.map((item) => (
|
||||
<Menu.Item key={item.name}>
|
||||
{({ active }) => (
|
||||
<a
|
||||
onClick={() => void item.onClick()}
|
||||
className={cn(
|
||||
active ? "bg-gray-50" : "",
|
||||
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<Menu as="div" className="relative left-1">
|
||||
<Menu.Button className="flex w-full items-center gap-x-4 p-1.5 px-6 py-3 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={session.data?.user?.image ?? undefined} />
|
||||
<AvatarFallback>
|
||||
{session.data?.user?.name
|
||||
? session.data.user.name
|
||||
.split(" ")
|
||||
.map((word) => word[0])
|
||||
.slice(0, 2)
|
||||
.concat("")
|
||||
: null}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="flex-shrink truncate text-sm font-semibold leading-6 text-gray-900">
|
||||
{session.data?.user?.name}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute -top-full right-0 z-10 mt-2.5 w-32 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||
{userNavigation.map((item) => (
|
||||
<Menu.Item key={item.name}>
|
||||
{({ active }) => (
|
||||
<a
|
||||
onClick={() => void item.onClick()}
|
||||
className={cn(
|
||||
active ? "bg-gray-50" : "",
|
||||
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -457,7 +421,11 @@ export default function Layout(props: PropsWithChildren) {
|
||||
<span className="sr-only">Open sidebar</span>
|
||||
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
<LangfuseLogo className="flex-1" />
|
||||
<LangfuseLogo
|
||||
version
|
||||
className="flex-1"
|
||||
showEnvLabel={session.data?.user?.email?.endsWith("@langfuse.com")}
|
||||
/>
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button className="flex items-center gap-x-4 text-sm font-semibold leading-6 text-gray-900">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
@@ -506,8 +474,8 @@ export default function Layout(props: PropsWithChildren) {
|
||||
<div className="xl:pl-72">
|
||||
{env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
|
||||
projectId === env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION &&
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== "STAGING" &&
|
||||
(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING" ||
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU") &&
|
||||
!session.data?.user?.email?.endsWith("@langfuse.com") ? (
|
||||
<div className="flex w-full items-center border-b border-yellow-500 bg-yellow-100 px-4 py-2 xl:sticky xl:top-0 xl:z-40">
|
||||
<div className="flex flex-1 flex-wrap gap-1">
|
||||
@@ -520,19 +488,184 @@ export default function Layout(props: PropsWithChildren) {
|
||||
|
||||
<Button size="sm" asChild className="ml-2">
|
||||
<Link
|
||||
href="https://langfuse.com/docs/qa-chatbot"
|
||||
href={
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
|
||||
? "https://langfuse.com/docs/qa-chatbot"
|
||||
: "https://docs-staging.langfuse.com/docs/qa-chatbot"
|
||||
}
|
||||
target="_blank"
|
||||
>
|
||||
Q&A Chatbot ↗
|
||||
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
|
||||
? "Q&A Chatbot ↗"
|
||||
: "Q&A Chatbot (staging) ↗"}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<main className="py-4">
|
||||
<div className="px-4">{props.children}</div>
|
||||
</main>
|
||||
<main className="p-4">{props.children}</main>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type NavigationItem = NestedNavigationItem & {
|
||||
children?: NestedNavigationItem[];
|
||||
};
|
||||
|
||||
type NestedNavigationItem = Omit<Route, "children"> & {
|
||||
href?: string;
|
||||
current: boolean;
|
||||
};
|
||||
|
||||
const MainNavigation: React.FC<{
|
||||
nav: NavigationItem[];
|
||||
onNavitemClick?: () => void;
|
||||
}> = ({ nav, onNavitemClick }) => {
|
||||
const STORAGE_KEY = "sidebar-tracing-default-open";
|
||||
const getDefaultOpen = () => {
|
||||
const savedState = localStorage.getItem(STORAGE_KEY);
|
||||
if (savedState !== null) {
|
||||
try {
|
||||
return JSON.parse(savedState) as boolean;
|
||||
} catch (e) {
|
||||
console.error("Error parsing saved state: ", e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleDropDownClick = () => {
|
||||
const savedState = localStorage.getItem(STORAGE_KEY);
|
||||
const isOpen =
|
||||
savedState !== null ? (JSON.parse(savedState) as boolean) : false;
|
||||
const newState = !isOpen;
|
||||
localStorage.setItem(
|
||||
"sidebar-tracing-default-open",
|
||||
JSON.stringify(newState),
|
||||
);
|
||||
};
|
||||
return (
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{nav.map((item) => (
|
||||
<li key={item.name}>
|
||||
{(!item.children || item.children.length === 0) && item.href ? (
|
||||
<Link
|
||||
href={item.href}
|
||||
className={clsx(
|
||||
item.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
|
||||
)}
|
||||
onClick={onNavitemClick}
|
||||
>
|
||||
{item.icon && (
|
||||
<item.icon
|
||||
className={clsx(
|
||||
item.current
|
||||
? "text-indigo-600"
|
||||
: "text-gray-400 group-hover:text-indigo-600",
|
||||
"h-6 w-6 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{item.name}
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
) : item.children && item.children.length > 0 ? (
|
||||
<Disclosure
|
||||
as="div"
|
||||
defaultOpen={
|
||||
item.children.some((child) => child.current) ||
|
||||
getDefaultOpen()
|
||||
}
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button
|
||||
className="group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold leading-6 hover:bg-gray-50 hover:text-indigo-600"
|
||||
onClick={handleDropDownClick}
|
||||
>
|
||||
{item.icon && (
|
||||
<item.icon
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{item.name}
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
<ChevronRightIcon
|
||||
className={clsx(
|
||||
open ? "rotate-90 text-gray-500" : "text-gray-400",
|
||||
"ml-auto h-5 w-5 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel as="ul" className="mt-1 px-2">
|
||||
{item.children?.map((subItem) => (
|
||||
<li key={subItem.name}>
|
||||
{/* 44px */}
|
||||
<Link
|
||||
href={subItem.href ?? "#"}
|
||||
className={clsx(
|
||||
subItem.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"flex w-full items-center gap-x-3 rounded-md py-2 pl-9 pr-2 text-sm leading-6",
|
||||
)}
|
||||
>
|
||||
{subItem.name}
|
||||
{subItem.label && (
|
||||
<span className="self-center whitespace-nowrap break-keep rounded-sm border border-gray-200 px-1 py-0.5 text-xs text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600">
|
||||
{subItem.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
<FeedbackButtonWrapper className="w-full">
|
||||
<li className="group flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
|
||||
<MessageSquarePlus
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Feedback
|
||||
</li>
|
||||
</FeedbackButtonWrapper>
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,47 +1,70 @@
|
||||
import { type Flag } from "@/src/features/feature-flags/types";
|
||||
import { type Scope } from "@/src/features/rbac/constants/roleAccessRights";
|
||||
import {
|
||||
Database,
|
||||
LayoutDashboard,
|
||||
LifeBuoy,
|
||||
LineChart,
|
||||
ListTree,
|
||||
type LucideIcon,
|
||||
Settings,
|
||||
TextSelect,
|
||||
UsersIcon,
|
||||
PenSquareIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
export const ROUTES: Array<{
|
||||
export type Route = {
|
||||
name: string;
|
||||
pathname: string;
|
||||
icon: LucideIcon;
|
||||
featureFlag?: Flag;
|
||||
}> = [
|
||||
label?: string;
|
||||
rbacScope?: Scope;
|
||||
icon?: LucideIcon; // ignored for nested routes
|
||||
pathname?: string; // link, ignored if children
|
||||
children?: Array<Route>; // folder
|
||||
};
|
||||
|
||||
export const ROUTES: Route[] = [
|
||||
{
|
||||
name: "Dashboard",
|
||||
pathname: `/project/[projectId]`,
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{
|
||||
name: "Traces",
|
||||
pathname: `/project/[projectId]/traces`,
|
||||
name: "Tracing",
|
||||
icon: ListTree,
|
||||
},
|
||||
{
|
||||
name: "Generations",
|
||||
pathname: `/project/[projectId]/generations`,
|
||||
icon: TextSelect,
|
||||
},
|
||||
{
|
||||
name: "Scores",
|
||||
pathname: `/project/[projectId]/scores`,
|
||||
icon: LineChart,
|
||||
children: [
|
||||
{
|
||||
name: "Traces",
|
||||
pathname: `/project/[projectId]/traces`,
|
||||
},
|
||||
{
|
||||
name: "Sessions",
|
||||
pathname: `/project/[projectId]/sessions`,
|
||||
},
|
||||
{
|
||||
name: "Generations",
|
||||
pathname: `/project/[projectId]/generations`,
|
||||
},
|
||||
{
|
||||
name: "Scores",
|
||||
pathname: `/project/[projectId]/scores`,
|
||||
},
|
||||
{
|
||||
name: "Models",
|
||||
pathname: `/project/[projectId]/models`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Users",
|
||||
pathname: `/project/[projectId]/users`,
|
||||
icon: UsersIcon,
|
||||
},
|
||||
{
|
||||
name: "Prompts",
|
||||
pathname: "/project/[projectId]/prompts",
|
||||
icon: PenSquareIcon,
|
||||
label: "Beta",
|
||||
rbacScope: "prompts:read",
|
||||
},
|
||||
{
|
||||
name: "Datasets",
|
||||
pathname: `/project/[projectId]/datasets`,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user