Compare commits

...
174 Commits
Author SHA1 Message Date
Marc Klingen 2608878d20 chore: release v2.4.2
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests (18) (push) Waiting to run
CI/CD / tests (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-02-13 15:53:15 +01:00
Marc Klingen 62a2857619 docs: add DB_EXPORT_PAGE_SIZE to .env.prod.example 2024-02-13 15:48:49 +01:00
0c1942c2b3 fix(docker): remove the need for runtime variables at build time (#1067)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-02-13 15:40:16 +01:00
b95c93cdb1 fix: memory optimizes generation.export trpc to allow for larger exports (#1124)
* fix: memory optimizes generation.export trpc

* test: disable S3 uploads in export test

* refactor: modularize generations.export

* feat: adds streaming to S3 bucket

* feat: adds DB_EXPORT_PAGE_SIZE environment variable

* refactor: clean up generationsRouter

---------

Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-02-13 12:55:12 +00:00
Marc Klingen 3691099c74 chore: release v2.4.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests (18) (push) Waiting to run
CI/CD / tests (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-02-13 13:26:51 +01:00
Marc KlingenandGitHub 3f1ae71eec fix: feedback api route (#1146)
Fixes #1144
2024-02-13 13:14:27 +01:00
Max DeichmannandGitHub 668cf3f6c2 Feat: update openai pricing (#1145) 2024-02-13 11:36:22 +00:00
Marc KlingenandGitHub 6221be155d feat(security): add audit logging (#1142) 2024-02-13 11:35:54 +01:00
Marc KlingenandGitHub 3e13d191db chore: drop middleware previously used for IP blocklisting (#1143) 2024-02-12 22:53:07 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
61297826f4 chore(deps): bump lucide-react from 0.323.0 to 0.330.0 (#1140)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.323.0 to 0.330.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.330.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-12 23:47:36 +01:00
Max DeichmannandGitHub 3268ef6488 style: adjust contributing template (#1136)
remove
2024-02-12 20:54:20 +01:00
Marc Klingen 4aba13ec35 chore: release v2.4.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests (18) (push) Waiting to run
CI/CD / tests (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-02-12 17:52:28 +01:00
Marc Klingen 0f5d5cd9a0 fix(ui): invalidation of traces after delete 2024-02-12 17:46:58 +01:00
Marc KlingenandGitHub 78df145e20 fix(ui): invalidation of traces when deleting a single trace (#1134) 2024-02-12 12:00:57 +00:00
Max DeichmannandGitHub b35618f1c9 refactor: add betterstack env variable to env.prod.example (#1132) 2024-02-12 10:17:51 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a139ada85e chore(deps): bump the sentry group with 3 updates (#1110)
Bumps the sentry group with 3 updates: [@sentry/nextjs](https://github.com/getsentry/sentry-javascript), [@sentry/profiling-node](https://github.com/getsentry/sentry-javascript) and [@sentry/types](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/nextjs` from 7.99.0 to 7.100.1
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.100.1/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.99.0...7.100.1)

Updates `@sentry/profiling-node` from 1.3.5 to 7.100.1
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.100.1/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/commits/7.100.1)

Updates `@sentry/types` from 7.99.0 to 7.100.1
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.100.1/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.99.0...7.100.1)

---
updated-dependencies:
- dependency-name: "@sentry/nextjs"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
- dependency-name: "@sentry/profiling-node"
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: sentry
- dependency-name: "@sentry/types"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-12 00:07:57 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cdfb0c6b7c chore(deps): bump posthog-js from 1.104.4 to 1.105.7 (#1131)
Bumps [posthog-js](https://github.com/PostHog/posthog-js) from 1.104.4 to 1.105.7.
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/compare/v1.104.4...1.105.7)

---
updated-dependencies:
- dependency-name: posthog-js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-12 01:02:10 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c0bbf39b29 chore(deps-dev): bump the patches group with 3 updates (#1130)
Bumps the patches group with 3 updates: [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom), [postcss](https://github.com/postcss/postcss) and [tsx](https://github.com/privatenumber/tsx).


Updates `@types/react-dom` from 18.2.18 to 18.2.19
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `postcss` from 8.4.34 to 8.4.35
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.34...8.4.35)

Updates `tsx` from 4.7.0 to 4.7.1
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/develop/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.7.0...v4.7.1)

---
updated-dependencies:
- dependency-name: "@types/react-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: postcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: tsx
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-11 23:44:28 +00:00
Marc Klingen a6900ad3c4 fix(ui): use lightner colors in observation tree 2024-02-12 00:31:00 +01:00
21940a0464 feat(ui): improve trace view: collapse/expand observation tree + color coding of observation types (#916)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-02-11 23:47:46 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fe642f8e9e chore(deps-dev): bump @typescript-eslint/eslint-plugin from 6.20.0 to 6.21.0 (#1112)
chore(deps-dev): bump @typescript-eslint/eslint-plugin

Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.20.0 to 6.21.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.21.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-11 22:05:53 +00:00
c803b0d9c2 feat(ui): delete prompts and prompt versions + deduplication of prompt variables (#1116)
* fix and feat: update Var renaming and add functionality to delete prompt and versions

* pass ci by removing unused components

* ux improvements

---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-02-11 22:00:31 +00:00
Marc KlingenandGitHub e7086cf044 fix(ci): temporarily fix modelmatch test by waiting 5 sec (#1129)
* fix(ci): temporarily fix modelmatch test by waiting 5 sec

* fix
2024-02-11 21:56:19 +00:00
Marc KlingenandGitHub 9d49bbe987 chore(ui): responsive layout (#1126) 2024-02-10 03:29:38 +00:00
Marc Klingen f2c792d3dc chore: release v2.3.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests (18) (push) Waiting to run
CI/CD / tests (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-02-09 21:38:20 +01:00
Marc Klingen 0527fcf097 fix(ui): add space 2024-02-09 21:30:06 +01:00
Max DeichmannandGitHub 85737221d9 feat: send sdk logs to betterstack (#1125) 2024-02-09 20:01:46 +00:00
ffe95cf427 feat(ui): add request dashboard button (#1122)
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-02-09 19:25:08 +00:00
adec1476ea feat(ui): add quickstart examples to api key creation popup (#1119)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-02-09 19:10:21 +00:00
c5d7772fe6 chore(ui): convert model pricing in UI to per 1000 units (#1123)
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-02-09 19:05:04 +00:00
Max DeichmannandGitHub a88a6b8026 fix: upgrade API keys (#1120) 2024-02-09 16:44:48 +00:00
Marc Klingen 5d77e09eed docs: update readme and contributing 2024-02-09 15:38:22 +01:00
Hassieb PakzadandGitHub 85e5ea4661 fix: allow bearer auth for score ingestion via /api/public/ingestion (#1117)
* fix: allow bearer auth for score ingestion

* tests: add test for opening ingestion endpoint for scoring

* test: adds db check for bearer auth tests in ingestion
2024-02-09 13:51:22 +01:00
366df5a5c9 Fix decimals in scores filter on traces + generations table (#1012)
Co-authored-by: gitstart-langfuse <gitstart-langfuse@users.noreply.github.com>
Co-authored-by: GitStart-LangFuse <151670644+gitstart-langfuse@users.noreply.github.com>
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-02-09 08:44:12 +01:00
4441383dfd docs: update readme (#1107)
docs: update and significantly shorten readme
---------

Co-authored-by: Clemo <121163007+clemra@users.noreply.github.com>
2024-02-08 22:18:02 +00:00
Richard KrümmelandGitHub 7f7696e9ff feat(ui): add in-product notifications (#1057) 2024-02-08 19:41:30 +00:00
Max Deichmann a829447410 chore: release v2.2.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests (18) (push) Waiting to run
CI/CD / tests (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-02-07 11:06:35 +01:00
Hassieb PakzadandGitHub 53e7ff4e16 feat: expand generation model units (#1095) 2024-02-07 11:05:07 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4a85b4a4f2 chore(deps): bump the patches group with 3 updates (#1101)
Bumps the patches group with 3 updates: [posthog-node](https://github.com/PostHog/posthog-js-lite/tree/HEAD/posthog-node), [react-hook-form](https://github.com/react-hook-form/react-hook-form) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).


Updates `posthog-node` from 3.6.1 to 3.6.2
- [Release notes](https://github.com/PostHog/posthog-js-lite/releases)
- [Changelog](https://github.com/PostHog/posthog-js-lite/blob/main/posthog-node/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js-lite/commits/posthog-node-v3.6.2/posthog-node)

Updates `react-hook-form` from 7.50.0 to 7.50.1
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.50.0...v7.50.1)

Updates `@types/react` from 18.2.54 to 18.2.55
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: posthog-node
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: react-hook-form
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-07 03:16:05 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d2e6dca3d3 chore(deps-dev): bump @typescript-eslint/parser from 6.20.0 to 6.21.0 (#1103)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.20.0 to 6.21.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.21.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-07 03:08:08 +00:00
Marc KlingenandGitHub cdbdc0af67 feat(api): add notice regarding hostname to default 401 (#1088) 2024-02-07 04:02:26 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2af9b52213 chore(deps): bump @aws-sdk/s3-request-presigner from 3.504.0 to 3.507.0 (#1104)
Bumps [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) from 3.504.0 to 3.507.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-request-presigner/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.507.0/packages/s3-request-presigner)

---
updated-dependencies:
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-07 04:01:08 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
894a3d1b32 chore(deps-dev): bump the patches group with 4 updates (#1083)
Bumps the patches group with 4 updates: [@testing-library/jest-dom](https://github.com/testing-library/jest-dom), [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react), [postcss](https://github.com/postcss/postcss) and [prettier](https://github.com/prettier/prettier).


Updates `@testing-library/jest-dom` from 6.4.1 to 6.4.2
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v6.4.1...v6.4.2)

Updates `@types/react` from 18.2.51 to 18.2.54
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `postcss` from 8.4.33 to 8.4.34
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.33...8.4.34)

Updates `prettier` from 3.2.4 to 3.2.5
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.2.4...3.2.5)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: postcss
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-06 02:53:42 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
82f0134368 chore(deps): bump react-hook-form from 7.49.3 to 7.50.0 (#1084)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.49.3 to 7.50.0.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.49.3...v7.50.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-06 02:41:56 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e52ba46b0e chore(deps): bump lucide-react from 0.321.0 to 0.323.0 (#1085)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.321.0 to 0.323.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.323.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-06 02:37:21 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6aca4954ef chore(deps): bump @aws-sdk/client-s3 from 3.504.0 to 3.507.0 (#1086)
Bumps [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) from 3.504.0 to 3.507.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.507.0/clients/client-s3)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-06 03:32:53 +01:00
Marc Klingen 9d5fa4e2f1 chore: fix discussion template 2024-02-05 10:51:50 +01:00
Marc Klingen c1db242edd chore: fix discussion template 2024-02-05 10:51:21 +01:00
Marc Klingen 91a2c63c88 chore: fix discussion template 2024-02-05 10:50:25 +01:00
Marc Klingen f7d9bd7297 chore: add template for gh discussions 2024-02-05 10:49:16 +01:00
Max DeichmannandGitHub d94e880520 fix: openai model prices (#1068) 2024-02-03 18:28:18 +01:00
Yash KhareandGitHub 74e9053f85 feat(dashboard): enable selection of score names in legend of chart + slider (#907) 2024-02-03 01:53:44 +00:00
Marc Klingen ae5260d808 docs: update example envs 2024-02-03 02:29:50 +01:00
Marc KlingenandGitHub f72f8b7e2c chore(deps): upgrade prisma and fix tests (#1064) 2024-02-03 01:34:05 +01:00
Marc Klingen 43108eccde docs: readme 2024-02-03 01:16:34 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c93c9560e1 chore(deps): bump the patches group with 1 update (#1060)
Bumps the patches group with 1 update: [@tanstack/react-table](https://github.com/TanStack/table/tree/HEAD/packages/react-table).


Updates `@tanstack/react-table` from 8.11.7 to 8.11.8
- [Release notes](https://github.com/TanStack/table/releases)
- [Commits](https://github.com/TanStack/table/commits/v8.11.8/packages/react-table)

---
updated-dependencies:
- dependency-name: "@tanstack/react-table"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 23:31:07 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ddad1afd7b chore(deps): bump @aws-sdk/client-s3 from 3.501.0 to 3.504.0 (#1061)
Bumps [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) from 3.501.0 to 3.504.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.504.0/clients/client-s3)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 23:21:19 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2b70ae3742 chore(deps): bump @aws-sdk/s3-request-presigner from 3.501.0 to 3.504.0 (#1063)
Bumps [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner) from 3.501.0 to 3.504.0.
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-request-presigner/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.504.0/packages/s3-request-presigner)

---
updated-dependencies:
- dependency-name: "@aws-sdk/s3-request-presigner"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 23:16:52 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b110fd7932 chore(deps): bump posthog-js from 1.103.1 to 1.104.4 (#1062)
Bumps [posthog-js](https://github.com/PostHog/posthog-js) from 1.103.1 to 1.104.4.
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/compare/v1.103.1...v1.104.4)

---
updated-dependencies:
- dependency-name: posthog-js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-03 00:12:21 +01:00
Marc Klingen cd537fb571 chore: release v2.1.2
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 53s
CI/CD / tests (20) (push) Failing after 42s
CI/CD / test-docker-build (push) Successful in 2m59s
CI/CD / e2e-tests (push) Failing after 45s
release.yml / release (push) Failing after 3s
CI/CD / lint (push) Failing after 22m27s
CI/CD / all-ci-passed (push) Waiting to run
2024-02-02 23:22:12 +01:00
Marc KlingenandGitHub 3cbd252980 chore(cloud): add environment labeling (#1058) 2024-02-02 23:02:19 +01:00
Max DeichmannandGitHub 0a99988e6e perf: do not load observation i/o from db on traces.byId (#1053) 2024-02-02 16:01:19 +00:00
Marc KlingenandGitHub a19d20d973 fix: remove Expires from S3 PutObject (#1047)
Fixes #1046
2024-02-02 03:56:47 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b86eab9e04 chore(deps-dev): bump the patches group with 1 update (#1037)
Bumps the patches group with 1 update: [@playwright/test](https://github.com/microsoft/playwright).


Updates `@playwright/test` from 1.41.1 to 1.41.2
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.41.1...v1.41.2)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 00:57:37 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b36530ff96 chore(deps): bump lucide-react from 0.316.0 to 0.321.0 (#1039)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.316.0 to 0.321.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.321.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 00:50:03 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1019298060 chore(deps-dev): bump @testing-library/react from 14.1.2 to 14.2.1 (#1040)
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 14.1.2 to 14.2.1.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v14.1.2...v14.2.1)

---
updated-dependencies:
- dependency-name: "@testing-library/react"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 00:45:30 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
99a2ad30ce chore(deps-dev): bump @typescript-eslint/parser from 6.19.1 to 6.20.0 (#1038)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.19.1 to 6.20.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.20.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-02 00:40:21 +00:00
Marc Klingen 9783a22abf chore: release v2.1.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / test-docker-build (push) Successful in 2m42s
CI/CD / tests (18) (push) Failing after 1m13s
CI/CD / tests (20) (push) Failing after 42s
CI/CD / e2e-tests (push) Failing after 44s
CI/CD / lint (push) Successful in 4m9s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 17s
2024-02-02 00:48:34 +01:00
Marc KlingenandGitHub 68f63b3b90 fix(ui): generation exports from UI (#1042) 2024-02-02 00:18:58 +01:00
Marc Klingen 1189d13c14 chore: release v2.1.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 47s
CI/CD / e2e-tests (push) Failing after 44s
CI/CD / lint (push) Successful in 4m1s
CI/CD / test-docker-build (push) Successful in 3m43s
release.yml / release (push) Failing after 3s
CI/CD / tests (18) (push) Failing after 11m20s
CI/CD / all-ci-passed (push) Waiting to run
2024-02-01 21:08:59 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
243a784480 chore(deps): bump the patches group with 4 updates (#1035)
Bumps the patches group with 4 updates: cmdk, [@testing-library/jest-dom](https://github.com/testing-library/jest-dom), [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react).


Updates `cmdk` from 0.2.0 to 0.2.1

Updates `@testing-library/jest-dom` from 6.4.0 to 6.4.1
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v6.4.0...v6.4.1)

Updates `@types/jest` from 29.5.11 to 29.5.12
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

Updates `@types/react` from 18.2.48 to 18.2.51
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: cmdk
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/jest"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 21:08:19 +01:00
c89b8c3bab fix(ui): update regex matching description (#1031)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-02-01 20:54:57 +01:00
b5172219af security: upgrade nodemailer from 6.9.8 to 6.9.9 (#1027)
* fix: package.json to reduce vulnerabilities

The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-NODEMAILER-6219989

* update

---------

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2024-02-01 20:53:40 +01:00
Marc Klingen db90f85041 fix(ui): disabled state of AddModel button 2024-02-01 17:41:19 +01:00
Marc KlingenandGitHub db9f90aa91 fix(models): allow price = 0 (#1030) 2024-02-01 15:56:51 +00:00
Marc KlingenandGitHub 41b19582cc feat(models): allow to set input/output/total price without restrictions (#1029) 2024-02-01 16:50:25 +01:00
766102797b feat(models): allow custom regex for user-defined models (#1022)
---------

Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-02-01 16:42:21 +01:00
Max Deichmann 773cb1312b chore: release v2.0.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m0s
CI/CD / test-docker-build (push) Successful in 3m27s
CI/CD / e2e-tests (push) Failing after 49s
CI/CD / tests (20) (push) Failing after 1m39s
CI/CD / lint (push) Successful in 4m44s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2024-02-01 16:31:54 +01:00
Max DeichmannandGitHub bb593020d0 fix: gh action (#1028) 2024-02-01 16:27:06 +01:00
Max DeichmannandGitHub 33ffe62add fix: display total cost correctly (#1026) 2024-02-01 14:56:49 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6e1aabb2de chore(deps-dev): bump @testing-library/jest-dom from 6.2.1 to 6.4.0 (#988)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 6.2.1 to 6.4.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v6.2.1...v6.4.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-31 22:14:20 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ab3bdebc57 chore(deps): bump the sentry group with 1 update (#1000)
Bumps the sentry group with 1 update: [@sentry/nextjs](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/nextjs` from 7.95.0 to 7.99.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.95.0...7.99.0)

---
updated-dependencies:
- dependency-name: "@sentry/nextjs"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-31 22:09:55 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
20d00e5ef2 chore(deps-dev): bump @mermaid-js/mermaid-cli from 10.6.1 to 10.7.0 (#1003)
Bumps [@mermaid-js/mermaid-cli](https://github.com/mermaid-js/mermaid-cli) from 10.6.1 to 10.7.0.
- [Release notes](https://github.com/mermaid-js/mermaid-cli/releases)
- [Commits](https://github.com/mermaid-js/mermaid-cli/compare/10.6.1...10.7.0)

---
updated-dependencies:
- dependency-name: "@mermaid-js/mermaid-cli"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-31 22:04:28 +00:00
Marc Klingen e689c1cacb chore: move github discussions to org level 2024-01-31 22:59:35 +01:00
Richard KrümmelandGitHub 37c0737624 feat(db): add openai fine tuned and vertex ai models (#995) 2024-01-31 14:19:17 +01:00
23bc448088 fix(ui): fix create new model label (#1005)
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-01-31 11:46:15 +01:00
Max Deichmann f1bdea98be chore: release v2.0.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (20) (push) Failing after 45s
CI/CD / tests (18) (push) Failing after 1m17s
CI/CD / e2e-tests (push) Failing after 45s
CI/CD / test-docker-build (push) Successful in 3m16s
CI/CD / lint (push) Successful in 4m1s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 4s
2024-01-30 19:56:07 +01:00
Max DeichmannandGitHub 9c00d1905f perf: reduce batch size (#997) 2024-01-30 17:47:31 +00:00
Max DeichmannandGitHub a31d737538 perf: improve migration script (#996) 2024-01-30 17:00:57 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7535872222 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 6.19.1 to 6.20.0 (#985)
chore(deps-dev): bump @typescript-eslint/eslint-plugin

Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.19.1 to 6.20.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.20.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-30 15:47:44 +00:00
7e7bf99fac refactor: improve usage/cost (#992)
---------

Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-01-30 15:43:08 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ac5a7a3171 chore(deps): bump the patches group with 1 update (#983)
Bumps the patches group with 1 update: [js-tiktoken](https://github.com/dqbd/tiktoken).


Updates `js-tiktoken` from 1.0.8 to 1.0.10
- [Release notes](https://github.com/dqbd/tiktoken/releases)
- [Changelog](https://github.com/dqbd/tiktoken/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dqbd/tiktoken/compare/js-tiktoken@1.0.8...js-tiktoken@1.0.10)

---
updated-dependencies:
- dependency-name: js-tiktoken
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-30 01:03:47 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
eda5da635d chore(deps): bump clsx from 2.0.0 to 2.1.0 (#986)
Bumps [clsx](https://github.com/lukeed/clsx) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/lukeed/clsx/releases)
- [Commits](https://github.com/lukeed/clsx/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: clsx
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-30 01:56:29 +01:00
Max DeichmannandGitHub dc0cef5dbb feat: add cost to observations API (#947) 2024-01-29 19:09:16 +01:00
Marc KlingenandGitHub 24607c10ee chore(deps): bump dependencies (#979) 2024-01-29 15:23:15 +00:00
Marc KlingenandGitHub 8ddcfa8136 refactor: typing of AutoCompleteOption (#978) 2024-01-29 15:15:47 +00:00
Max DeichmannandGitHub 1f54043778 refactor: correct model migration command (#977) 2024-01-29 15:04:31 +00:00
Richard KrümmelandGitHub 97bec94093 feat(db): improve seeding script for prompts (#965) 2024-01-29 14:16:38 +01:00
93df11e043 feat: Add filtering by scores to the generations table (#880)
Co-authored-by: gitstart_bot <gitstart_bot@users.noreply.github.com>
Co-authored-by: gitstart-langfuse <gitstart-langfuse@users.noreply.github.com>
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-01-29 11:44:26 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Marc Klingen
327b66c241 chore(deps): bump the patches group with 13 updates (#970)
Bumps the patches group with 13 updates:

| Package | From | To |
| --- | --- | --- |
| [@headlessui/react](https://github.com/tailwindlabs/headlessui/tree/HEAD/packages/@headlessui-react) | `1.7.17` | `1.7.18` |
| [@react-email/components](https://github.com/resend/react-email/tree/HEAD/packages/components) | `0.0.12` | `0.0.14` |
| [@react-email/render](https://github.com/resend/react-email/tree/HEAD/packages/render) | `0.0.10` | `0.0.12` |
| [@tanstack/react-table](https://github.com/TanStack/table/tree/HEAD/packages/react-table) | `8.11.3` | `8.11.7` |
| [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) | `3.35.0` | `3.35.1` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.49.2` | `7.49.3` |
| [tailwind-merge](https://github.com/dcastil/tailwind-merge) | `2.2.0` | `2.2.1` |
| [@types/eslint](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/eslint) | `8.56.1` | `8.56.2` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `18.2.46` | `18.2.48` |
| [@types/uuid](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/uuid) | `9.0.7` | `9.0.8` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `6.19.0` | `6.19.1` |
| [autoprefixer](https://github.com/postcss/autoprefixer) | `10.4.16` | `10.4.17` |
| [release-it](https://github.com/release-it/release-it) | `17.0.1` | `17.0.3` |


Updates `@headlessui/react` from 1.7.17 to 1.7.18
- [Release notes](https://github.com/tailwindlabs/headlessui/releases)
- [Changelog](https://github.com/tailwindlabs/headlessui/blob/main/packages/@headlessui-react/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/headlessui/commits/@headlessui/react@v1.7.18/packages/@headlessui-react)

Updates `@react-email/components` from 0.0.12 to 0.0.14
- [Release notes](https://github.com/resend/react-email/releases)
- [Commits](https://github.com/resend/react-email/commits/v0.0.14/packages/components)

Updates `@react-email/render` from 0.0.10 to 0.0.12
- [Release notes](https://github.com/resend/react-email/releases)
- [Commits](https://github.com/resend/react-email/commits/v0.0.12/packages/render)

Updates `@tanstack/react-table` from 8.11.3 to 8.11.7
- [Release notes](https://github.com/TanStack/table/releases)
- [Commits](https://github.com/TanStack/table/commits/v8.11.7/packages/react-table)

Updates `core-js` from 3.35.0 to 3.35.1
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/commits/v3.35.1/packages/core-js)

Updates `react-hook-form` from 7.49.2 to 7.49.3
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.49.2...v7.49.3)

Updates `tailwind-merge` from 2.2.0 to 2.2.1
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v2.2.0...v2.2.1)

Updates `@types/eslint` from 8.56.1 to 8.56.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/eslint)

Updates `@types/react` from 18.2.46 to 18.2.48
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@types/uuid` from 9.0.7 to 9.0.8
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/uuid)

Updates `@typescript-eslint/eslint-plugin` from 6.19.0 to 6.19.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.19.1/packages/eslint-plugin)

Updates `autoprefixer` from 10.4.16 to 10.4.17
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.4.16...10.4.17)

Updates `release-it` from 17.0.1 to 17.0.3
- [Release notes](https://github.com/release-it/release-it/releases)
- [Changelog](https://github.com/release-it/release-it/blob/main/CHANGELOG.md)
- [Commits](https://github.com/release-it/release-it/compare/17.0.1...17.0.3)

---
updated-dependencies:
- dependency-name: "@headlessui/react"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@react-email/components"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@react-email/render"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@tanstack/react-table"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: core-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: react-hook-form
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: tailwind-merge
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/eslint"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@types/uuid"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: autoprefixer
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
- dependency-name: release-it
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patches
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-01-27 19:08:09 +01:00
Marc KlingenandGitHub f3fd52fdbb feat(ui): custom model prices and tokenization (#954) 2024-01-27 18:52:31 +01:00
Marc KlingenandGitHub f3727c6209 revert(api): increase of max body size (#975) 2024-01-27 18:44:18 +01:00
Marc KlingenandGitHub 8a3281db03 chore: fix typo (#974) 2024-01-27 17:39:41 +00:00
Max Deichmann 19a21dca5a Merge branch 'main' of https://github.com/langfuse/langfuse 2024-01-26 23:45:06 +01:00
Max Deichmann 09dcad7427 perf: increase body size 2024-01-26 23:45:02 +01:00
Max DeichmannandGitHub 172cbed9b5 feat: add new openai model prices (#967) 2024-01-26 17:37:19 +00:00
Richard KrümmelandGitHub f7e20a9ff6 fix: error log for api prompts (#966) 2024-01-26 16:50:50 +01:00
Max DeichmannandGitHub 4b213151d1 fix: retrieve api prompts (#963) 2024-01-26 15:20:46 +00:00
Max DeichmannandGitHub dd99efe692 refactor: formatting (#962) 2024-01-26 13:41:40 +00:00
Richard KrümmelandGitHub ff303750cc feat(ui): add improved prompt ui (#961) 2024-01-26 14:20:58 +01:00
Max DeichmannandGitHub ef6e1e0bf4 perf: use js-tiktoken (#957) 2024-01-25 20:42:46 +01:00
Max DeichmannandGitHub c0ed93132f fix: support emoji in tokenizer (#956) 2024-01-25 11:18:53 +00:00
Max DeichmannandGitHub 3503537539 refactor: db logging in dev mode (#952) 2024-01-24 18:35:23 +00:00
Max DeichmannandGitHub b9a3f33e97 fix: fix token configuration (#951) 2024-01-24 18:14:28 +00:00
Max DeichmannandGitHub c2f612d0b8 fix: fix toktoken tokenizer for unexpected input (#950) 2024-01-24 15:55:24 +00:00
Marc KlingenandGitHub 9f492ecec9 fix(sessions): composite ids (#946) 2024-01-24 15:27:31 +01:00
Max DeichmannandGitHub d87432cce3 fix: pull metrics table from observations only (#945) 2024-01-24 12:46:41 +00:00
Max DeichmannandGitHub cf34c91734 fix: model usage chart (#944) 2024-01-24 12:20:34 +00:00
Max DeichmannandGitHub 851b54fa47 perf: Improve migration (#943) 2024-01-24 12:56:13 +01:00
Richard KrümmelandGitHub c858a1800b fix(security): add validation for html in inputs (#927) 2024-01-24 11:43:33 +01:00
Max DeichmannandGitHub f2405f1561 feat: revamp pricing and cost (#901) 2024-01-24 10:53:48 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
deee890647 chore(deps-dev): bump @testing-library/jest-dom from 6.1.5 to 6.2.1 (#936)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 6.1.5 to 6.2.1.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v6.1.5...v6.2.1)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-24 00:18:28 +00:00
Marc Klingen ebabf0e100 chore: release v1.34.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 50s
CI/CD / tests (20) (push) Failing after 55s
release.yml / release (push) Failing after 3s
CI/CD / e2e-tests (push) Failing after 45s
CI/CD / test-docker-build (push) Successful in 3m12s
CI/CD / lint (push) Successful in 4m0s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-24 01:12:22 +01:00
Marc KlingenandGitHub e05d4693d2 fix(ui): encodeURIComponent for session and user ids (#940)
Before, special characters in those ids would break the UI as the ids are used as URI components
2024-01-24 01:11:37 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
77d49adc00 chore(deps): bump react-icons from 4.12.0 to 5.0.1 (#935)
Bumps [react-icons](https://github.com/react-icons/react-icons) from 4.12.0 to 5.0.1.
- [Release notes](https://github.com/react-icons/react-icons/releases)
- [Commits](https://github.com/react-icons/react-icons/compare/v4.12.0...v5.0.1)

---
updated-dependencies:
- dependency-name: react-icons
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 23:55:10 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7dc45f0074 chore(deps): bump lucide-react from 0.306.0 to 0.314.0 (#937)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 0.306.0 to 0.314.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/0.314.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-24 00:49:42 +01:00
44d1da3c9c fix: fix playwright test timeouts (#868)
Co-authored-by: gitstart_bot <gitstart_bot@users.noreply.github.com>
Co-authored-by: gitstart-langfuse <gitstart-langfuse@users.noreply.github.com>
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-01-23 23:47:53 +01:00
jhlopenandGitHub 564ba4bd68 chore: mitigate type truncation in VS Code tooltip (#932) 2024-01-23 23:38:30 +01:00
Marc Klingen 63df788908 chore: release v1.34.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 47s
CI/CD / tests (20) (push) Failing after 44s
release.yml / release (push) Failing after 2s
CI/CD / e2e-tests (push) Failing after 41s
CI/CD / test-docker-build (push) Successful in 3m14s
CI/CD / lint (push) Successful in 5m59s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-23 22:59:17 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fb463cbf98 chore(deps): bump the sentry group with 2 updates (#931)
Bumps the sentry group with 2 updates: [@sentry/nextjs](https://github.com/getsentry/sentry-javascript) and [@sentry/profiling-node](https://github.com/getsentry/profiling-node).


Updates `@sentry/nextjs` from 7.92.0 to 7.95.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.92.0...7.95.0)

Updates `@sentry/profiling-node` from 1.3.2 to 1.3.5
- [Release notes](https://github.com/getsentry/profiling-node/releases)
- [Changelog](https://github.com/getsentry/profiling-node/blob/main/CHANGES)
- [Commits](https://github.com/getsentry/profiling-node/compare/v1.3.2...v1.3.5)

---
updated-dependencies:
- dependency-name: "@sentry/nextjs"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
- dependency-name: "@sentry/profiling-node"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: sentry
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 21:56:41 +00:00
Marc KlingenandGitHub cfd99efd48 feat(ui): add ordering to sessions table (#930) 2024-01-23 21:33:12 +00:00
Marc KlingenandGitHub 3b2c5f8bf3 feat(ui): add ordering to generations table (#929) 2024-01-23 21:23:47 +00:00
Marc KlingenandGitHub d08a276b67 perf: revert splitting of generations trpc call (#928) 2024-01-23 21:03:17 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
23bc2cd39c chore(deps): bump the next group with 2 updates (#924)
Bumps the next group with 2 updates: [next](https://github.com/vercel/next.js) and [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next).


Updates `next` from 14.0.4 to 14.1.0
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.0.4...v14.1.0)

Updates `eslint-config-next` from 14.0.4 to 14.1.0
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v14.1.0/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: next
- dependency-name: eslint-config-next
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: next
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 14:57:01 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a31eda8c68 chore(deps): bump date-fns from 3.1.0 to 3.3.1 (#926)
Bumps [date-fns](https://github.com/date-fns/date-fns) from 3.1.0 to 3.3.1.
- [Release notes](https://github.com/date-fns/date-fns/releases)
- [Changelog](https://github.com/date-fns/date-fns/blob/main/CHANGELOG.md)
- [Commits](https://github.com/date-fns/date-fns/compare/v3.1.0...v3.3.1)

---
updated-dependencies:
- dependency-name: date-fns
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 15:52:08 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d044ae51d1 chore(deps-dev): bump @typescript-eslint/parser from 6.16.0 to 6.19.1 (#922)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.16.0 to 6.19.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.19.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 22:11:22 +00:00
Marc KlingenandGitHub 552578afc3 fix(ui): improve ui of tag manager (#921) 2024-01-22 21:41:52 +00:00
Marc Klingen 00997db325 chore: release v1.33.10
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m38s
CI/CD / tests (20) (push) Failing after 2m2s
CI/CD / test-docker-build (push) Successful in 5m10s
CI/CD / lint (push) Successful in 5m33s
CI/CD / e2e-tests (push) Failing after 2m17s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2024-01-22 22:17:00 +01:00
Marc KlingenandGitHub 550464cf0b perf: native db upserts for TraceProcessor and ObservationProcessor (#920) 2024-01-22 22:05:48 +01:00
Max DeichmannandGitHub e43568a3d2 fix: send error only in custom sentry alert (#919) 2024-01-22 18:21:17 +00:00
Max DeichmannandGitHub 4869e8a093 fix: remove sentry instrumentation (#918) 2024-01-22 18:16:27 +00:00
Max Deichmann d9b307ec87 chore: release v1.33.9
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m16s
CI/CD / tests (20) (push) Failing after 42s
release.yml / release (push) Failing after 3s
CI/CD / e2e-tests (push) Failing after 1m1s
CI/CD / test-docker-build (push) Successful in 3m52s
CI/CD / lint (push) Successful in 4m15s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-22 13:11:40 +01:00
Max Deichmann 3abfbef4c3 refactor: update openapi docs 2024-01-22 13:08:06 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Max Deichmann
86aa1eb34f chore(deps): bump posthog-js from 1.96.1 to 1.100.0 (#885)
Bumps [posthog-js](https://github.com/PostHog/posthog-js) from 1.96.1 to 1.100.0.
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/compare/v1.96.1...v1.100.0)

---
updated-dependencies:
- dependency-name: posthog-js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-01-22 11:35:24 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Max Deichmann
59edc9851a chore(deps): bump the prisma group with 2 updates (#884)
Bumps the prisma group with 2 updates: [@prisma/client](https://github.com/prisma/prisma/tree/HEAD/packages/client) and [prisma](https://github.com/prisma/prisma/tree/HEAD/packages/cli).


Updates `@prisma/client` from 5.7.1 to 5.8.1
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/5.8.1/packages/client)

Updates `prisma` from 5.7.1 to 5.8.1
- [Release notes](https://github.com/prisma/prisma/releases)
- [Commits](https://github.com/prisma/prisma/commits/5.8.1/packages/cli)

---
updated-dependencies:
- dependency-name: "@prisma/client"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prisma
- dependency-name: prisma
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: prisma
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Max Deichmann <m.deichmann@tum.de>
2024-01-22 10:17:09 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0e31dcef99 chore(deps-dev): bump @typescript-eslint/eslint-plugin from 6.17.0 to 6.19.0 (#887)
chore(deps-dev): bump @typescript-eslint/eslint-plugin

Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.17.0 to 6.19.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.19.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 11:12:11 +01:00
Max DeichmannandGitHub a67235ab8a feat: support ordering traces on the GET API (#913) 2024-01-19 13:37:31 +00:00
Max Deichmann eb88c9bdf8 chore: release v1.33.8
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 51s
CI/CD / tests (20) (push) Failing after 44s
release.yml / release (push) Failing after 3s
CI/CD / e2e-tests (push) Failing after 48s
CI/CD / lint (push) Successful in 4m17s
CI/CD / test-docker-build (push) Successful in 4m5s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-19 11:58:20 +01:00
Richard KrümmelandGitHub 77e4791cbe feat(ui): add option to keep tracing side menu open (#912) 2024-01-19 11:54:44 +01:00
Max DeichmannandGitHub 5d922dfae7 fix: filter generations metadata correctly (#911) 2024-01-18 15:35:41 +00:00
Max DeichmannandGitHub c704c84451 fix: fix jsonl export to correctly append output (#909) 2024-01-18 13:14:37 +00:00
Max DeichmannandGitHub 6f23488438 fix: sentry reports only unknown errors (#908) 2024-01-18 12:14:54 +00:00
Max Deichmann ca50abb290 chore: release v1.33.7
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 46s
CI/CD / tests (20) (push) Failing after 42s
CI/CD / e2e-tests (push) Failing after 42s
release.yml / release (push) Failing after 2s
CI/CD / test-docker-build (push) Successful in 3m34s
CI/CD / lint (push) Successful in 4m6s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-17 21:18:44 +01:00
Max DeichmannandGitHub 15067114a2 feat: support array of strings in model parameters (#905) 2024-01-17 20:13:12 +00:00
Max DeichmannandGitHub 645f14535b fic: use encoding once (#904) 2024-01-17 16:58:25 +00:00
Max Deichmann 250a5b03d2 chore: release v1.33.6
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 58s
CI/CD / tests (20) (push) Failing after 1m7s
release.yml / release (push) Failing after 3s
CI/CD / test-docker-build (push) Successful in 3m6s
CI/CD / e2e-tests (push) Failing after 48s
CI/CD / lint (push) Successful in 4m1s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-17 16:25:56 +01:00
Max DeichmannandGitHub 5ee060dfc6 refactor: remove external id uniqueness constraint (#903) 2024-01-17 15:24:31 +00:00
Max DeichmannandGitHub f08502cde4 fix: upsert traces on ingestion only on the id (#900) 2024-01-17 13:55:32 +00:00
Max Deichmann 90ecb88249 chore: release v1.33.5
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 48s
CI/CD / tests (20) (push) Failing after 44s
release.yml / release (push) Failing after 2s
CI/CD / e2e-tests (push) Failing after 44s
CI/CD / test-docker-build (push) Successful in 2m47s
CI/CD / lint (push) Successful in 4m15s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-17 14:05:47 +01:00
Max DeichmannandGitHub 16e60f74da fix: capture ingestion errors in sentry (#899) 2024-01-17 12:52:56 +00:00
Max Deichmann 0ab252cc7c chore: release v1.33.3
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 1m10s
CI/CD / tests (20) (push) Failing after 43s
CI/CD / e2e-tests (push) Failing after 42s
release.yml / release (push) Failing after 3s
CI/CD / test-docker-build (push) Successful in 3m39s
CI/CD / lint (push) Successful in 4m7s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-17 13:36:03 +01:00
Max DeichmannandGitHub ebadbaf30d fix: increase ingestion API body size (#898) 2024-01-17 12:23:06 +00:00
Max Deichmann 7ce6b6817e chore: release v1.33.2
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m32s
CI/CD / tests (20) (push) Failing after 2m2s
CI/CD / e2e-tests (push) Failing after 2m11s
release.yml / release (push) Failing after 3s
CI/CD / test-docker-build (push) Successful in 5m16s
CI/CD / lint (push) Successful in 5m31s
CI/CD / all-ci-passed (push) Waiting to run
2024-01-17 11:39:23 +01:00
Max DeichmannandGitHub 6d4040d5ed fix: free tiktoken encoder to prevent OOM issues (#897) 2024-01-17 10:32:28 +00:00
Max DeichmannandGitHub 4a7a4c0add feat(infrastructure): add decimal.js support for trpc (#895) 2024-01-16 19:29:29 +01:00
Max Deichmann 883e53d8b5 chore: release v1.33.1
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m36s
CI/CD / tests (20) (push) Failing after 2m2s
CI/CD / e2e-tests (push) Failing after 2m14s
CI/CD / lint (push) Successful in 5m48s
CI/CD / test-docker-build (push) Successful in 6m10s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2024-01-16 18:17:46 +01:00
Richard KrümmelandGitHub a9e6b9ae5c fix: remove tags json view from TracePreview (#894) 2024-01-16 17:12:10 +00:00
Max Deichmann a61ee1f29f chore: release v1.33.0
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m50s
CI/CD / tests (20) (push) Failing after 2m2s
CI/CD / test-docker-build (push) Successful in 4m42s
CI/CD / lint (push) Successful in 5m33s
CI/CD / e2e-tests (push) Failing after 2m19s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2024-01-16 17:44:50 +01:00
Richard KrümmelandGitHub eefdf60e31 feat(ui): add ui for tags on traces (#879) 2024-01-16 17:44:16 +01:00
Max DeichmannandGitHub 19a633a756 fix: add new cost visualisation to trace detail header (#893)
add new cost to traces
2024-01-16 15:49:55 +00:00
Max DeichmannandGitHub e84c1a3ab8 fix: upgrade decimal on export (#892) 2024-01-16 15:32:53 +00:00
Max Deichmann 4d41aa7e53 chore: release v1.32.3
CI/CD / push-docker-image (push) Blocked by required conditions
CI/CD / tests (18) (push) Failing after 2m31s
CI/CD / tests (20) (push) Failing after 2m21s
CI/CD / test-docker-build (push) Successful in 4m44s
CI/CD / e2e-tests (push) Failing after 2m13s
CI/CD / lint (push) Successful in 5m48s
CI/CD / all-ci-passed (push) Waiting to run
release.yml / release (push) Failing after 3s
2024-01-16 16:09:48 +01:00
Max DeichmannandGitHub a4c28cc324 fix: generations table latency (#891) 2024-01-16 15:09:28 +00:00
Max DeichmannandGitHub 187f2e7b1d fix: convert decimals on trpc to number (#890) 2024-01-16 14:50:52 +00:00
180 changed files with 10257 additions and 4003 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
[codespell]
skip = .git,*.pdf,*.svg,package-lock.json,*.prisma
ignore-words-list = afterall
ignore-words-list = afterall,vertx
+13
View File
@@ -16,8 +16,21 @@ 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=
# Set during docker build of application
# Used to disable environment verification at build time
# DOCKER_BUILD=1
+1 -8
View File
@@ -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.
+72 -34
View File
@@ -1,32 +1,75 @@
# 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=
# Exports are streamed to S3 in pages to avoid memory issues
# The page size can be adjusted if needed to optimize performance
# DB_EXPORT_PAGE_SIZE=1000
### 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=
@@ -35,24 +78,19 @@ 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=
# Betterstack
# LANGFUSE_TEAM_BETTERSTACK_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.
# S3 storage (eg for exports), necessary for serverless deployments
S3_ENDPOINT=""
S3_ACCESS_KEY_ID=""
S3_SECRET_ACCESS_KEY=""
S3_BUCKET_NAME=""
S3_REGION=""
### END Langfuse Cloud Config
+11
View File
@@ -0,0 +1,11 @@
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.
+4 -4
View File
@@ -1,7 +1,7 @@
contact_links:
- name: 🤗 Get Help
url: https://github.com/langfuse/langfuse/discussions/new?category=q-a
about: If you cant get something to work the way you expect, open a question in our discussion forums.
- name: 💡 Feature Request
url: https://github.com/langfuse/langfuse/discussions/new?category=ideas
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 cant get something to work the way you expect, open a question in our discussion forums.
+5 -18
View File
@@ -33,10 +33,6 @@ jobs:
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 }}
@@ -54,11 +50,6 @@ jobs:
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
@@ -66,6 +57,11 @@ jobs:
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
@@ -147,10 +143,6 @@ jobs:
environment: "protected branches"
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 }}
permissions:
@@ -193,8 +185,3 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
DATABASE_URL=${{ env.DATABASE_URL }}
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
NEXTAUTH_URL=${{ env.NEXTAUTH_URL }}
SALT=${{ env.SALT }}
+5 -1
View File
@@ -48,4 +48,8 @@ yarn-error.log*
/generated/typescript-server
# openapi spec that is copied during build
/public/openapi*.yml
/public/openapi*.yml
# vscode
.devcontainer
+12 -4
View File
@@ -2,22 +2,30 @@
First off, thanks for taking the time to contribute! ❤️
Langfuse is an open-source observability and analytics solution for LLM-based applications. We welcome contributions through GitHub pull requests. This document outlines our conventions regarding development workflow, commit message formatting, contact points, and other resources. Our goal is to simplify the process and ensure that your contributions are easily accepted.
The best ways to contribute to Langfuse:
We gratefully welcome improvements to documentation as well as to code.
- Submit and vote on [Ideas](https://github.com/orgs/langfuse/discussions/categories/ideas)
- Create and comment on [Issues](https://github.com/langfuse/langfuse/issues)
- Open a PR.
We welcome contributions through GitHub pull requests. This document outlines our conventions regarding development workflow, commit message formatting, contact points, and other resources. Our goal is to simplify the process and ensure that your contributions are easily accepted.
We gratefully welcome improvements to documentation ([docs repo](https://github.com/langfuse/langfuse-docs)), the core application (this repo) and the SDKs ([Python](https://github.com/langfuse/langfuse-python), [JS](https://github.com/langfuse/langfuse-js)).
The maintainers are available on [Discord](https://langfuse.com/discord) in case you have any questions.
> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
> And if you like the project, but just don't have time to contribute code, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
>
> - Star the project;
> - Tweet about it;
> - Refer to this project in your project's readme;
> - Submit and vote on [Ideas](https://github.com/orgs/langfuse/discussions/categories/ideas);
> - Create and comment on [Issues](https://github.com/langfuse/langfuse/issues);
> - Mention the project at local meetups and tell your friends/colleagues.
## Making a change
_Before making any significant changes, please [open an issue](https://github.com/langfuse/langfuse/issues)._ Discussing your proposed changes ahead of time will make the contribution process smooth for everyone.
_Before making any significant changes, please [open an issue](https://github.com/langfuse/langfuse/issues)._ Discussing your proposed changes ahead of time will make the contribution process smooth for everyone. Large changes that were not discussed in an issue may be rejected.
Once we've discussed your changes and you've got your code ready, make sure that tests are passing and open your pull request.
+5 -16
View File
@@ -1,19 +1,11 @@
# Base image
FROM node:20-alpine AS base
ARG DATABASE_URL
ARG NEXTAUTH_SECRET
ARG NEXTAUTH_URL
ARG SALT
# It's important to update the index before installing packages to ensure you're getting the latest versions.
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk update && apk upgrade --no-cache libcrypto3 libssl3 libc6-compat
FROM base AS deps
ARG DATABASE_URL
ARG NEXTAUTH_SECRET
ARG NEXTAUTH_URL
ARG SALT
WORKDIR /app
@@ -29,10 +21,6 @@ RUN \
# Rebuild the source code only when needed
FROM base AS builder
ARG DATABASE_URL
ARG NEXTAUTH_SECRET
ARG NEXTAUTH_URL
ARG SALT
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
@@ -46,6 +34,9 @@ RUN rm -f ./src/middleware.ts
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1
# Disable validation of environment variables during build
ENV DOCKER_BUILD 1
# Generate prisma client
RUN npx prisma generate
@@ -54,10 +45,6 @@ RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
ARG DATABASE_URL
ARG NEXTAUTH_SECRET
ARG NEXTAUTH_URL
ARG SALT
RUN apk add --no-cache dumb-init
@@ -66,6 +53,8 @@ WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED 1
# Needed to re-enable validation of environment variables during runtime
ENV DOCKER_BUILD 0
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
+90 -165
View File
@@ -2,42 +2,43 @@
<a href="https://langfuse.com">
<h1>🪢 Langfuse</h1>
</a>
<div>
<h3> <a href="https://cloud.langfuse.com">
<strong>Sign up</strong>
</a> ·
<a href="https://langfuse.com/docs/deployment/self-host">
<strong>Self Host Langfuse</strong>
</a> ·
<a href="https://langfuse.com/demo">
<strong>Demo Project (live data)</strong>
</a>
</h3>
<h3>
Open source observability & analytics for LLM-based applications
Langfuse is the open source LLM engineering platform.
</h3>
<div>
<strong>Observability:</strong> Explore and debug complex logs & traces in a visual UI
</div>
<div>
<strong>Analytics:</strong> Measure & improve costs, latency and response quality
<div>
Debug, analyze and iterate - together
</div>
</br>
<div>
<a href="https://discord.gg/7NXusRtqYU">
<strong>Join the Langfuse Discord »</strong>
</a>
</br>
<a href="https://langfuse.com">
<strong>langfuse.com</strong>
</a> ·
<a href="https://langfuse.com/docs">
<strong>Docs</strong>
</a> ·
<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+">
<a href="https://langfuse.com/issue">
<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://langfuse.com/idea">
<strong>Feature Request</strong>
</a>
</a> ·
<a href="https://langfuse.com/changelog">
<strong>Changelog</strong>
</a> ·
<a href="https://langfuse.com/discord">
<strong>Discord</strong>
</a>
</div>
</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>
<a href="https://github.com/langfuse/langfuse"><img src="https://img.shields.io/github/stars/langfuse/langfuse?style=flat-square&logo=GitHub&label=langfuse%2Flangfuse" alt="Github Repo Stars"></a>
<a href="https://github.com/langfuse/langfuse/releases"><img src="https://img.shields.io/github/v/release/langfuse/langfuse?include_prereleases&style=flat-square" alt="langfuse releases"></a>
<a href="https://github.com/langfuse/langfuse/actions/workflows/pipeline.yml?query=branch:main"><img src="https://img.shields.io/github/actions/workflow/status/langfuse/langfuse/pipeline.yml?style=flat-square&label=All%20tests" alt="CI test status"></a>
<a href="https://status.langfuse.com"><img src="https://uptime.betterstack.com/status-badges/v1/monitor/udlc.svg" alt="Uptime Status"/></a>
<a href="https://www.ycombinator.com/companies/langfuse"><img src="https://img.shields.io/badge/Y%20Combinator-W23-orange?style=flat-square" alt="Y Combinator W23"></a>
<a href="https://github.com/langfuse/langfuse/pkgs/container/langfuse"><img alt="Docker Image" src="https://img.shields.io/badge/docker-langfuse-blue?logo=Docker&logoColor=white&style=flat-square"></a>
<a href="https://www.npmjs.com/package/langfuse"><img src="https://img.shields.io/npm/v/langfuse?style=flat-square&label=npm+langfuse" alt="langfuse npm package"></a>
@@ -45,61 +46,44 @@
</div>
</div>
</br>
</div>
</br>
## What is Langfuse?
## Overview
Langfuse is an open source observability & analytics solution for LLM-based applications. It is mostly geared towards production usage but some users also use it for local development of their LLM applications.
### Develop
Langfuse is focused on applications built on top of LLMs. Many new abstractions and common best practices evolved recently, e.g. agents, chained prompts, embedding-based retrieval, LLM access to REPLs & APIs. These make applications more powerful but also unpredictable for developers as they cannot fully anticipate how changes impact the quality, cost and overall latency of their application. Thus Langfuse helps to monitor and debug these applications.
- **Observability:** Instrument your app and start ingesting traces to Langfuse ([Quickstart](https://langfuse.com/docs/get-started), [Integrations](https://langfuse.com/docs/integrations) [Tracing](https://langfuse.com/docs/tracing))
- **Langfuse UI:** Inspect and debug complex logs ([Demo](https://langfuse.com/docs/demo), [Tracing](https://langfuse.com/docs/tracing))
- **Prompts:** Manage, version and deploy prompts from within Langfuse ([Prompt Management](https://langfuse.com/docs/prompts))
**Demo (2 min)**
### Monitor
- **Analytics:** Track metrics (cost, latency, quality) and gain insights from dashboards & data exports ([Analytics](https://langfuse.com/docs/analytics))
- **Evals:** Collect and calculate scores for your LLM completions ([Scores & Evaluations](https://langfuse.com/docs/scores))
- Run model-based evaluations ([Model-based evaluations](https://langfuse.com/docs/scores/model-based-evals))
- Collect user feedback ([User Feedback](https://langfuse.com/docs/scores/user-feedback))
- Manually score observations in Langfuse ([Manual Scores](https://langfuse.com/docs/scores/manually))
### Test
- **Experiments:** Track and test app behaviour before deploying a new version
- Datasets let you test expected in and output pairs and benchmark performance before deployiong ([Datasets](https://langfuse.com/docs/datasets))
- Track versions and releases in your application ([Experimentation](https://langfuse.com/docs/experimentation), [Prompt Management](https://langfuse.com/docs/prompts))
### Video: Langfuse in two minutes
https://github.com/langfuse/langfuse/assets/2834609/6041347a-b517-4a11-8737-93ef8f8af49f
_Muted by default, enable sound for voice-over_
Explore demo project in Langfuse here (free account required): https://langfuse.com/demo
### Observability
Langfuse offers an admin UI to explore the ingested data.
- Nested view of LLM app executions; detailed information along the traces on: latency, cost, scores
- Segment execution traces by user feedback, to e.g. identify production issues
### Analytics
Reporting on
- Token usage by model
- Volume of traces
- Scores/evals
Broken down by
- Users
- Releases
- Prompt/chain versions
- Prompt/chain types
- Time
→ Expect releases with more ways to analyze the data over the next weeks.
## Get started
### Step 1: Run Server
### Langfuse Cloud
#### Langfuse Cloud
Managed deployment by the Langfuse team, generous free-tier (hobby plan), no credit card required.
Managed deployment by the Langfuse team, generous free-tier (hobby plan) available, no credit card required.
**[» Langfuse Cloud](https://cloud.langfuse.com)**
Links: [Create account](https://cloud.langfuse.com), [learn more](https://cloud.langfuse.com)
#### Localhost
Requirements: docker, docker compose (e.g. using Docker Desktop)
### Localhost (docker)
```bash
# Clone repository
@@ -110,143 +94,84 @@ cd langfuse
docker compose up -d
```
#### Self-host (Docker)
[→ Learn more about deploying locally](https://langfuse.com/docs/deployment/local)
[→ Instructions](https://langfuse.com/docs/deployment/self-host)
### Self-host (docker)
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/gmbqa_)
Langfuse is simple to self-host and keep updated. It currently requires only a single docker container.
[→ Self Hosting Instructions](https://langfuse.com/docs/deployment/self-host)
### Step 2: Data ingestion
Templated deployments: [Railway, GCP Cloud Run, AWS Fargate, Kubernetes and others](https://langfuse.com/docs/deployment/self-host#platform-specific-information)
#### SDKs to instrument application
## Get Started
Fully async, typed SDKs to instrument any LLM application. Currently available for Python & JS/TS.
### API Keys
[Guide](https://langfuse.com/docs/guides/sdk-integration) with an example of how the SDK can be used
You require a Langfuse public and secret key to get started. Sign up [here](https://cloud.langfuse.com) and find them in your project settings.
| Package | Description | Links |
| --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| [![PyPI Version](https://img.shields.io/pypi/v/langfuse.svg?style=flat-square&label=pypi+langfuse)](https://pypi.python.org/pypi/langfuse) | Python | [docs](https://langfuse.com/docs/integrations/sdk/python), [repo](https://github.com/langfuse/langfuse-python) |
| [![npm Version](https://img.shields.io/npm/v/langfuse?style=flat-square&label=npm+langfuse)](https://www.npmjs.com/package/langfuse) | JS/TS: Node >= 18, Edge runtimes | [docs](https://langfuse.com/docs/integrations/sdk/typescript), [repo](https://github.com/langfuse/langfuse-js) |
| [![npm package](https://img.shields.io/npm/v/langfuse-node?style=flat-square&label=npm+langfuse-node)](https://www.npmjs.com/package/langfuse-node) | JS/TS: Node <18 | [docs](https://langfuse.com/docs/integrations/sdk/typescript), [repo](https://github.com/langfuse/langfuse-js) |
### Ingesting Data · Instrumenting Your Application
#### Langchain applications
Note: We recommend using our fully async, typed [SDKs](https://langfuse.com/docs/sdk) that allow you to instrument any LLM application with any underlying model. They are available in [Python](https://langfuse.com/docs/sdk/python) & [JS/TS](https://langfuse.com/docs/sdk/typescript). The SDKs will always be the most fully featured and stable way to ingest data into Langfuse.
The Langfuse callback handler automatically instruments Langchain applications. Currently available for Python and JS/TS.
You may want to use another integration to get started quickly or implement a use case that we do not yet support. However, we recommend to migrate to the Langfuse SDKs over time to ensure performance and stability.
**Python**
See our the [→ Quickstart](https://langfuse.com/docs/get-started) to get started in integrating Langfuse.
```shell
pip install langfuse
```
### Integrations
```python
# Initialize Langfuse handler
from langfuse.callback import CallbackHandler
handler = CallbackHandler(PUBLIC_KEY, SECRET_KEY)
| Integration | Supports | Description |
| -------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------- |
| [**SDK** - _recommended_](https://langfuse.com/docs/sdk) | Python, JS/TS | Manual instrumentation using the SDKs for full flexibility. |
| [OpenAI](https://langfuse.com/docs/openai) | Python | Automated instrumentation using drop-in replacement of OpenAI SDK. |
| [Langchain](https://langfuse.com/docs/langchain) | Python, JS/TS | Automated instrumentation by passing callback handler to Langchain application. |
| [API](https://langfuse.com/docs/api) | | Directly call the public API. OpenAPI spec available. |
# Setup Langchain
from langchain.chains import LLMChain
...
chain = LLMChain(llm=llm, prompt=prompt)
External projects/packages that integrate with Langfuse:
# Add Langfuse handler as callback
chain.run(input="<user_input", callbacks=[handler])
```
| Name | Description |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [LiteLLM](/https://langfuse.comdocs/litellm) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
| [Flowise](https://langfuse.com/docs/flowise) | JS/TS no-code builder for customized LLM flows. |
| [Langflow](https://langfuse.com/docs/langflow) | Python-based UI for LangChain, designed with react-flow to provide an effortless way to experiment and prototype flows. |
→ [Langchain integration docs for Python](https://langfuse.com/docs/integrations/langchain/python)
## Questions and feedback
**JS/TS**
### Ideas and roadmap
[Langchain integration docs for JS/TS](https://langfuse.com/docs/integrations/langchain/typescript)
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions)
- [Feature Requests](https://langfuse.com/idea)
#### Add scores/evaluations to traces (optional)
### Support and feedback
Quality/evaluation of traces is tracked via scores ([docs](https://langfuse.com/docs/scores)). Scores are related to traces and optionally to observations. Scores can be added via:
In order of preference the best way to communicate with us:
- **Backend SDKs** (see docs above): `{trace, event, span, generation}.score()`
- **API** (see docs below): `POST /api/public/scores`
- **Client-side using Web SDK**, e.g. to capture user feedback or other user-based quality metrics:
```sh
npm install langfuse
```
```ts
// Client-side (browser)
import { LangfuseWeb } from "langfuse";
const langfuseWeb = new LangfuseWeb({
publicKey: process.env.LANGFUSE_PUBLIC_KEY,
});
// frontend handler (example: React)
export function UserFeedbackComponent(props: { traceId: string }) {
const handleUserFeedback = async (value: number) => {
await langfuseWeb.score({
traceId: props.traceId,
name: "user_feedback",
value,
});
};
return (
<div>
<button onClick={() => handleUserFeedback(1)}>👍</button>
<button onClick={() => handleUserFeedback(-1)}>👎</button>
</div>
);
}
```
#### API
[**Api reference**](https://langfuse.com/docs/integrations/api)
- POST/PATCH routes to ingest data
- GET routes to use data in downstream applications (e.g. embedded analytics)
## Questions / Feedback
The maintainers are very active in the Langfuse [Discord](https://langfuse.com/discord) and are happy to answer questions or discuss feedback/ideas regarding the future of the project.
- [GitHub Discussions](https://github.com/orgs/langfuse/discussions): Contribute [ideas](https://langfuse.com/idea) [support requests](https://github.com/orgs/langfuse/discussions/categories/support) and [report bugs](https://github.com/langfuse/langfuse/issues/new?labels=%F0%9F%90%9E%E2%9D%94+unconfirmed+bug&projects=&template=bug_report.yml&title=bug%3A+) (preferred as we create a permanent, indexed artifact for other community members)
- [Discord](https://langfuse.com/discord): For community support and to chat directly with maintainers
- Privately: Email contact at langfuse dot com
## Contributing to Langfuse
Join the community [on Discord](https://discord.gg/7NXusRtqYU).
To contribute, send us a PR, raise a GitHub issue, or email at contributing@langfuse.com
### Development setup
See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to setup a development environment.
- Vote on [Ideas](https://github.com/orgs/langfuse/discussions/categories/ideas)
- Raise and comment on [Issues](https://github.com/langfuse/langfuse/issues)
- Open a PR - see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to setup a development environment.
## License
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
This repository is MIT licensed, except for the `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
## Misc
### Upgrade Langfuse (localhost)
### GET API to export your data
```bash
# Stop server and db
docker compose down
[**GET routes**](https://langfuse.com/docs/integrations/api) to use data in downstream applications (e.g. embedded analytics).
# Pull latest changes
git pull
docker-compose pull
### Security & Privacy
# Run server and db
docker compose up -d
```
### Run Langfuse in CI for integration tests
Checkout GitHub Actions workflows of [Python SDK](https://github.com/langfuse/langfuse-python/blob/main/.github/workflows/ci.yml) and [JS/TS SDK](https://github.com/langfuse/langfuse-js/blob/main/.github/workflows/ci.yml).
We take data security and privacy seriously. Please refer to our [Security and Privacy](https://langfuse.com/security) page for more information.
### Telemetry
By default, Langfuse automatically reports basic usage statistics to a centralized server (PostHog).
By default, Langfuse automatically reports basic usage statistics of self-hosted instances to a centralized server (PostHog).
This helps us to:
-6
View File
@@ -4,17 +4,11 @@ services:
langfuse-server:
build:
dockerfile: Dockerfile
args:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
- NEXTAUTH_URL=http://localhost:3000
depends_on:
- db
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
-1
View File
@@ -8,7 +8,6 @@ services:
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postgres:postgres@db:5432/postgres
- NEXTAUTH_SECRET=mysecret
- SALT=mysalt
+1
View File
@@ -5,6 +5,7 @@ docs: |
- username: Langfuse Public Key
- password: Langfuse Secret Key
error-discrimination:
strategy: status-code
auth: basic
+20 -1
View File
@@ -30,7 +30,7 @@ types:
TraceWithFullDetails:
extends: Trace
properties:
observations: list<Observation>
observations: list<ObservationsView>
scores: list<Score>
Session:
properties:
@@ -61,12 +61,27 @@ types:
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
@@ -117,6 +132,9 @@ types:
enum:
- CHARACTERS
- TOKENS
- MILLISECONDS
- SECONDS
- IMAGES
ObservationLevel:
enum:
- DEBUG
@@ -129,6 +147,7 @@ types:
- optional<string>
- optional<integer>
- optional<boolean>
- optional<list<string>>
DatasetStatus:
enum:
- ACTIVE
+7 -2
View File
@@ -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
+6
View File
@@ -26,6 +26,9 @@ 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
@@ -37,3 +40,6 @@ types:
properties:
data: list<commons.TraceWithDetails>
meta: pagination.MetaResponse
Sort:
properties:
id: string
+1 -1
View File
@@ -1,4 +1,4 @@
{
"organization": "finto",
"version": "0.16.22"
"version": "0.16.36"
}
+84 -3
View File
@@ -408,7 +408,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/Observation'
$ref: '#/components/schemas/ObservationsView'
'400':
description: ''
content:
@@ -490,7 +490,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/Observations'
$ref: '#/components/schemas/ObservationsViews'
'400':
description: ''
content:
@@ -884,6 +884,14 @@ paths:
schema:
type: string
nullable: true
- name: orderBy
in: query
description: >-
Format of the string sort_by=timestamp.asc (id, timestamp, name,
userId, release, version, public, bookmarked, sessionId)
required: true
schema:
type: string
- name: tags
in: query
description: Only traces that include all of these tags will be returned.
@@ -997,7 +1005,7 @@ components:
observations:
type: array
items:
$ref: '#/components/schemas/Observation'
$ref: '#/components/schemas/ObservationsView'
scores:
type: array
items:
@@ -1095,6 +1103,39 @@ components:
- type
- startTime
- level
ObservationsView:
title: ObservationsView
type: object
properties:
modelId:
type: string
nullable: true
inputPrice:
type: number
format: double
nullable: true
outputPrice:
type: number
format: double
nullable: true
totalPrice:
type: number
format: double
nullable: true
calculatedInputCost:
type: number
format: double
nullable: true
calculatedOutputCost:
type: number
format: double
nullable: true
calculatedTotalCost:
type: number
format: double
nullable: true
allOf:
- $ref: '#/components/schemas/Observation'
Usage:
title: Usage
type: object
@@ -1111,6 +1152,18 @@ components:
unit:
$ref: '#/components/schemas/ModelUsageUnit'
nullable: true
inputCost:
type: number
format: double
nullable: true
outputCost:
type: number
format: double
nullable: true
totalCost:
type: number
format: double
nullable: true
Score:
title: Score
type: object
@@ -1258,6 +1311,9 @@ components:
enum:
- CHARACTERS
- TOKENS
- MILLISECONDS
- SECONDS
- IMAGES
ObservationLevel:
title: ObservationLevel
type: string
@@ -1275,6 +1331,10 @@ components:
nullable: true
- type: boolean
nullable: true
- type: array
items:
type: string
nullable: true
DatasetStatus:
title: DatasetStatus
type: string
@@ -1889,6 +1949,19 @@ components:
required:
- data
- meta
ObservationsViews:
title: ObservationsViews
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/ObservationsView'
meta:
$ref: '#/components/schemas/utilsMetaResponse'
required:
- data
- meta
Projects:
title: Projects
type: object
@@ -1988,6 +2061,14 @@ components:
required:
- data
- meta
Sort:
title: Sort
type: object
properties:
id:
type: string
required:
- id
utilsMetaResponse:
title: utilsMetaResponse
type: object
+6 -1
View File
@@ -708,7 +708,7 @@
"request": {
"description": "Get list of traces",
"url": {
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&tags=",
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&orderBy=&tags=",
"host": [
"{{baseUrl}}"
],
@@ -738,6 +738,11 @@
"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": "",
+1909 -1792
View File
File diff suppressed because it is too large Load Diff
+53 -48
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse-core",
"version": "1.32.2",
"version": "2.4.2",
"private": true,
"scripts": {
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
@@ -16,24 +16,28 @@
"test": "jest --runInBand",
"test:watch": "jest --watch --runInBand",
"test:e2e": "playwright test",
"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",
"@aws-sdk/client-s3": "^3.485.0",
"@aws-sdk/s3-request-presigner": "^3.485.0",
"@headlessui/react": "^1.7.17",
"@aws-sdk/client-s3": "^3.507.0",
"@aws-sdk/lib-storage": "^3.511.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.7.1",
"@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",
@@ -54,87 +58,88 @@
"@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.92.0",
"@sentry/profiling-node": "^1.3.2",
"@react-email/components": "^0.0.14",
"@react-email/render": "^0.0.12",
"@sentry/nextjs": "^7.100.1",
"@sentry/profiling-node": "^7.100.1",
"@sentry/types": "^7.88.0",
"@t3-oss/env-nextjs": "^0.7.1",
"@t3-oss/env-nextjs": "^0.8.0",
"@tailwindcss/forms": "^0.5.7",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.11.3",
"@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",
"@vercel/edge-config": "^0.4.1",
"@trpc/client": "^10.45.0",
"@trpc/next": "^10.45.0",
"@trpc/react-query": "^10.45.0",
"@trpc/server": "^10.45.0",
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"core-js": "^3.35.0",
"clsx": "^2.1.0",
"cmdk": "^0.2.1",
"core-js": "^3.35.1",
"cors": "^2.8.5",
"date-fns": "^3.1.0",
"date-fns": "^3.3.1",
"decimal.js": "^10.4.3",
"exponential-backoff": "^3.1.1",
"js-tiktoken": "^1.0.10",
"lodash": "^4.17.21",
"lucide-react": "^0.306.0",
"next": "^14.0.4",
"lucide-react": "^0.330.0",
"next": "^14.1.0",
"next-auth": "^4.24.5",
"next-query-params": "^5.0.0",
"nodemailer": "^6.9.8",
"posthog-js": "^1.96.1",
"posthog-node": "^3.2.1",
"nodemailer": "^6.9.9",
"posthog-js": "^1.105.7",
"posthog-node": "^3.6.2",
"react": "18.2.0",
"react-day-picker": "^8.10.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.49.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",
"react18-json-view": "^0.2.7",
"sonner": "^1.4.0",
"superjson": "2.2.1",
"tailwind-merge": "^2.2.0",
"tailwind-merge": "^2.2.1",
"tailwindcss-animate": "^1.0.7",
"tiktoken": "^1.0.11",
"use-query-params": "^2.2.1",
"uuid": "^9.0.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@jedmao/location": "^3.0.0",
"@mermaid-js/mermaid-cli": "^10.6.1",
"@playwright/test": "^1.40.1",
"@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/react": "^14.1.2",
"@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.56.1",
"@types/jest": "^29.5.11",
"@types/eslint": "^8.56.2",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.14.202",
"@types/node": "20.10.5",
"@types/nodemailer": "^6.4.14",
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
"@types/uuid": "^9.0.7",
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.16.0",
"autoprefixer": "^10.4.16",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"autoprefixer": "^10.4.17",
"dotenv-cli": "^7.3.0",
"eslint": "^8.56.0",
"eslint-config-next": "^14.0.4",
"eslint-config-next": "^14.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.4.33",
"prettier": "^3.1.1",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.11",
"prisma": "^5.7.1",
"prisma": "^5.9.1",
"prisma-erd-generator": "^1.11.2",
"release-it": "^17.0.1",
"release-it": "^17.0.3",
"tailwindcss": "^3.4.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.7.1",
"typescript": "^5.3.3"
},
"ct3aMetadata": {
@@ -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" }')
@@ -0,0 +1,28 @@
-- CreateTable
CREATE TABLE "audit_logs" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"user_id" TEXT NOT NULL,
"project_id" TEXT NOT NULL,
"user_project_role" "MembershipRole" NOT NULL,
"resource_type" TEXT NOT NULL,
"resource_id" TEXT NOT NULL,
"action" TEXT NOT NULL,
"before" TEXT,
"after" TEXT,
CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "audit_logs_project_id_idx" ON "audit_logs"("project_id");
-- CreateIndex
CREATE INDEX "audit_logs_created_at_idx" ON "audit_logs"("created_at");
-- AddForeignKey
ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,25 @@
-- This is an empty migration.
DELETE FROM models
WHERE id in ('clruwnahl00040al78f1lb0at');
INSERT INTO models (
id,
project_id,
model_name,
match_pattern,
start_date,
input_price,
output_price,
total_price,
unit,
tokenizer_id,
tokenizer_config
)
VALUES
-- according to email, gpt-3.5-turbo and gpt-3.5-turbo-16k will point to 0125 models as of 2024-02-16
-- gpt-3.5-turbo-0125 now supports 16k token length. 16k model will point to regular 3.5 turbo model according to mail.
('clruwnahl00040al78f1lb0at', NULL, 'gpt-3.5-turbo', '(?i)^(gpt-)(35|3.5)(-turbo)$', '2024-02-16', 0.0000005, 0.0000015, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-3.5-turbo" }'),
('clsk9lntu000008jwfc51bbqv', NULL, 'gpt-3.5-turbo-16k', '(?i)^(gpt-)(35|3.5)(-turbo-16k)$', '2024-02-16', 0.0000005, 0.0000015, NULL, 'TOKENS', 'openai', '{ "tokensPerMessage": 3, "tokensPerName": 1, "tokenizerModel": "gpt-3.5-turbo-16k" }')
+99 -6
View File
@@ -3,7 +3,7 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["tracing"]
previewFeatures = ["tracing", "views"]
}
datasource db {
@@ -72,6 +72,7 @@ model User {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
featureFlags String[] @default([]) @map("feature_flags")
AuditLog AuditLog[]
@@map("users")
}
@@ -100,6 +101,8 @@ model Project {
invitations MembershipInvitation[]
sessions TraceSession[]
Prompt Prompt[]
Model Model[]
AuditLog AuditLog[]
@@map("projects")
}
@@ -162,7 +165,7 @@ enum MembershipRole {
}
model TraceSession {
id String @id @default(cuid())
id String @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
projectId String @map("project_id")
@@ -171,7 +174,7 @@ model TraceSession {
public Boolean @default(false)
traces Trace[]
@@unique([id, projectId])
@@id([id, projectId])
@@index([projectId])
@@index([createdAt])
@@map("trace_sessions")
@@ -194,11 +197,10 @@ model Trace {
input Json?
output Json?
sessionId String? @map("session_id")
session TraceSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull)
session TraceSession? @relation(fields: [sessionId, projectId], references: [id, projectId])
scores Score[]
@@unique([projectId, externalId])
@@index([projectId])
@@index([sessionId])
@@index([name])
@@ -227,13 +229,17 @@ 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 @default("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)
@@ -421,3 +427,90 @@ model Prompt {
@@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")
}
model AuditLog {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
userId String @map("user_id")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
userProjectRole MembershipRole @map("user_project_role")
resourceType String @map("resource_type")
resourceId String @map("resource_id")
action String
before String? //stringified JSON
after String? // stringified JSON
@@index([projectId])
@@index([createdAt])
@@map("audit_logs")
}
+142 -11
View File
@@ -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" },
@@ -98,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: [
@@ -119,8 +120,120 @@ 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"];
@@ -143,6 +256,8 @@ async function main() {
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)}`,
@@ -155,9 +270,7 @@ async function main() {
},
tags: tags as string[],
project: {
connect: {
id: [project1.id, project2.id][i % 2],
},
connect: { id: projectId },
},
userId: `user-${i % 10}`,
session:
@@ -165,13 +278,14 @@ async function main() {
? {
connectOrCreate: {
where: {
id: `session-${i % 10}`,
id_projectId: {
id: `session-${i % 10}`,
projectId: projectId,
},
},
create: {
id: `session-${i % 10}`,
project: {
connect: { id: [project1.id, project2.id][i % 2] },
},
projectId: projectId,
},
},
}
@@ -268,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",
@@ -276,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
? [
@@ -334,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),
@@ -358,6 +488,7 @@ async function main() {
? { prompt: { connect: { id: prompt.id } } }
: {}),
},
unit: ModelUsageUnit.Tokens,
},
});
if (Math.random() > 0.6)
+250
View File
@@ -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`);
}
+1
View File
@@ -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");
});
+16 -7
View File
@@ -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";
+442
View File
@@ -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");
});
});
@@ -1,27 +1,8 @@
import { orderByToPrismaSql } from "@/src/features/orderBy/server/orderByToPrisma";
import { tracesTableCols } from "@/src/server/api/definitions/tracesTable";
import { Prisma } from "@prisma/client";
// The test for the orderByToPrisma function
describe("orderByToPrisma (Convert orderBy to Prisma.sql)", () => {
test("orderByToPrisma returns default sql when orderBy=null", () => {
expect(orderByToPrismaSql(null, tracesTableCols)).toStrictEqual(
Prisma.sql`ORDER BY t.timestamp DESC`,
);
});
test("orderByToPrisma returns correct clause for orderBy column included in column defs", () => {
expect(
orderByToPrismaSql(
{
column: "latency",
order: "ASC",
},
tracesTableCols,
),
).toStrictEqual(Prisma.sql`ORDER BY tl.latency ASC`);
});
test("orderByToPrisma throws error for orderBy column not included in column defs", () => {
expect(() =>
orderByToPrismaSql(
@@ -33,4 +14,16 @@ describe("orderByToPrisma (Convert orderBy to Prisma.sql)", () => {
),
).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,194 @@
/** @jest-environment node */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { pruneDatabase } from "@/src/__tests__/test-utils";
import { ModelUsageUnit } from "@/src/constants";
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("observations.export RPC", () => {
const numberOfGenerations = 5;
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
beforeAll(async () => {
// Disable S3 upload
process.env.S3_ENDPOINT = "";
await pruneDatabase();
const traceId = "trace-1";
await prisma.trace.create({
data: {
id: traceId,
name: "trace-name",
userId: "user-1",
projectId,
metadata: { key: "value" },
release: "1.0.0",
version: "2.0.0",
},
});
for (let i = 1; i <= numberOfGenerations; i++) {
await prisma.observation.create({
data: {
type: "GENERATION",
id: `generation-${i}`,
name: `generation-${i}`,
model: "gpt-3.5-turbo",
totalCost: 1,
startTime: new Date("2021-01-01T00:00:00.000Z"),
endTime: new Date("2021-01-01T00:00:05.000Z"),
project: { connect: { id: projectId } },
traceId,
input: [
{
role: "system",
content: "Be a helpful assistant",
},
{
role: "user",
content: "How can i create a React component?",
},
],
output: {
completion: `Creating a React component can be done in two ways.`,
},
metadata: {
user: `user-@langfuse.com`,
},
unit: ModelUsageUnit.Tokens,
},
});
}
});
afterAll(async () => await pruneDatabase());
const session: Session = {
expires: "1",
user: {
id: "clgb17vnp000008jjere5g15i",
name: "John Doe",
projects: [
{
id: projectId,
role: "ADMIN",
name: "test",
},
],
featureFlags: {
templateFlag: true,
},
admin: true,
},
};
const ctx = createInnerTRPCContext({ session });
const caller = appRouter.createCaller({ ...ctx, prisma });
it("should return a CSV file", async () => {
const result = await caller.generations.export({
fileFormat: "CSV",
orderBy: { column: "id", order: "ASC" },
filter: [
{
column: "start_time",
type: "datetime",
operator: ">",
value: new Date("1990-01-01"),
},
],
projectId,
searchQuery: null,
});
if (result.type !== "data")
throw new Error("No data returned. Is S3 accidentally enabled?");
const { data, fileName } = result;
const fileExtension = fileName.split(".").pop();
expect(fileName).toContain(`lf-export-${projectId}`);
expect(fileExtension).toBe("csv");
expect(data.split("\n").filter(Boolean).length).toBe(
numberOfGenerations + 1,
);
});
it("should return a JSON file", async () => {
const result = await caller.generations.export({
fileFormat: "JSON",
orderBy: { column: "id", order: "ASC" },
filter: [
{
column: "start_time",
type: "datetime",
operator: ">",
value: new Date("1990-01-01"),
},
],
projectId,
searchQuery: null,
});
if (result.type !== "data")
throw new Error("No data returned. Is S3 accidentally enabled?");
const { data, fileName } = result;
const fileExtension = fileName.split(".").pop();
expect(fileName).toContain(`lf-export-${projectId}`);
expect(fileExtension).toBe("json");
expect(JSON.parse(data).length).toBe(numberOfGenerations);
});
it("should return a OPENAI-JSONL file", async () => {
const result = await caller.generations.export({
fileFormat: "OPENAI-JSONL",
orderBy: { column: "id", order: "ASC" },
filter: [
{
column: "start_time",
type: "datetime",
operator: ">",
value: new Date("1990-01-01"),
},
],
projectId,
searchQuery: null,
});
if (result.type !== "data")
throw new Error("No data returned. Is S3 accidentally enabled?");
const { data, fileName } = result;
const fileExtension = fileName.split(".").pop();
expect(fileName).toContain(`lf-export-${projectId}`);
expect(fileExtension).toBe("jsonl");
expect(data.split("\n").filter(Boolean).length).toBe(numberOfGenerations);
});
it("should throw on unsupported file formats", async () => {
const unsupportedFileFormat = "XLSX";
const call = caller.generations.export({
fileFormat: unsupportedFileFormat as unknown as "JSON",
orderBy: { column: "id", order: "ASC" },
filter: [
{
column: "start_time",
type: "datetime",
operator: ">",
value: new Date("1990-01-01"),
},
],
projectId,
searchQuery: null,
});
await expect(call).rejects.toThrow();
});
});
+14 -12
View File
@@ -1,9 +1,11 @@
/** @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());
@@ -13,9 +15,9 @@ describe("/api/public/generations API Endpoint", () => {
input: 100,
output: 200,
total: 100,
unit: "CHARACTERS",
unit: ModelUsageUnit.Characters,
},
expectedUnit: "CHARACTERS",
expectedUnit: ModelUsageUnit.Characters,
expectedPromptTokens: 100,
expectedCompletionTokens: 200,
expectedTotalTokens: 100,
@@ -23,9 +25,9 @@ describe("/api/public/generations API Endpoint", () => {
{
usage: {
total: 100,
unit: "CHARACTERS",
unit: ModelUsageUnit.Characters,
},
expectedUnit: "CHARACTERS",
expectedUnit: ModelUsageUnit.Characters,
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 100,
@@ -34,7 +36,7 @@ describe("/api/public/generations API Endpoint", () => {
usage: {
total: 100,
},
expectedUnit: "TOKENS",
expectedUnit: null,
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 100,
@@ -48,7 +50,7 @@ describe("/api/public/generations API Endpoint", () => {
expectedPromptTokens: 100,
expectedCompletionTokens: 200,
expectedTotalTokens: 100,
expectedUnit: "TOKENS",
expectedUnit: ModelUsageUnit.Tokens,
},
{
usage: {
@@ -57,28 +59,28 @@ describe("/api/public/generations API Endpoint", () => {
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 100,
expectedUnit: "TOKENS",
expectedUnit: ModelUsageUnit.Tokens,
},
{
usage: undefined,
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 0,
expectedUnit: "TOKENS",
expectedUnit: null,
},
{
usage: null,
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 0,
expectedUnit: "TOKENS",
expectedUnit: null,
},
{
usage: {},
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 0,
expectedUnit: "TOKENS",
expectedUnit: null,
},
].forEach((testConfig) => {
it(`should create generation after trace 1 ${JSON.stringify(
+760 -18
View File
@@ -1,9 +1,11 @@
/** @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());
@@ -14,9 +16,12 @@ describe("/api/public/ingestion API Endpoint", () => {
input: 100,
output: 200,
total: 100,
unit: "CHARACTERS",
unit: ModelUsageUnit.Characters,
inputCost: 123,
outputCost: 456,
totalCost: 789,
},
expectedUnit: "CHARACTERS",
expectedUnit: ModelUsageUnit.Characters,
expectedPromptTokens: 100,
expectedCompletionTokens: 200,
expectedTotalTokens: 100,
@@ -24,9 +29,9 @@ describe("/api/public/ingestion API Endpoint", () => {
{
usage: {
total: 100,
unit: "CHARACTERS",
unit: ModelUsageUnit.Characters,
},
expectedUnit: "CHARACTERS",
expectedUnit: ModelUsageUnit.Characters,
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 100,
@@ -34,8 +39,40 @@ describe("/api/public/ingestion API Endpoint", () => {
{
usage: {
total: 100,
unit: ModelUsageUnit.Milliseconds,
},
expectedUnit: "TOKENS",
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,
@@ -49,7 +86,7 @@ describe("/api/public/ingestion API Endpoint", () => {
expectedPromptTokens: 100,
expectedCompletionTokens: 200,
expectedTotalTokens: 100,
expectedUnit: "TOKENS",
expectedUnit: ModelUsageUnit.Tokens,
},
{
usage: {
@@ -58,31 +95,31 @@ describe("/api/public/ingestion API Endpoint", () => {
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 100,
expectedUnit: "TOKENS",
expectedUnit: ModelUsageUnit.Tokens,
},
{
usage: undefined,
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 0,
expectedUnit: "TOKENS",
expectedUnit: null,
},
{
usage: null,
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 0,
expectedUnit: "TOKENS",
expectedUnit: null,
},
{
usage: {},
expectedPromptTokens: 0,
expectedCompletionTokens: 0,
expectedTotalTokens: 0,
expectedUnit: "TOKENS",
expectedUnit: null,
},
].forEach((testConfig) => {
it(`should create trace and generation ${JSON.stringify(
it(`should create trace, generation and score without matching models ${JSON.stringify(
testConfig,
)}`, async () => {
const traceId = v4();
@@ -208,6 +245,7 @@ describe("/api/public/ingestion API Endpoint", () => {
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,
);
@@ -248,6 +286,265 @@ describe("/api/public/ingestion API Endpoint", () => {
});
});
[
{
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();
@@ -300,6 +597,7 @@ describe("/api/public/ingestion API Endpoint", () => {
id: generationId,
traceId: traceId,
parentObservationId: spanId,
modelParameters: { someKey: ["user-1", "user-2"] },
},
},
{
@@ -376,6 +674,9 @@ describe("/api/public/ingestion API Endpoint", () => {
expect(dbGeneration?.traceId).toBe(traceId);
expect(dbGeneration?.name).toBe("generation-name");
expect(dbGeneration?.parentObservationId).toBe(spanId);
expect(dbGeneration?.modelParameters).toEqual({
someKey: ["user-1", "user-2"],
});
const dbEvent = await prisma.observation.findUnique({
where: {
@@ -494,10 +795,10 @@ describe("/api/public/ingestion API Endpoint", () => {
expect("errors" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body?.errors.length).toBe(1);
expect(responseOne.body.errors.length).toBe(1);
expect("successes" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body?.successes.length).toBe(1);
expect(responseOne.body.successes.length).toBe(1);
const dbTrace = await prisma.trace.findMany({
where: {
@@ -546,10 +847,10 @@ describe("/api/public/ingestion API Endpoint", () => {
expect("errors" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body?.errors.length).toBe(1);
expect(responseOne.body.errors.length).toBe(1);
expect("successes" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body?.successes.length).toBe(1);
expect(responseOne.body.successes.length).toBe(1);
const dbTrace = await prisma.trace.findMany({
where: {
@@ -596,10 +897,10 @@ describe("/api/public/ingestion API Endpoint", () => {
expect("errors" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body?.errors.length).toBe(1);
expect(responseOne.body.errors.length).toBe(1);
expect("successes" in responseOne.body).toBe(true);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(responseOne.body?.successes.length).toBe(1);
expect(responseOne.body.successes.length).toBe(1);
const dbTrace = await prisma.trace.findMany({
where: {
@@ -614,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: [
{
@@ -681,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: [
{
@@ -790,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" }],
@@ -1023,4 +1414,355 @@ IB Home / . . . / News / News about the IB / Why ChatGPT is an o
expect(cleanedEvent).toStrictEqual(expected);
});
});
it("should allow score ingestion via Basic auth", async () => {
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
const traceId = "trace_id";
const scoreId = "score_id";
const scoreEventId = "score_event_id";
const scoreName = "score-name";
const scoreValue = 100.5;
// Seed db with a trace to be scored
await prisma.trace.create({
data: {
id: traceId,
name: "trace-name",
project: { connect: { id: projectId } },
},
});
const response = await makeAPICall("POST", "/api/public/ingestion", {
batch: [
{
id: scoreEventId,
type: "score-create",
timestamp: new Date().toISOString(),
body: {
id: scoreId,
name: scoreName,
value: scoreValue,
traceId: traceId,
},
},
],
});
expect(response.status).toBe(207);
expect(response.body.successes.length).toBe(1);
expect(response.body.successes[0]?.id).toBe(scoreEventId);
expect(response.body.errors.length).toBe(0);
const dbScore = await prisma.score.findUnique({
where: {
id: scoreId,
},
});
expect(dbScore?.id).toBe(scoreId);
expect(dbScore?.traceId).toBe(traceId);
expect(dbScore?.name).toBe(scoreName);
expect(dbScore?.value).toBe(scoreValue);
});
it("should allow score ingestion via Bearer auth", async () => {
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
const traceId = "trace_id";
const bearerAuth = "Bearer pk-lf-1234567890";
const scoreId = "score_id";
const scoreEventId = "score_event_id";
const scoreName = "score-name";
const scoreValue = 100.5;
// Seed db with a trace to be scored
await prisma.trace.create({
data: {
id: traceId,
name: "trace-name",
project: { connect: { id: projectId } },
},
});
const response = await makeAPICall(
"POST",
"/api/public/ingestion",
{
batch: [
{
id: scoreEventId,
type: "score-create",
timestamp: new Date().toISOString(),
body: {
id: scoreId,
name: scoreName,
value: scoreValue,
traceId: traceId,
},
},
],
},
bearerAuth,
);
expect(response.status).toBe(207);
expect(response.body.successes.length).toEqual(1);
expect(response.body.successes[0]?.id).toBe(scoreEventId);
expect(response.body.errors.length).toBe(0);
const dbScore = await prisma.score.findUnique({
where: {
id: scoreId,
},
});
expect(dbScore?.id).toBe(scoreId);
expect(dbScore?.traceId).toBe(traceId);
expect(dbScore?.name).toBe(scoreName);
expect(dbScore?.value).toBe(scoreValue);
});
it("should throw an Auth error on Bearer Auth for all events that are NOT 'score-create'", async () => {
const projectId = "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a";
const traceId = "trace_id";
const bearerAuth = "Bearer pk-lf-1234567890";
const scoreId = "score_id";
const scoreEventId = "score_event_id";
const scoreName = "score-name";
const scoreValue = 100.5;
const generationId = v4();
const spanId = v4();
const anotherTraceId = "another_trace_id";
// Seed db with a trace to be scored
await prisma.trace.create({
data: {
id: traceId,
name: "trace-name",
project: { connect: { id: projectId } },
},
});
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: anotherTraceId,
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" },
},
},
{
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: scoreEventId,
type: "score-create",
timestamp: new Date().toISOString(),
body: {
id: scoreId,
name: "score-name",
value: 100.5,
traceId: traceId,
},
},
],
},
bearerAuth,
);
expect(response.status).toBe(207);
expect(response.body.successes.length).toEqual(1);
expect(response.body.successes[0]?.id).toEqual(scoreEventId);
expect(response.body.errors.length).toEqual(4);
const dbScore = await prisma.score.findUnique({
where: {
id: scoreId,
},
});
expect(await prisma.trace.count()).toBe(1);
expect(await prisma.trace.count({ where: { id: traceId } })).toBe(1);
expect(await prisma.observation.count()).toBe(0);
expect(dbScore?.id).toBe(scoreId);
expect(dbScore?.traceId).toBe(traceId);
expect(dbScore?.name).toBe(scoreName);
expect(dbScore?.value).toBe(scoreValue);
});
it("should error on Bearer Auth for a trace from different project", async () => {
const otherProjectId = "other_project_id";
const traceId = "trace_id";
const bearerAuth = "Bearer pk-lf-1234567890";
const scoreId = "score_id";
const scoreEventId = "score_event_id";
const scoreName = "score-name";
const scoreValue = 100.5;
// Seed db with a trace to be scored
try {
await prisma.project.create({
data: {
id: otherProjectId,
name: "another-project",
},
});
await prisma.trace.create({
data: {
id: traceId,
name: "trace-name",
project: { connect: { id: otherProjectId } },
},
});
const response = await makeAPICall(
"POST",
"/api/public/ingestion",
{
batch: [
{
id: scoreEventId,
type: "score-create",
timestamp: new Date().toISOString(),
body: {
id: scoreId,
name: scoreName,
value: scoreValue,
traceId: traceId,
},
},
],
},
bearerAuth,
);
expect(response.status).toBe(207);
expect(response.body.successes.length).toBe(0);
expect(response.body).toHaveProperty("errors");
expect(response.body.errors.length).toEqual(1);
expect(response.body.errors[0]?.id).toEqual(scoreEventId);
expect(await prisma.trace.count()).toBe(1);
expect(await prisma.trace.count({ where: { id: traceId } })).toBe(1);
const dbScore = await prisma.score.findUnique({
where: {
id: scoreId,
},
});
expect(dbScore).toBeNull();
} finally {
await prisma.project.delete({ where: { id: otherProjectId } });
}
});
it("should error on Bearer Auth with a trace that does not exist", async () => {
const traceId = "trace_id";
const bearerAuth = "Bearer pk-lf-1234567890";
const scoreId = "score_id";
const scoreEventId = "score_event_id";
const scoreName = "score-name";
const scoreValue = 100.5;
const response = await makeAPICall(
"POST",
"/api/public/ingestion",
{
batch: [
{
id: scoreEventId,
type: "score-create",
timestamp: new Date().toISOString(),
body: {
id: scoreId,
name: scoreName,
value: scoreValue,
traceId: traceId,
},
},
],
},
bearerAuth,
);
expect(response.status).toBe(207);
expect(response.body.successes.length).toBe(0);
expect(response.body.errors.length).toBe(1);
expect(response.body.errors[0]?.id).toBe(scoreEventId);
expect(await prisma.trace.count()).toBe(0);
const dbScore = await prisma.score.findUnique({
where: {
id: scoreId,
},
});
expect(dbScore).toBeNull();
});
});
+100
View File
@@ -0,0 +1,100 @@
/** @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);
});
// temporary fix: wait for 5 additional seconds to ensure that the model match is complete
// had issue with the test failing because the model match was not complete and logged to console
await new Promise((resolve) => setTimeout(resolve, 5000));
}, 10000);
});
+70 -10
View File
@@ -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[];
};
-2
View File
@@ -255,8 +255,6 @@ describe("/api/public/prompts API Endpoint", () => {
expect(response.status).toBe(207);
console.log("response body", response.body);
const dbGeneration = await prisma.observation.findUnique({
where: {
id: generationId,
+2 -2
View File
@@ -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",
+23 -5
View File
@@ -14,6 +14,7 @@ export const pruneDatabase = async () => {
await prisma.datasetRuns.deleteMany();
await prisma.prompt.deleteMany();
await prisma.events.deleteMany();
await prisma.model.deleteMany();
};
export function createBasicAuthHeader(
@@ -26,21 +27,38 @@ export function createBasicAuthHeader(
return `Basic ${base64Credentials}`;
}
export type IngestionAPIResponse = {
errors: ErrorIngestion[];
successes: SuccessfulIngestion[];
};
export type SuccessfulIngestion = {
id: string;
status: number;
};
export type ErrorIngestion = {
id: string;
status: number;
message: string;
error: string;
};
export async function makeAPICall(
method: "POST" | "GET" | "PUT" | "DELETE" | "PATCH",
url: string,
body?: unknown,
auth?: string,
) {
const finalUrl = `http://localhost:3000/${url}`;
const authorization =
auth || createBasicAuthHeader("pk-lf-1234567890", "sk-lf-1234567890");
const options = {
method: method,
headers: {
Accept: "application/json",
"Content-Type": "application/json;charset=UTF-8",
Authorization: createBasicAuthHeader(
"pk-lf-1234567890",
"sk-lf-1234567890",
),
Authorization: authorization,
},
// Conditionally include the body property if the method is not "GET"
...(method !== "GET" &&
@@ -49,7 +67,7 @@ export async function makeAPICall(
const a = await fetch(finalUrl, options);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
return { body: await a.json(), status: a.status };
return { body: (await a.json()) as IngestionAPIResponse, status: a.status };
}
export const setupUserAndProject = async () => {
+94 -25
View File
@@ -1,20 +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-35", 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 },
{ model: "gpt-3.5-turbo-1106", tokens: 114 },
{ model: "gpt-35-turbo-1106", tokens: 114 },
].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();
@@ -24,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);
@@ -46,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();
@@ -55,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();
@@ -71,14 +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: "gpt-35-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?" },
@@ -95,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);
@@ -103,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();
@@ -111,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);
+46 -20
View File
@@ -1,4 +1,7 @@
import { VERSION } from "@/src/constants/VERSION";
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 = ({
@@ -22,31 +25,54 @@ 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",
)}
>
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"
<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",
)}
>
{VERSION}
</a>
<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>
)}
</div>
</div>
);
+35 -21
View File
@@ -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>
);
}
+3 -6
View File
@@ -30,12 +30,9 @@ export function DeleteTrace({
const mutDeleteTraces = api.traces.deleteMany.useMutation({
onSuccess: () => {
setIsDeleted(true);
void utils.traces.invalidate();
if (!isTableAction) {
void router
.push(`/project/${projectId}/traces`)
.then(() => utils.traces.invalidate());
} else {
void utils.traces.invalidate();
void router.push(`/project/${projectId}/traces`);
}
},
});
@@ -52,7 +49,7 @@ export function DeleteTrace({
<TrashIcon className="h-4 w-4" />
</Button>
) : (
<Button variant="outline" type="button" size="sm">
<Button variant="outline" type="button" size="icon">
<TrashIcon className="h-4 w-4" />
</Button>
)}
+30 -11
View File
@@ -3,22 +3,25 @@ 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",
@@ -35,14 +38,30 @@ export default function DocPopup({
}}
>
<HoverCardTrigger className="mx-1 cursor-pointer" asChild>
<Link
href={href}
rel="noopener"
target="_blank"
className="inline-block whitespace-nowrap text-gray-500 sm:pl-0"
>
<HelpCircle className={sizes[size]} />
</Link>
{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>
)}
</HoverCardTrigger>
<HoverCardContent>
{typeof description === "string" ? (
+2 -2
View File
@@ -13,7 +13,7 @@ export default function Header({
title: string;
breadcrumb?: { name: string; href?: string }[];
status?: Status;
help?: { description: string; href: string };
help?: { description: string; href?: string };
actionButtons?: React.ReactNode;
level?: "h2" | "h3";
}) {
@@ -40,7 +40,7 @@ export default function Header({
[...props.breadcrumb.map((i) => i.href).filter(Boolean)].pop();
return (
<div className={cn(level === "h2" ? "mb-8" : "mb-1")}>
<div className={cn(level === "h2" ? "mb-4" : "mb-1")}>
<div>
{backHref ? (
<nav className="sm:hidden" aria-label="Back">
+165 -122
View File
@@ -8,7 +8,6 @@ import { useRouter } from "next/router";
import clsx from "clsx";
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";
import {
Avatar,
@@ -24,6 +23,13 @@ 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";
import { Toaster } from "@/src/components/ui/sonner";
import {
NOTIFICATIONS,
useCheckNotification,
} from "@/src/features/notifications/checkNotifications";
import { ChevronDownIcon } from "@heroicons/react/20/solid";
import useLocalStorage from "@/src/components/useLocalStorage";
const userNavigation = [
{
@@ -46,6 +52,9 @@ export default function Layout(props: PropsWithChildren) {
const [sidebarOpen, setSidebarOpen] = useState(false);
const router = useRouter();
const session = useSession();
useCheckNotification(NOTIFICATIONS, session.status === "authenticated");
const enableExperimentalFeatures =
api.environment.enableExperimentalFeatures.useQuery().data ?? false;
@@ -141,7 +150,6 @@ export default function Layout(props: PropsWithChildren) {
{props.children}
</main>
);
return (
<>
<Head>
@@ -221,7 +229,13 @@ 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 version 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">
<MainNavigation nav={navigation} />
@@ -287,7 +301,14 @@ 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 version 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">
<MainNavigation nav={navigation} />
@@ -409,7 +430,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 version 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>
@@ -467,26 +492,27 @@ export default function Layout(props: PropsWithChildren) {
<Info className="h-4 w-4" />
<span className="font-semibold">DEMO (view-only)</span>
</div>
<div>Live data from the Langfuse Q&A Chatbot.</div>
<div>Use demo RAG chat to see live data in this project.</div>
</div>
<Button size="sm" asChild className="ml-2">
<Link
href={
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
? "https://langfuse.com/docs/qa-chatbot"
: "https://docs-staging.langfuse.com/docs/qa-chatbot"
? "https://langfuse.com/docs/demo"
: "https://docs-staging.langfuse.com/docs/demo"
}
target="_blank"
>
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU"
? "Q&A Chatbot ↗"
: "Q&A Chatbot (staging) ↗"}
? "Use Chat ↗"
: "Use Chat (staging) ↗"}
</Link>
</Button>
</div>
) : null}
<main className="p-4">{props.children}</main>
<Toaster visibleToasts={1} />
</div>
</div>
</>
@@ -505,120 +531,137 @@ type NestedNavigationItem = Omit<Route, "children"> & {
const MainNavigation: React.FC<{
nav: NavigationItem[];
onNavitemClick?: () => void;
}> = ({ nav, onNavitemClick }) => (
<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)}
>
{({ 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">
{item.icon && (
<item.icon
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
aria-hidden="true"
/>
}> = ({ nav, onNavitemClick }) => {
const [isOpen, setIsOpen] = useLocalStorage(
"sidebar-tracing-default-open",
false,
);
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",
)}
{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>
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",
)}
<ChevronRightIcon
className={clsx(
open ? "rotate-90 text-gray-500" : "text-gray-400",
"ml-auto h-5 w-5 shrink-0",
>
{item.label}
</span>
)}
</Link>
) : item.children && item.children.length > 0 ? (
<Disclosure
as="div"
defaultOpen={
item.children.some((child) => child.current) || isOpen
}
>
{({ 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={() => setIsOpen(!isOpen)}
>
{item.icon && (
<item.icon
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
aria-hidden="true"
/>
)}
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",
{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",
)}
>
{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>
);
{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"
title="Provide feedback"
description="What do you think about this project? What can be improved?"
type="feedback"
>
<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>
);
};
+4
View File
@@ -47,6 +47,10 @@ export const ROUTES: Route[] = [
name: "Scores",
pathname: `/project/[projectId]/scores`,
},
{
name: "Models",
pathname: `/project/[projectId]/models`,
},
],
},
{
+11 -4
View File
@@ -68,15 +68,22 @@ export const SessionPage: React.FC<{
/>,
<DetailPageNav
key="nav"
currentId={sessionId}
path={(id) => `/project/${projectId}/sessions/${id}`}
currentId={encodeURIComponent(sessionId)}
path={(id) =>
`/project/${projectId}/sessions/${encodeURIComponent(id)}`
}
listKey="sessions"
/>,
]}
/>
<div className="flex flex-wrap gap-2">
{session.data?.users.map((userId) => (
<Link key={userId} href={`/project/${projectId}/users/${userId}`}>
{session.data?.users.filter(Boolean).map((userId) => (
<Link
key={userId}
href={`/project/${projectId}/users/${encodeURIComponent(
userId ?? "",
)}`}
>
<Badge>User ID: {userId}</Badge>
</Link>
))}
+26 -14
View File
@@ -3,6 +3,7 @@
import DocPopup from "@/src/components/layouts/doc-popup";
import { DataTablePagination } from "@/src/components/table/data-table-pagination";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { type ModelTableRow } from "@/src/components/table/use-cases/models";
import {
Table,
TableBody,
@@ -100,8 +101,9 @@ export function DataTable<TData extends object, TValue>({
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const sortingEnabled =
header.column.columnDef.enableSorting;
const columnDef = header.column
.columnDef as LangfuseColumnDef<ModelTableRow>;
const sortingEnabled = columnDef.enableSorting;
return header.column.getIsVisible() ? (
<TableHead
key={header.id}
@@ -113,22 +115,22 @@ export function DataTable<TData extends object, TValue>({
onClick={(event) => {
event.preventDefault(); // Add this line
if (
!setOrderBy ||
!header.column.columnDef.id ||
!sortingEnabled
) {
if (!setOrderBy || !columnDef.id || !sortingEnabled) {
return;
}
if (orderBy?.column === header.column.columnDef.id) {
setOrderBy({
column: header.column.columnDef.id,
order: orderBy.order === "ASC" ? "DESC" : "ASC",
});
if (orderBy?.column === columnDef.id) {
if (orderBy.order === "DESC") {
setOrderBy({
column: columnDef.id,
order: "ASC",
});
} else {
setOrderBy(null);
}
} else {
setOrderBy({
column: header.column.columnDef.id,
column: columnDef.id,
order: "DESC",
});
}
@@ -142,7 +144,17 @@ export function DataTable<TData extends object, TValue>({
header.getContext(),
)}
{orderBy?.column === header.column.columnDef.id
{columnDef.headerTooltip && (
<DocPopup
description={
columnDef.headerTooltip.description
}
href={columnDef.headerTooltip.href}
size="xs"
/>
)}
{orderBy?.column === columnDef.id
? renderOrderingIndicator(orderBy)
: null}
</div>
+7 -1
View File
@@ -9,4 +9,10 @@ export type TableRowOptions = {
export type LangfuseColumnDef<
TData extends RowData,
TValue = unknown,
> = ColumnDef<TData, TValue> & { defaultHidden?: boolean };
> = ColumnDef<TData, TValue> & {
defaultHidden?: boolean;
headerTooltip?: {
description: string;
href?: string;
};
};
+65 -37
View File
@@ -1,4 +1,5 @@
import { api, directApi } from "@/src/utils/api";
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
import { DataTable } from "@/src/components/table/data-table";
import TableLink from "@/src/components/table/table-link";
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
@@ -26,7 +27,7 @@ import { formatInterval, utcDateOffsetByDays } from "@/src/utils/dates";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { JSONView } from "@/src/components/ui/code";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { type ObservationLevel } from "@prisma/client";
import { type Score, type ObservationLevel } from "@prisma/client";
import { cn } from "@/src/utils/tailwind";
import { LevelColors } from "@/src/components/level-colors";
import { usdFormatter } from "@/src/utils/numbers";
@@ -34,6 +35,7 @@ import {
exportOptions,
type ExportFileFormats,
} from "@/src/server/api/interfaces/exportTypes";
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
export type GenerationsTableRow = {
id: string;
@@ -49,6 +51,7 @@ export type GenerationsTableRow = {
output?: unknown;
traceName?: string;
metadata?: string;
scores: Score[];
usage: {
promptTokens: number;
completionTokens: number;
@@ -82,42 +85,35 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
},
]);
const generationsQueries = api.useQueries((t) => [
t.generations.all({
page: paginationState.pageIndex,
limit: paginationState.pageSize / 2,
projectId,
filter: filterState,
searchQuery,
}),
t.generations.all({
page: paginationState.pageIndex + paginationState.pageSize / 2,
limit: paginationState.pageSize / 2,
projectId,
filter: filterState,
searchQuery,
}),
]);
const generations = {
isLoading:
generationsQueries[0].isLoading || generationsQueries[1].isLoading,
isError: generationsQueries[0].isError || generationsQueries[1].isError,
isSuccess:
generationsQueries[0].isSuccess || generationsQueries[1].isSuccess,
data: [
...(generationsQueries[0].data ?? []),
...(generationsQueries[1].data ?? []),
],
error: generationsQueries[0].error ?? generationsQueries[1].error,
};
const totalCount = generations.data.slice(1)[0]?.totalCount ?? 0;
const filterOptions = api.generations.filterOptions.useQuery({
projectId,
const [orderByState, setOrderByState] = useOrderByState({
column: "startTime",
order: "DESC",
});
const generations = api.generations.all.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
projectId,
filter: filterState,
orderBy: orderByState,
searchQuery,
});
const totalCount = generations.data?.totalCount ?? 0;
const filterOptions = api.generations.filterOptions.useQuery(
{
projectId,
},
{
trpc: {
context: {
skipBatch: true,
},
},
},
);
const handleExport = async (fileFormat: ExportFileFormats) => {
if (isExporting) return;
@@ -129,6 +125,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
fileFormat,
filter: filterState,
searchQuery,
orderBy: orderByState,
});
let url: string;
@@ -166,6 +163,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
const columns: LangfuseColumnDef<GenerationsTableRow>[] = [
{
accessorKey: "id",
id: "id",
header: "ID",
cell: ({ row }) => {
const observationId = row.getValue("id");
@@ -178,13 +176,17 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
/>
) : null;
},
enableSorting: true,
},
{
accessorKey: "name",
id: "name",
header: "name",
enableSorting: true,
},
{
accessorKey: "traceId",
id: "traceId",
header: "Trace ID",
cell: ({ row }) => {
const value = row.getValue("traceId");
@@ -195,19 +197,35 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
/>
) : undefined;
},
enableSorting: true,
},
{
accessorKey: "traceName",
id: "traceName",
header: "Trace Name",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "startTime",
id: "startTime",
header: "Start Time",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "scores",
id: "scores",
header: "Scores",
cell: ({ row }) => {
const values: Score[] = row.getValue("scores");
return <GroupedScoreBadges scores={values} variant="headings" />;
},
enableHiding: true,
},
{
accessorKey: "latency",
id: "latency",
header: "Latency",
cell: ({ row }) => {
const value: number | undefined = row.getValue("latency");
@@ -216,6 +234,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
) : undefined;
},
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "cost",
@@ -231,6 +250,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
},
{
accessorKey: "level",
id: "level",
header: "Level",
enableHiding: true,
cell({ row }) {
@@ -247,6 +267,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
</span>
) : undefined;
},
enableSorting: true,
},
{
accessorKey: "statusMessage",
@@ -256,8 +277,10 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
},
{
accessorKey: "model",
id: "model",
header: "Model",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "usage",
@@ -311,8 +334,10 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
},
{
accessorKey: "version",
id: "version",
header: "Version",
enableHiding: true,
enableSorting: true,
},
];
const [columnVisibility, setColumnVisibility] =
@@ -322,7 +347,7 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
);
const rows: GenerationsTableRow[] = generations.isSuccess
? generations.data.map((generation) => {
? generations.data.generations.map((generation) => {
return {
id: generation.id,
traceId: generation.traceId,
@@ -330,11 +355,12 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
startTime: generation.startTime.toLocaleString(),
endTime: generation.endTime?.toLocaleString() ?? undefined,
latency: generation.latency === null ? undefined : generation.latency,
cost: generation.cost,
cost: generation.calculatedTotalCost,
name: generation.name ?? undefined,
version: generation.version ?? "",
model: generation.model ?? "",
input: generation.input,
scores: generation.scores,
output: generation.output,
level: generation.level,
metadata: generation.metadata
@@ -420,6 +446,8 @@ export default function GenerationsTable({ projectId }: GenerationsTableProps) {
onChange: setPaginationState,
state: paginationState,
}}
setOrderBy={setOrderByState}
orderBy={orderByState}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
+330
View File
@@ -0,0 +1,330 @@
import { DataTable } from "@/src/components/table/data-table";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { Button } from "@/src/components/ui/button";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { api } from "@/src/utils/api";
import { usdFormatter } from "@/src/utils/numbers";
import { type Prisma, type Model } from "@prisma/client";
import Decimal from "decimal.js";
import { Trash } from "lucide-react";
import { useQueryParams, withDefault, NumberParam } from "use-query-params";
export type ModelTableRow = {
modelId: string;
maintainer: string;
modelName: string;
matchPattern: string;
startDate?: Date;
inputPrice?: Decimal;
outputPrice?: Decimal;
totalPrice?: Decimal;
unit: string;
tokenizerId?: string;
config?: Prisma.JsonValue;
};
const modelConfigDescriptions = {
modelName:
"Standardized model name. Generations are assigned to this model name if they match the `matchPattern` upon ingestion.",
matchPattern:
"Regex pattern to match `model` parameter of generations to model pricing",
startDate:
"Date to start pricing model. If not set, model is active unless a more recent version exists.",
inputPrice: "Price per 1000 units of input",
outputPrice: "Price per 1000 units of output",
totalPrice:
"Price per 1000 units, for models that don't have input/output specific prices",
unit: "Unit of measurement for generative model, can be TOKENS, CHARACTERS, SECONDS, MILLISECONDS, or IMAGES.",
tokenizerId:
"Tokenizer used for this model to calculate token counts if none are ingested. Pick from list of supported tokenizers.",
config:
"Some tokenizers require additional configuration (e.g. openai tiktoken). See docs for details.",
} as const;
export default function ModelTable({ projectId }: { projectId: string }) {
const [paginationState, setPaginationState] = useQueryParams({
pageIndex: withDefault(NumberParam, 0),
pageSize: withDefault(NumberParam, 50),
});
const models = api.models.all.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
projectId,
});
const totalCount = models.data?.totalCount ?? 0;
const columns: LangfuseColumnDef<ModelTableRow>[] = [
{
accessorKey: "maintainer",
id: "maintainer",
enableColumnFilter: true,
header: "Maintainer",
},
{
accessorKey: "modelName",
id: "modelName",
header: "Model Name",
headerTooltip: {
description: modelConfigDescriptions.modelName,
},
},
{
accessorKey: "startDate",
id: "startDate",
header: "Start Date",
headerTooltip: {
description: modelConfigDescriptions.startDate,
},
cell: ({ row }) => {
const value: Date | undefined = row.getValue("startDate");
return value ? (
<span className="text-xs">{value.toISOString().slice(0, 10)} </span>
) : (
<span className="text-xs">-</span>
);
},
},
{
accessorKey: "matchPattern",
id: "matchPattern",
headerTooltip: {
description: modelConfigDescriptions.matchPattern,
},
header: "Match Pattern",
cell: ({ row }) => {
const value: string = row.getValue("matchPattern");
return (
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-xs ">
{value}
</code>
);
},
},
{
accessorKey: "inputPrice",
id: "inputPrice",
header: () => {
return (
<>
Input Price{" "}
<span className="text-xs text-gray-400">/ 1k units</span>
</>
);
},
headerTooltip: {
description: modelConfigDescriptions.inputPrice,
},
cell: ({ row }) => {
const value: Decimal | undefined = row.getValue("inputPrice");
return value ? (
<span className="text-xs">
{usdFormatter(value.toNumber() * 1000, 2, 8)}
</span>
) : (
<span className="text-xs">-</span>
);
},
},
{
accessorKey: "outputPrice",
id: "outputPrice",
headerTooltip: {
description: modelConfigDescriptions.outputPrice,
},
header: () => {
return (
<>
Output Price{" "}
<span className="text-xs text-gray-400">/ 1k units</span>
</>
);
},
cell: ({ row }) => {
const value: Decimal | undefined = row.getValue("outputPrice");
return value ? (
<span className="text-xs">
{usdFormatter(value.toNumber() * 1000, 2, 8)}
</span>
) : (
<span className="text-xs">-</span>
);
},
},
{
accessorKey: "totalPrice",
id: "totalPrice",
header: () => {
return (
<>
Total Price{" "}
<span className="text-xs text-gray-400">/ 1k units</span>
</>
);
},
headerTooltip: {
description: modelConfigDescriptions.totalPrice,
},
cell: ({ row }) => {
const value: Decimal | undefined = row.getValue("totalPrice");
return value ? (
<span className="text-xs">
{usdFormatter(value.toNumber() * 1000, 2, 8)}
</span>
) : (
<span className="text-xs">-</span>
);
},
},
{
accessorKey: "unit",
id: "unit",
header: "Unit",
headerTooltip: {
description: modelConfigDescriptions.unit,
},
enableHiding: true,
},
{
accessorKey: "tokenizerId",
id: "tokenizerId",
header: "Tokenizer",
headerTooltip: {
description: modelConfigDescriptions.tokenizerId,
},
enableHiding: true,
},
{
accessorKey: "config",
id: "config",
header: "Tokenizer Configuration",
headerTooltip: {
description: modelConfigDescriptions.config,
},
enableHiding: true,
cell: ({ row }) => {
const value: Prisma.JsonValue | undefined = row.getValue("config");
return value ? (
<span className="text-xs">{JSON.stringify(value)}</span>
) : (
<span className="text-xs">-</span>
);
},
},
{
accessorKey: "actions",
header: "Actions",
cell: ({ row }) => {
return row.original.maintainer === "User" ? (
<DeleteModelButton
projectId={projectId}
modelId={row.original.modelId}
/>
) : (
<div className="h-6" />
);
},
},
];
const [columnVisibility, setColumnVisibility] =
useColumnVisibility<ModelTableRow>("scoresColumnVisibility", columns);
const convertToTableRow = (model: Model): ModelTableRow => {
return {
modelId: model.id,
maintainer: model.projectId ? "User" : "Langfuse",
modelName: model.modelName,
matchPattern: model.matchPattern,
startDate: model.startDate ? new Date(model.startDate) : undefined,
inputPrice: model.inputPrice ? new Decimal(model.inputPrice) : undefined,
outputPrice: model.outputPrice
? new Decimal(model.outputPrice)
: undefined,
totalPrice: model.totalPrice ? new Decimal(model.totalPrice) : undefined,
unit: model.unit,
tokenizerId: model.tokenizerId ?? undefined,
config: model.tokenizerConfig,
};
};
return (
<div>
<DataTable
columns={columns}
data={
models.isLoading
? { isLoading: true, isError: false }
: models.isError
? {
isLoading: false,
isError: true,
error: models.error.message,
}
: {
isLoading: false,
isError: false,
data: models.data.models.map((t) => convertToTableRow(t)),
}
}
pagination={{
pageCount: Math.ceil(totalCount / paginationState.pageSize),
onChange: setPaginationState,
state: paginationState,
}}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
/>
</div>
);
}
const DeleteModelButton = ({
modelId,
projectId,
}: {
modelId: string;
projectId: string;
}) => {
const utils = api.useUtils();
const mut = api.models.delete.useMutation({
onSuccess: () => {
void utils.models.invalidate();
},
});
const hasAccess = useHasAccess({
projectId,
scope: "models:CUD",
});
if (!hasAccess) {
return null;
}
return (
<Button
size="xs"
variant="ghost"
onClick={() => {
mut
.mutateAsync({
projectId,
modelId,
})
.catch((error) => {
console.error(error);
});
}}
>
<Trash size={14} />
</Button>
);
};
+21 -2
View File
@@ -7,6 +7,7 @@ import useColumnVisibility from "@/src/features/column-visibility/hooks/useColum
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import { type FilterState } from "@/src/features/filters/types";
import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context";
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
import { sessionsViewCols } from "@/src/server/api/definitions/sessionsView";
import { api } from "@/src/utils/api";
import { formatInterval, utcDateOffsetByDays } from "@/src/utils/dates";
@@ -63,11 +64,17 @@ export default function SessionsTable({
pageSize: withDefault(NumberParam, 50),
});
const [orderByState, setOrderByState] = useOrderByState({
column: "createdAt",
order: "DESC",
});
const sessions = api.sessions.all.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
projectId,
filter: filterState,
orderBy: orderByState,
});
const totalCount = sessions.data?.slice(1)[0]?.totalCount ?? 0;
@@ -97,6 +104,7 @@ export default function SessionsTable({
const columns: LangfuseColumnDef<SessionTableRow>[] = [
{
accessorKey: "bookmarked",
id: "bookmarked",
header: undefined,
cell: ({ row }) => {
const bookmarked = row.getValue("bookmarked");
@@ -112,27 +120,33 @@ export default function SessionsTable({
/>
) : undefined;
},
enableSorting: true,
},
{
accessorKey: "id",
id: "id",
header: "ID",
cell: ({ row }) => {
const value = row.getValue("id");
return value && typeof value === "string" ? (
<TableLink
path={`/project/${projectId}/sessions/${value}`}
path={`/project/${projectId}/sessions/${encodeURIComponent(value)}`}
value={value}
/>
) : undefined;
},
enableSorting: true,
},
{
accessorKey: "createdAt",
id: "createdAt",
header: "Created At",
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "sessionDuration",
id: "sessionDuration",
header: "Duration",
enableHiding: true,
cell: ({ row }) => {
@@ -141,6 +155,7 @@ export default function SessionsTable({
? formatInterval(value)
: undefined;
},
enableSorting: true,
},
{
accessorKey: "userIds",
@@ -154,7 +169,7 @@ export default function SessionsTable({
{(value as string[]).map((user) => (
<TableLink
key={user}
path={`/project/${projectId}/users/${user}`}
path={`/project/${projectId}/users/${encodeURIComponent(user)}`}
value={user}
truncateAt={40}
/>
@@ -165,8 +180,10 @@ export default function SessionsTable({
},
{
accessorKey: "countTraces",
id: "countTraces",
header: "Traces",
enableHiding: true,
enableSorting: true,
},
];
@@ -205,6 +222,8 @@ export default function SessionsTable({
onChange: setPaginationState,
state: paginationState,
}}
setOrderBy={setOrderByState}
orderBy={orderByState}
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
help={{
+41 -4
View File
@@ -6,6 +6,7 @@ import { TraceTableMultiSelectAction } from "@/src/components/table/data-table-m
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import TableLink from "@/src/components/table/table-link";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import { TagTracePopver } from "@/src/features/tag/components/TagTracePopver";
import { TokenUsageBadge } from "@/src/components/token-usage-badge";
import { Checkbox } from "@/src/components/ui/checkbox";
import { JSONView } from "@/src/components/ui/code";
@@ -28,6 +29,8 @@ import {
useQueryParams,
withDefault,
} from "use-query-params";
import type Decimal from "decimal.js";
import { usdFormatter } from "@/src/utils/numbers";
export type TracesTableRow = {
bookmarked: boolean;
@@ -49,6 +52,7 @@ export type TracesTableRow = {
completionTokens: number;
totalTokens: number;
};
cost?: Decimal;
};
export type TracesTableProps = {
@@ -135,7 +139,6 @@ export default function TracesTable({
},
},
);
const convertToTableRow = (
trace: RouterOutput["traces"]["all"]["traces"][0],
): TracesTableRow => {
@@ -159,6 +162,7 @@ export default function TracesTable({
completionTokens: trace.completionTokens,
totalTokens: trace.totalTokens,
},
cost: trace.calculatedTotalCost ?? undefined,
};
};
@@ -198,7 +202,6 @@ export default function TracesTable({
cell: ({ row }) => {
const bookmarked = row.getValue("bookmarked");
const traceId = row.getValue("id");
return typeof traceId === "string" &&
typeof bookmarked === "boolean" ? (
<StarTraceToggle
@@ -248,7 +251,7 @@ export default function TracesTable({
const value = row.getValue("userId");
return value && typeof value === "string" ? (
<TableLink
path={`/project/${projectId}/users/${value}`}
path={`/project/${projectId}/users/${encodeURIComponent(value)}`}
value={value}
truncateAt={40}
/>
@@ -265,7 +268,7 @@ export default function TracesTable({
const value = row.getValue("sessionId");
return value && typeof value === "string" ? (
<TableLink
path={`/project/${projectId}/sessions/${value}`}
path={`/project/${projectId}/sessions/${encodeURIComponent(value)}`}
value={value}
truncateAt={40}
/>
@@ -306,6 +309,25 @@ export default function TracesTable({
},
enableHiding: true,
},
{
accessorKey: "cost",
id: "cost",
header: "Cost",
cell: ({ row }) => {
const cost: Decimal | undefined = row.getValue("cost");
return (
<div>
{cost ? (
<span>{usdFormatter(cost.toNumber())}</span>
) : (
<span>Not Available</span>
)}
</div>
);
},
enableHiding: true,
enableSorting: true,
},
{
accessorKey: "scores",
id: "scores",
@@ -364,6 +386,21 @@ export default function TracesTable({
accessorKey: "tags",
id: "tags",
header: "Tags",
cell: ({ row }) => {
const tags: string[] = row.getValue("tags");
const traceId: string = row.getValue("id");
const filterOptionTags = traceFilterOptions.data?.tags ?? [];
const allTags = filterOptionTags.map((t) => t.value);
return (
<TagTracePopver
tags={tags}
availableTags={allTags}
projectId={projectId}
traceId={traceId}
tracesFilter={tracesAllQueryFilter}
/>
);
},
enableHiding: true,
},
{
+14 -8
View File
@@ -24,6 +24,7 @@ import { IOPreview } from "@/src/components/trace/IOPreview";
import { formatInterval } from "@/src/utils/dates";
import Link from "next/link";
import { usdFormatter } from "@/src/utils/numbers";
import { calculateDisplayTotalCost } from "@/src/components/trace";
export const ObservationPreview = (props: {
observations: Array<ObservationReturnType>;
@@ -41,6 +42,10 @@ export const ObservationPreview = (props: {
(o) => o.id === props.currentObservationId,
);
const totalCost = calculateDisplayTotalCost(
preloadedObservation ? [preloadedObservation] : [],
);
if (!preloadedObservation) return <div className="flex-1">Not found</div>;
return (
<Card className="flex-1">
@@ -97,9 +102,9 @@ export const ObservationPreview = (props: {
{preloadedObservation.model ? (
<Badge variant="outline">{preloadedObservation.model}</Badge>
) : null}
{preloadedObservation.price ? (
{totalCost ? (
<Badge variant="outline">
{usdFormatter(preloadedObservation.price.toNumber())}
{usdFormatter(totalCost.toNumber())}
</Badge>
) : undefined}
@@ -200,14 +205,15 @@ const PromptBadge = (props: { promptId: string; projectId: string }) => {
projectId: props.projectId,
});
if (prompt.isLoading) return null;
if (prompt.isLoading || !prompt.data) return null;
return (
<Link href={`/project/${props.projectId}/prompts/${props.promptId}`}>
<Link
href={`/project/${props.projectId}/prompts/${prompt.data.name}?version=${prompt.data.version}`}
>
<Badge>
Prompt: {prompt.data?.name}
{" - "}
{prompt.data?.version}
Prompt: {prompt.data.name}
{" - v"}
{prompt.data.version}
</Badge>
</Link>
);
+167 -86
View File
@@ -1,14 +1,21 @@
import { type NestedObservation } from "@/src/utils/types";
import { cn } from "@/src/utils/tailwind";
import { type Trace, type Score } from "@prisma/client";
import { type Trace, type Score, $Enums } from "@prisma/client";
import { GroupedScoreBadges } from "@/src/components/grouped-score-badge";
import { Fragment } from "react";
import { type ObservationReturnType } from "@/src/server/api/routers/traces";
import { LevelColors } from "@/src/components/level-colors";
import { formatInterval } from "@/src/utils/dates";
import { MinusCircle, MinusIcon, PlusCircleIcon, PlusIcon } from "lucide-react";
import { Toggle } from "@/src/components/ui/toggle";
import { Button } from "@/src/components/ui/button";
export const ObservationTree = (props: {
observations: ObservationReturnType[];
collapsedObservations: string[];
toggleCollapsedObservation: (id: string) => void;
collapseAll: () => void;
expandAll: () => void;
trace: Trace;
scores: Score[];
currentObservationId: string | undefined;
@@ -21,6 +28,8 @@ export const ObservationTree = (props: {
return (
<div className={props.className}>
<ObservationTreeTraceNode
expandAll={props.expandAll}
collapseAll={props.collapseAll}
trace={props.trace}
scores={props.scores}
currentObservationId={props.currentObservationId}
@@ -30,6 +39,8 @@ export const ObservationTree = (props: {
/>
<ObservationTreeNode
observations={nestedObservations}
collapsedObservations={props.collapsedObservations}
toggleCollapsedObservation={props.toggleCollapsedObservation}
scores={props.scores}
indentationLevel={1}
currentObservationId={props.currentObservationId}
@@ -40,8 +51,11 @@ export const ObservationTree = (props: {
</div>
);
};
const ObservationTreeTraceNode = (props: {
trace: Trace & { latency?: number };
expandAll: () => void;
collapseAll: () => void;
scores: Score[];
currentObservationId: string | undefined;
setCurrentObservationId: (id: string | undefined) => void;
@@ -50,7 +64,7 @@ const ObservationTreeTraceNode = (props: {
}) => (
<div
className={cn(
"group mb-0.5 flex cursor-pointer flex-col gap-1 rounded-sm p-1.5",
"group mb-0.5 flex cursor-pointer flex-col gap-1 rounded-sm p-1",
props.currentObservationId === undefined ||
props.currentObservationId === ""
? "bg-gray-100"
@@ -60,7 +74,23 @@ const ObservationTreeTraceNode = (props: {
>
<div className="flex gap-2">
<span className={cn("rounded-sm bg-gray-200 p-1 text-xs")}>TRACE</span>
<span className="text-sm">{props.trace.name}</span>
<span className="flex-1 text-sm">{props.trace.name}</span>
<Button
onClick={(ev) => (ev.stopPropagation(), props.expandAll())}
size="xs"
variant="ghost"
title="Expand all"
>
<PlusCircleIcon className="h-4 w-4" />
</Button>
<Button
onClick={(ev) => (ev.stopPropagation(), props.collapseAll())}
size="xs"
variant="ghost"
title="Collapse all"
>
<MinusCircle className="h-4 w-4" />
</Button>
</div>
{props.showMetrics && props.trace.latency ? (
@@ -79,8 +109,11 @@ const ObservationTreeTraceNode = (props: {
) : null}
</div>
);
const ObservationTreeNode = (props: {
observations: NestedObservation[];
collapsedObservations: string[];
toggleCollapsedObservation: (id: string) => void;
scores: Score[];
indentationLevel: number;
currentObservationId: string | undefined;
@@ -91,96 +124,144 @@ const ObservationTreeNode = (props: {
<>
{props.observations
.sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
.map((observation) => (
<Fragment key={observation.id}>
<div className="flex">
{Array.from({ length: props.indentationLevel }, (_, i) => (
<div className="mx-2 border-r" key={i} />
))}
<div
className={cn(
"group my-0.5 flex flex-1 cursor-pointer flex-col gap-1 rounded-sm p-1.5",
props.currentObservationId === observation.id
? "bg-gray-100"
: "hover:bg-gray-50",
)}
onClick={() => props.setCurrentObservationId(observation.id)}
>
<div className="flex gap-2">
<span
className={cn(
"self-start rounded-sm bg-gray-200 p-1 text-xs",
)}
>
{observation.type}
</span>
<span className="line-clamp-1 text-sm">{observation.name}</span>
</div>
{props.showMetrics &&
(observation.promptTokens ||
observation.completionTokens ||
observation.totalTokens ||
observation.endTime) && (
<div className="flex gap-2">
{observation.endTime ? (
<span className="text-xs text-gray-500">
{formatInterval(
(observation.endTime.getTime() -
observation.startTime.getTime()) /
1000,
)}
</span>
) : null}
{observation.promptTokens ||
observation.completionTokens ||
observation.totalTokens ? (
<span className="text-xs text-gray-500">
{observation.promptTokens} {" "}
{observation.completionTokens} ({" "}
{observation.totalTokens})
</span>
) : null}
</div>
.map((observation) => {
const collapsed = props.collapsedObservations.includes(observation.id);
return (
<Fragment key={observation.id}>
<div className="flex">
{Array.from({ length: props.indentationLevel }, (_, i) => (
<div className="mx-2 border-r" key={i} />
))}
<div
className={cn(
"group my-0.5 flex flex-1 cursor-pointer flex-col gap-1 rounded-sm p-1",
props.currentObservationId === observation.id
? "bg-gray-100"
: "hover:bg-gray-50",
)}
{observation.level !== "DEFAULT" ? (
<div className="flex">
<span
className={cn(
"rounded-sm p-0.5 text-xs",
LevelColors[observation.level].bg,
LevelColors[observation.level].text,
)}
>
{observation.level}
</span>
</div>
) : null}
{props.showScores &&
props.scores.find((s) => s.observationId === observation.id) ? (
<div className="flex flex-wrap gap-1">
<GroupedScoreBadges
scores={props.scores.filter(
(s) => s.observationId === observation.id,
)}
onClick={() => props.setCurrentObservationId(observation.id)}
>
<div className="flex gap-2">
<ColorCodedObservationType
observationType={observation.type}
/>
<span className="line-clamp-1 flex-1 text-sm">
{observation.name}
</span>
{observation.children.length === 0 ? null : (
<Toggle
onClick={(ev) => (
ev.stopPropagation(),
props.toggleCollapsedObservation(observation.id)
)}
variant="default"
pressed={collapsed}
size="xs"
className="w-7"
title={
collapsed ? "Expand children" : "Collapse children"
}
>
{collapsed ? (
<PlusIcon className="h-4 w-4" />
) : (
<MinusIcon className="h-4 w-4" />
)}
</Toggle>
)}
</div>
) : null}
{props.showMetrics &&
(observation.promptTokens ||
observation.completionTokens ||
observation.totalTokens ||
observation.endTime) && (
<div className="flex gap-2">
{observation.endTime ? (
<span className="text-xs text-gray-500">
{formatInterval(
(observation.endTime.getTime() -
observation.startTime.getTime()) /
1000,
)}
</span>
) : null}
{observation.promptTokens ||
observation.completionTokens ||
observation.totalTokens ? (
<span className="text-xs text-gray-500">
{observation.promptTokens} {" "}
{observation.completionTokens} ({" "}
{observation.totalTokens})
</span>
) : null}
</div>
)}
{observation.level !== "DEFAULT" ? (
<div className="flex">
<span
className={cn(
"rounded-sm p-0.5 text-xs",
LevelColors[observation.level].bg,
LevelColors[observation.level].text,
)}
>
{observation.level}
</span>
</div>
) : null}
{props.showScores &&
props.scores.find((s) => s.observationId === observation.id) ? (
<div className="flex flex-wrap gap-1">
<GroupedScoreBadges
scores={props.scores.filter(
(s) => s.observationId === observation.id,
)}
/>
</div>
) : null}
</div>
</div>
</div>
<ObservationTreeNode
observations={observation.children}
scores={props.scores}
indentationLevel={props.indentationLevel + 1}
currentObservationId={props.currentObservationId}
setCurrentObservationId={props.setCurrentObservationId}
showMetrics={props.showMetrics}
showScores={props.showScores}
/>
</Fragment>
))}
{!collapsed && (
<ObservationTreeNode
observations={observation.children}
collapsedObservations={props.collapsedObservations}
toggleCollapsedObservation={props.toggleCollapsedObservation}
scores={props.scores}
indentationLevel={props.indentationLevel + 1}
currentObservationId={props.currentObservationId}
setCurrentObservationId={props.setCurrentObservationId}
showMetrics={props.showMetrics}
showScores={props.showScores}
/>
)}
</Fragment>
);
})}
</>
);
const ColorCodedObservationType = (props: {
observationType: $Enums.ObservationType;
}) => {
const colors: Record<$Enums.ObservationType, string> = {
[$Enums.ObservationType.SPAN]: "bg-blue-100",
[$Enums.ObservationType.GENERATION]: "bg-orange-100",
[$Enums.ObservationType.EVENT]: "bg-green-100",
};
return (
<span
className={cn(
"self-start rounded-sm p-1 text-xs",
colors[props.observationType],
)}
>
{props.observationType}
</span>
);
};
export function nestObservations(
list: ObservationReturnType[],
): NestedObservation[] {
-3
View File
@@ -73,9 +73,6 @@ export const TracePreview = ({
title="Metadata"
json={trace.metadata}
/>
{trace.tags.length !== 0 && (
<JSONView key={trace.id + "-tags"} title="Tags" json={trace.tags} />
)}
{scores.find((s) => s.observationId === null) ? (
<div className="mt-5 flex flex-col gap-2">
<h3>Scores</h3>
+132 -17
View File
@@ -6,7 +6,6 @@ import { TracePreview } from "./TracePreview";
import Header from "@/src/components/layouts/header";
import { Badge } from "@/src/components/ui/badge";
import { TraceAggUsageBadge } from "@/src/components/token-usage-badge";
import Decimal from "decimal.js";
import { StringParam, useQueryParam } from "use-query-params";
import { PublishTraceSwitch } from "@/src/components/publish-object-switch";
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
@@ -17,10 +16,14 @@ import { DeleteTrace } from "@/src/components/delete-trace";
import { StarTraceDetailsToggle } from "@/src/components/star-toggle";
import Link from "next/link";
import { NoAccessError } from "@/src/components/no-access";
import { TagTraceDetailsPopover } from "@/src/features/tag/components/TagTraceDetailsPopover";
import useLocalStorage from "@/src/components/useLocalStorage";
import { Toggle } from "@/src/components/ui/toggle";
import { Award, ChevronsDownUp, ChevronsUpDown } from "lucide-react";
import { ScrollArea } from "@/src/components/ui/scroll-area";
import { usdFormatter } from "@/src/utils/numbers";
import Decimal from "decimal.js";
import { useCallback, useState } from "react";
export function Trace(props: {
observations: Array<ObservationReturnType>;
@@ -39,9 +42,57 @@ export function Trace(props: {
true,
);
const [collapsedObservations, setCollapsedObservations] = useState<string[]>(
[],
);
const toggleCollapsedObservation = useCallback(
(id: string) => {
if (collapsedObservations.includes(id)) {
setCollapsedObservations(collapsedObservations.filter((i) => i !== id));
} else {
setCollapsedObservations([...collapsedObservations, id]);
}
},
[collapsedObservations],
);
const collapseAll = useCallback(() => {
// exclude all parents of the current observation
let excludeParentObservations = new Set<string>();
let newExcludeParentObservations = new Set<string>();
do {
excludeParentObservations = new Set<string>([
...excludeParentObservations,
...newExcludeParentObservations,
]);
newExcludeParentObservations = new Set<string>(
props.observations
.filter(
(o) =>
o.parentObservationId !== null &&
(o.id === currentObservationId ||
excludeParentObservations.has(o.id)),
)
.map((o) => o.parentObservationId as string)
.filter((id) => !excludeParentObservations.has(id)),
);
} while (newExcludeParentObservations.size > 0);
setCollapsedObservations(
props.observations
.map((o) => o.id)
.filter((id) => !excludeParentObservations.has(id)),
);
}, [props.observations, currentObservationId]);
const expandAll = useCallback(() => {
setCollapsedObservations([]);
}, [setCollapsedObservations]);
return (
<div className="grid gap-4 md:h-full md:grid-cols-3">
<ScrollArea className="md:col-span-2 md:h-full">
<div className="grid gap-4 md:h-full md:grid-cols-5">
<ScrollArea className="md:col-span-3 md:h-full">
{currentObservationId === undefined ||
currentObservationId === "" ||
currentObservationId === null ? (
@@ -60,7 +111,7 @@ export function Trace(props: {
/>
)}
</ScrollArea>
<div className="md:flex md:h-full md:flex-col md:overflow-hidden">
<div className="md:col-span-2 md:flex md:h-full md:flex-col md:overflow-hidden">
<div className="mb-2 flex flex-shrink-0 flex-row justify-end gap-2">
<Toggle
pressed={scoresOnObservationTree}
@@ -90,6 +141,10 @@ export function Trace(props: {
<ScrollArea className="flex flex-grow">
<ObservationTree
observations={props.observations}
collapsedObservations={collapsedObservations}
toggleCollapsedObservation={toggleCollapsedObservation}
collapseAll={collapseAll}
expandAll={expandAll}
trace={props.trace}
scores={props.scores}
currentObservationId={currentObservationId ?? undefined}
@@ -114,18 +169,28 @@ export function TracePage({ traceId }: { traceId: string }) {
},
},
);
const totalCost = trace.data?.observations.reduce(
(acc, o) => {
if (!o.price) return acc;
return acc ? acc.plus(o.price) : new Decimal(0).plus(o.price);
const traceFilterOptions = api.traces.filterOptions.useQuery(
{
projectId: trace.data?.projectId ?? "",
},
{
trpc: {
context: {
skipBatch: true,
},
},
enabled: !!trace.data?.projectId && trace.isSuccess,
},
undefined as Decimal | undefined,
);
const filterOptionTags = traceFilterOptions.data?.tags ?? [];
const allTags = filterOptionTags.map((t) => t.value);
const totalCost = calculateDisplayTotalCost(trace.data?.observations ?? []);
if (trace.error?.data?.code === "UNAUTHORIZED") return <NoAccessError />;
if (!trace.data) return <div>loading...</div>;
return (
<div className="flex flex-col overflow-hidden xl:container md:h-[calc(100vh-2rem)]">
<Header
@@ -166,18 +231,18 @@ export function TracePage({ traceId }: { traceId: string }) {
<div className="flex flex-wrap gap-2">
{trace.data.sessionId ? (
<Link
href={`/project/${router.query.projectId as string}/sessions/${
trace.data.sessionId
}`}
href={`/project/${
router.query.projectId as string
}/sessions/${encodeURIComponent(trace.data.sessionId)}`}
>
<Badge>Session: {trace.data.sessionId}</Badge>
</Link>
) : null}
{trace.data.userId ? (
<Link
href={`/project/${router.query.projectId as string}/users/${
trace.data.userId
}`}
href={`/project/${
router.query.projectId as string
}/users/${encodeURIComponent(trace.data.userId)}`}
>
<Badge>User ID: {trace.data.userId}</Badge>
</Link>
@@ -185,10 +250,21 @@ export function TracePage({ traceId }: { traceId: string }) {
<TraceAggUsageBadge observations={trace.data.observations} />
{totalCost ? (
<Badge variant="outline">
Total cost: {totalCost.toString()} USD
Total cost: {usdFormatter(totalCost.toNumber())}
</Badge>
) : undefined}
</div>
<div className="mt-5 rounded-lg border bg-card font-semibold text-card-foreground shadow-sm">
<div className="flex flex-row items-center gap-3 p-2.5">
Tags
<TagTraceDetailsPopover
tags={trace.data.tags}
availableTags={allTags}
traceId={trace.data.id}
projectId={trace.data.projectId}
/>
</div>
</div>
<div className="mt-5 flex-1 overflow-hidden border-t pt-5">
<Trace
key={trace.data.id}
@@ -201,3 +277,42 @@ export function TracePage({ traceId }: { traceId: string }) {
</div>
);
}
export const calculateDisplayTotalCost = (
observations: ObservationReturnType[],
) => {
return observations.reduce(
(prev: Decimal | undefined, curr: ObservationReturnType) => {
// if we don't have any calculated costs, we can't do anything
if (
!curr.calculatedTotalCost &&
!curr.calculatedInputCost &&
!curr.calculatedOutputCost
)
return prev;
// if we have either input or output cost, but not total cost, we can use that
if (
!curr.calculatedTotalCost &&
(curr.calculatedInputCost || curr.calculatedOutputCost)
) {
return prev
? prev.plus(
curr.calculatedInputCost ??
new Decimal(0).plus(
curr.calculatedOutputCost ?? new Decimal(0),
),
)
: curr.calculatedInputCost ?? curr.calculatedOutputCost ?? undefined;
}
if (!curr.calculatedTotalCost) return prev;
// if we have total cost, we can use that
return prev
? prev.plus(curr.calculatedTotalCost)
: curr.calculatedTotalCost;
},
undefined,
);
};
+29
View File
@@ -0,0 +1,29 @@
"use client";
import { Toaster as Sonner } from "sonner";
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => {
return (
<Sonner
theme={"light"}
className="toaster group"
position="top-right"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
);
};
export { Toaster };
+1
View File
@@ -17,6 +17,7 @@ const toggleVariants = cva(
},
size: {
default: "h-10 px-3",
xs: "h-6 px-1.5",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
+3
View File
@@ -20,6 +20,9 @@ import { useState, useEffect } from "react";
*/
function useLocalStorage<T>(localStorageKey: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
const storedValue = localStorage.getItem(localStorageKey);
return storedValue ? (JSON.parse(storedValue) as T) : initialValue;
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v1.32.2";
export const VERSION = "v2.4.2";
+9
View File
@@ -0,0 +1,9 @@
export { VERSION } from "./VERSION";
export enum ModelUsageUnit {
Characters = "CHARACTERS",
Tokens = "TOKENS",
Seconds = "SECONDS",
Milliseconds = "MILLISECONDS",
Images = "IMAGES",
}
+10
View File
@@ -26,6 +26,7 @@ export const env = createEnv({
),
NEXTAUTH_COOKIE_DOMAIN: z.string().optional(),
LANGFUSE_TEAM_SLACK_WEBHOOK: z.string().url().optional(),
LANGFUSE_TEAM_BETTERSTACK_TOKEN: z.string().optional(),
LANGFUSE_NEW_USER_SIGNUP_WEBHOOK: z.string().url().optional(),
// Add `.min(1) on ID and SECRET if you want to make sure they're not empty
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: z.enum(["true", "false"]).optional(),
@@ -57,6 +58,8 @@ export const env = createEnv({
S3_SECRET_ACCESS_KEY: z.string().optional(),
S3_BUCKET_NAME: z.string().optional(),
S3_REGION: z.string().optional(),
// Database exports
DB_EXPORT_PAGE_SIZE: z.number().optional(),
},
/**
@@ -91,6 +94,8 @@ export const env = createEnv({
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:
process.env.LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES,
LANGFUSE_TEAM_SLACK_WEBHOOK: process.env.LANGFUSE_TEAM_SLACK_WEBHOOK,
LANGFUSE_TEAM_BETTERSTACK_TOKEN:
process.env.LANGFUSE_TEAM_BETTERSTACK_TOKEN,
LANGFUSE_NEW_USER_SIGNUP_WEBHOOK:
process.env.LANGFUSE_NEW_USER_SIGNUP_WEBHOOK,
SALT: process.env.SALT,
@@ -117,5 +122,10 @@ export const env = createEnv({
S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY,
S3_BUCKET_NAME: process.env.S3_BUCKET_NAME,
S3_REGION: process.env.S3_REGION,
// Database exports
DB_EXPORT_PAGE_SIZE: process.env.DB_EXPORT_PAGE_SIZE,
},
// Skip validation in Docker builds
// DOCKER_BUILD is set in Dockerfile
skipValidation: process.env.DOCKER_BUILD === "1",
});
+57
View File
@@ -0,0 +1,57 @@
import { prisma as _prisma } from "@/src/server/db";
import { type MembershipRole } from "@prisma/client";
export type AuditableResource =
| "membership"
| "membershipInvitation"
| "datasetItem"
| "dataset"
| "trace"
| "project"
| "observation"
| "score"
| "model"
| "prompt"
| "session"
| "apiKey";
type AuditLog = {
resourceType: AuditableResource;
resourceId: string;
action: string;
before?: unknown;
after?: unknown;
} & (
| {
projectId: string;
userId: string;
userProjectRole: MembershipRole;
}
| {
session: {
user: {
id: string;
};
projectRole: MembershipRole;
projectId: string;
};
}
);
export async function auditLog(log: AuditLog, prisma?: typeof _prisma) {
await (prisma ?? _prisma).auditLog.create({
data: {
projectId: "projectId" in log ? log.projectId : log.session.projectId,
userId: "userId" in log ? log.userId : log.session.user.id,
userProjectRole:
"userProjectRole" in log
? log.userProjectRole
: log.session.projectRole,
resourceType: log.resourceType,
resourceId: log.resourceId,
action: log.action,
before: log.before ? JSON.stringify(log.before) : undefined,
after: log.after ? JSON.stringify(log.after) : undefined,
},
});
}
@@ -0,0 +1,11 @@
import { noHtmlCheck } from "@/src/utils/zod";
import * as z from "zod";
export const projectNameSchema = z.object({
name: z
.string()
.min(3, "Must have at least 3 characters")
.refine((value) => noHtmlCheck(value), {
message: "Input should not contain HTML",
}),
});
+7 -3
View File
@@ -1,9 +1,13 @@
import { noHtmlCheck } from "@/src/utils/zod";
import * as z from "zod";
export const signupSchema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
name: z
.string()
.min(1, { message: "Name is required" })
.refine((value) => noHtmlCheck(value), {
message: "Input should not contain HTML",
}),
email: z.string().email(),
password: z.string().min(8, {
message: "Password must be at least 8 characters long",
@@ -0,0 +1,28 @@
import { env } from "@/src/env.mjs";
export const sendToBetterstack = async (message: unknown) => {
if (!env.LANGFUSE_TEAM_BETTERSTACK_TOKEN)
throw new Error("LANGFUSE_TEAM_BETTERSTACK_TOKEN is not set");
const url = "https://in.logs.betterstack.com";
const headers = new Headers({
"Content-Type": "application/json",
Authorization: `Bearer ${env.LANGFUSE_TEAM_BETTERSTACK_TOKEN}`,
});
const payload = {
dt: new Date().toISOString(), // Gets the current date in ISO format (UTC)
message: JSON.stringify(message, null, 2),
};
const response = await fetch(url, {
method: "POST",
headers: headers,
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
};
@@ -1,42 +1,28 @@
import { useState, useEffect } from "react";
import { type VisibilityState } from "@tanstack/react-table";
import { type LangfuseColumnDef } from "@/src/components/table/types";
import useLocalStorage from "@/src/components/useLocalStorage";
import { useEffect } from "react";
function useColumnVisibility<TData>(
localStorageKey: string,
columns: LangfuseColumnDef<TData>[],
) {
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
() => {
try {
const savedVisibility = localStorage.getItem(localStorageKey);
const visibilityState: VisibilityState = savedVisibility
? (JSON.parse(savedVisibility) as VisibilityState)
: {};
// set default visibility for columns that are not in the saved state
columns.forEach((column) => {
if (
"accessorKey" in column &&
typeof column.accessorKey === "string"
) {
if (!(column.accessorKey in visibilityState)) {
visibilityState[column.accessorKey] =
column.defaultHidden === true ? false : true;
}
}
});
return visibilityState;
} catch (e) {
console.error("Error while loading saved column visibility", e);
return {};
const initialVisibilityState = () => {
const visibilityState: VisibilityState = {};
columns.forEach((column) => {
if ("accessorKey" in column && typeof column.accessorKey === "string") {
visibilityState[column.accessorKey] =
column.defaultHidden === true ? false : true;
}
},
);
});
return visibilityState;
};
const [columnVisibility, setColumnVisibility] =
useLocalStorage<VisibilityState>(localStorageKey, initialVisibilityState());
useEffect(() => {
const localStorageItem = localStorage.getItem(localStorageKey);
if (!localStorageItem || localStorageItem === "{}") {
if (Object.keys(columnVisibility).length === 0) {
const initialVisibility: VisibilityState = {};
columns.forEach((column) => {
if ("accessorKey" in column && typeof column.accessorKey === "string") {
@@ -45,23 +31,8 @@ function useColumnVisibility<TData>(
}
});
setColumnVisibility(initialVisibility);
} else {
// make sure all columns are in the visibility state
const visibilityState = JSON.parse(localStorageItem) as VisibilityState;
columns.forEach((column) => {
if ("accessorKey" in column && typeof column.accessorKey === "string") {
if (!(column.accessorKey in visibilityState)) {
visibilityState[column.accessorKey] =
column.defaultHidden === true ? false : true;
}
}
});
}
}, [columns, localStorageKey]);
useEffect(() => {
localStorage.setItem(localStorageKey, JSON.stringify(columnVisibility));
}, [columnVisibility, localStorageKey]);
}, [columnVisibility, columns, setColumnVisibility]);
return [columnVisibility, setColumnVisibility] as const;
}
@@ -68,6 +68,8 @@ export function BaseTimeSeriesChart(props: {
noDataText="No data"
showLegend={props.showLegend}
showAnimation={true}
onValueChange={() => {}}
enableLegendSlider={true}
/>
);
}
@@ -16,23 +16,20 @@ export const MetricTable = ({
projectId: string;
globalFilterState: FilterState;
}) => {
const localFilters = globalFilterState.map((f) => ({
...f,
column: "timestamp",
}));
const metrics = api.dashboard.chart.useQuery(
{
projectId,
from: "traces_observations",
from: "observations",
select: [
{ column: "totalTokenCost" },
{ column: "calculatedTotalCost", agg: "SUM" },
{ column: "totalTokens", agg: "SUM" },
{ column: "model" },
],
filter: localFilters,
filter: globalFilterState,
groupBy: [{ type: "string", column: "model" }],
orderBy: [{ column: "totalTokenCost", direction: "DESC" }],
orderBy: [
{ column: "calculatedTotalCost", direction: "DESC", agg: "SUM" },
],
},
{
trpc: {
@@ -43,9 +40,12 @@ export const MetricTable = ({
},
);
const totalTokens = metrics.data?.reduce(
const totalTokenCost = metrics.data?.reduce(
(acc, curr) =>
acc + (curr.totalTokenCost ? (curr.totalTokenCost as number) : 0),
acc +
(curr.sumCalculatedTotalCost
? (curr.sumCalculatedTotalCost as number)
: 0),
0,
);
@@ -60,8 +60,8 @@ export const MetricTable = ({
: "0"}
</RightAlignedCell>,
<RightAlignedCell key={`${i}-cost`}>
{item.totalTokenCost
? usdFormatter(item.totalTokenCost as number, 2, 2)
{item.sumCalculatedTotalCost
? usdFormatter(item.sumCalculatedTotalCost as number, 2, 2)
: "$0"}
</RightAlignedCell>,
])
@@ -83,12 +83,12 @@ export const MetricTable = ({
collapse={{ collapsed: 5, expanded: 20 }}
>
<TotalMetric
metric={totalTokens ? usdFormatter(totalTokens, 2, 2) : "$0"}
metric={totalTokenCost ? usdFormatter(totalTokenCost, 2, 2) : "$0"}
description="Total cost"
>
<DocPopup
description="Calculated multiplying the number of tokens with cost per token for each model."
href="https://langfuse.com/docs/token-usage"
href="https://langfuse.com/docs/model-usage-and-cost"
/>
</TotalMetric>
</DashboardTable>
@@ -36,7 +36,7 @@ export const ModelUsageChart = ({
from: "observations",
select: [
{ column: "totalTokens", agg: "SUM" },
{ column: "totalTokenCost" },
{ column: "calculatedTotalCost", agg: "SUM" },
{ column: "model" },
],
filter: globalFilterState,
@@ -51,7 +51,9 @@ export const ModelUsageChart = ({
column: "model",
},
],
orderBy: [{ column: "totalTokenCost", direction: "DESC" }],
orderBy: [
{ column: "calculatedTotalCost", direction: "DESC", agg: "SUM" },
],
},
{
trpc: {
@@ -78,14 +80,17 @@ export const ModelUsageChart = ({
tokens.data && allModels.length > 0
? fillMissingValuesAndTransform(
extractTimeSeriesData(tokens.data, "startTime", [
{ labelColumn: "model", valueColumn: "totalTokenCost" },
{
labelColumn: "model",
valueColumn: "sumCalculatedTotalCost",
},
]),
allModels,
)
: [];
const totalCost = tokens.data?.reduce(
(acc, curr) => acc + (curr.totalTokenCost as number),
(acc, curr) => acc + (curr.sumCalculatedTotalCost as number),
0,
);
@@ -7,6 +7,7 @@ import { DashboardCard } from "@/src/features/dashboard/components/cards/Dashboa
import { TotalMetric } from "@/src/features/dashboard/components/TotalMetric";
import { BarList } from "@tremor/react";
import { NoData } from "@/src/features/dashboard/components/NoData";
import { compactNumberFormatter } from "@/src/utils/numbers";
export const TracesBarListChart = ({
className,
@@ -80,7 +81,9 @@ export const TracesBarListChart = ({
>
<>
<TotalMetric
metric={totalTraces.data?.[0]?.countTraceId as number}
metric={compactNumberFormatter(
totalTraces.data?.[0]?.countTraceId as number,
)}
description={"Total traces tracked"}
/>
{adjustedData.length > 0 ? (
@@ -32,7 +32,7 @@ export const UserChart = ({
projectId,
from: "traces_observations",
select: [
{ column: "totalTokenCost" },
{ column: "calculatedTotalCost", agg: "SUM" },
{ column: "user" },
{ column: "traceId", agg: "COUNT" },
],
@@ -43,7 +43,9 @@ export const UserChart = ({
column: "user",
},
],
orderBy: [{ column: "totalTokenCost", direction: "DESC" }],
orderBy: [
{ column: "calculatedTotalCost", direction: "DESC", agg: "SUM" },
],
},
{
trpc: {
@@ -97,13 +99,15 @@ export const UserChart = ({
.map((item) => {
return {
name: (item.user as string | null | undefined) ?? "Unknown",
value: item.totalTokenCost ? (item.totalTokenCost as number) : 0,
value: item.sumCalculatedTotalCost
? (item.sumCalculatedTotalCost as number)
: 0,
};
})
: [];
const totalCost = user.data?.reduce(
(acc, curr) => acc + (curr.totalTokenCost as number),
(acc, curr) => acc + (curr.sumCalculatedTotalCost as number),
0,
);
+43 -4
View File
@@ -6,6 +6,7 @@ import {
} from "@/src/server/api/trpc";
import { type DatasetRuns, Prisma, type Dataset } from "@prisma/client";
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
import { auditLog } from "@/src/features/audit-logs/auditLog";
export const datasetRouter = createTRPCRouter({
allDatasets: protectedProjectProcedure
@@ -203,7 +204,7 @@ export const datasetRouter = createTRPCRouter({
projectId: input.projectId,
scope: "datasets:CUD",
});
return ctx.prisma.datasetItem.update({
const datasetItem = await ctx.prisma.datasetItem.update({
where: {
id: input.datasetItemId,
datasetId: input.datasetId,
@@ -226,6 +227,15 @@ export const datasetRouter = createTRPCRouter({
status: input.status,
},
});
await auditLog({
session: ctx.session,
resourceType: "datasetItem",
resourceId: input.datasetItemId,
projectId: input.projectId,
action: "update",
after: datasetItem,
});
return datasetItem;
}),
createDataset: protectedProjectProcedure
.input(z.object({ projectId: z.string(), name: z.string() }))
@@ -235,12 +245,23 @@ export const datasetRouter = createTRPCRouter({
projectId: input.projectId,
scope: "datasets:CUD",
});
return ctx.prisma.dataset.create({
const dataset = await ctx.prisma.dataset.create({
data: {
name: input.name,
projectId: input.projectId,
},
});
await auditLog({
session: ctx.session,
resourceType: "dataset",
resourceId: dataset.id,
projectId: input.projectId,
action: "create",
after: dataset,
});
return dataset;
}),
deleteDataset: protectedProjectProcedure
.input(z.object({ projectId: z.string(), datasetId: z.string() }))
@@ -250,12 +271,21 @@ export const datasetRouter = createTRPCRouter({
projectId: input.projectId,
scope: "datasets:CUD",
});
return ctx.prisma.dataset.delete({
const deletedDataset = await ctx.prisma.dataset.delete({
where: {
id: input.datasetId,
projectId: input.projectId,
},
});
await auditLog({
session: ctx.session,
resourceType: "dataset",
resourceId: deletedDataset.id,
projectId: input.projectId,
action: "delete",
before: deletedDataset,
});
return deletedDataset;
}),
createDatasetItem: protectedProjectProcedure
.input(
@@ -283,7 +313,7 @@ export const datasetRouter = createTRPCRouter({
throw new Error("Dataset not found");
}
return ctx.prisma.datasetItem.create({
const datasetItem = await ctx.prisma.datasetItem.create({
data: {
input: JSON.parse(input.input) as Prisma.InputJsonObject,
expectedOutput:
@@ -296,6 +326,15 @@ export const datasetRouter = createTRPCRouter({
sourceObservationId: input.sourceObservationId,
},
});
await auditLog({
session: ctx.session,
resourceType: "datasetItem",
resourceId: datasetItem.id,
projectId: input.projectId,
action: "create",
after: datasetItem,
});
return datasetItem;
}),
runitemsByRunIdOrItemId: protectedProjectProcedure
.input(
@@ -1,4 +1,3 @@
import * as Sentry from "@sentry/nextjs";
import { Button } from "@/src/components/ui/button";
import {
Dialog,
@@ -18,17 +17,28 @@ import {
FormItem,
FormMessage,
} from "@/src/components/ui/form";
import { type PropsWithChildren, useState } from "react";
import { useState } from "react";
import { useSession } from "next-auth/react";
import { Textarea } from "@/src/components/ui/textarea";
interface FeedbackDialogProps {
className?: string;
children: React.ReactNode;
title: string;
description: string;
type: "feedback" | "dashboard";
}
const formSchema = z.object({
feedback: z.string().min(3, "Must have at least 3 characters"),
});
export function FeedbackButtonWrapper(
props: PropsWithChildren<{ className?: string }>,
) {
export function FeedbackButtonWrapper({
className,
children,
title,
description,
type,
}: FeedbackDialogProps) {
const [open, setOpen] = useState(false);
const session = useSession();
@@ -40,16 +50,6 @@ export function FeedbackButtonWrapper(
});
async function onSubmit(values: z.infer<typeof formSchema>) {
// Add to sentry
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
const eventId = Sentry.captureMessage(`User submitted feedback`);
Sentry.captureUserFeedback({
event_id: eventId,
email: session.data?.user?.email ?? "",
name: session.data?.user?.name ?? "",
comments: values.feedback,
});
}
try {
const res = await fetch("https://cloud.langfuse.com/api/feedback", {
method: "POST",
@@ -57,6 +57,7 @@ export function FeedbackButtonWrapper(
"Content-Type": "application/json",
},
body: JSON.stringify({
type,
...values,
url: window.location.href,
user: session.data?.user,
@@ -84,27 +85,25 @@ export function FeedbackButtonWrapper(
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger className={props.className} asChild>
{props.children}
<DialogTrigger className={className} asChild>
{children}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle className="mb-5">Provide feedback</DialogTitle>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
<Form {...form}>
<form
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-8"
className="space-y-4"
>
<FormField
control={form.control}
name="feedback"
render={({ field }) => (
<FormItem>
<FormDescription>
What do you think about this project? What can be improved?
</FormDescription>
<FormDescription>{description}</FormDescription>
<FormControl>
<Textarea {...field} />
</FormControl>
@@ -1,5 +1,5 @@
import { env } from "@/src/env.mjs";
import { runFeedbackCorsMiddleware } from "@/src/features/feedback/server/corsMiddleware";
import { sendToSlack } from "@/src/features/slack/server/slack-webhook";
import { type NextApiRequest, type NextApiResponse } from "next";
// Collects feedack from users that do not use the cloud version of the app
@@ -10,17 +10,7 @@ export default async function feedbackApiHandler(
await runFeedbackCorsMiddleware(req, res);
try {
if (!env.LANGFUSE_TEAM_SLACK_WEBHOOK)
throw new Error("LANGFUSE_TEAM_SLACK_WEBHOOK is not set");
const slackResponse = await fetch(env.LANGFUSE_TEAM_SLACK_WEBHOOK, {
method: "POST",
body: JSON.stringify({ rawBody: JSON.stringify(req.body, null, 2) }),
headers: {
"Content-Type": "application/json",
},
});
const slackResponse = await sendToSlack(req.body);
if (slackResponse.status === 200) {
res.status(200).json({ status: "OK" });
} else {
@@ -292,18 +292,17 @@ function FilterBuilderForm({
) : filter.type === "number" ||
filter.type === "numberObject" ? (
<Input
value={filter.value?.toString() ?? ""}
value={filter.value ?? undefined}
type="number"
step="0.01"
lang="en-US"
onChange={(e) =>
handleFilterChange(
{
...filter,
value:
isNaN(Number(e.target.value)) ||
e.target.value.endsWith(".")
? e.target.value
: Number(e.target.value),
value: isNaN(Number(e.target.value))
? e.target.value
: Number(e.target.value),
},
i,
)
@@ -58,7 +58,7 @@ export function filterToPrismaSql(
break;
case "number":
case "numberObject":
valuePrisma = Prisma.sql`${filter.value}::DOUBLE PRECISION`;
valuePrisma = Prisma.sql`${filter.value.toString()}::DOUBLE PRECISION`;
break;
case "string":
case "stringObject":
+149 -250
View File
@@ -1,13 +1,54 @@
import { isChatModel, isTiktokenModel } from "@/src/utils/types";
import { countTokens } from "@anthropic-ai/tokenizer";
import { type Model } from "@prisma/client";
import {
type TiktokenEncoding,
get_encoding,
encoding_for_model,
type TiktokenModel,
type Tiktoken,
} from "tiktoken";
import { countTokens } from "@anthropic-ai/tokenizer";
import { type PricingUnit, type Pricing } from "@prisma/client";
import { Decimal } from "decimal.js";
getEncoding,
encodingForModel,
} from "js-tiktoken";
import { z } from "zod";
const OpenAiTokenConfig = z.object({
tokenizerModel: z.string().refine(isTiktokenModel, {
message: "Unknown tiktoken model",
}),
});
const OpenAiChatTokenConfig = z.object({
tokenizerModel: z
.string()
.refine((m) => isTiktokenModel(m) && isChatModel(m), {
message: "Chat model expected",
}),
tokensPerMessage: z.number(),
tokensPerName: z.number(),
});
export function tokenCount(p: {
model: Model;
text: unknown;
}): number | undefined {
if (
p.text === null ||
p.text === undefined ||
(Array.isArray(p.text) && p.text.length === 0)
) {
return undefined;
}
if (p.model.tokenizerId === "openai") {
return openAiTokenCount({
model: p.model,
text: p.text,
});
} else if (p.model.tokenizerId === "claude") {
return claudeTokenCount(p.text);
} else {
console.error(`Unknown tokenizer ${p.model.tokenizerId}`);
return undefined;
}
}
type ChatMessage = {
role: string;
@@ -15,187 +56,115 @@ type ChatMessage = {
content: string;
};
type TokenCalculationParams = {
messages: ChatMessage[];
model: TiktokenModel;
};
export function tokenCount(p: {
model: string;
text: unknown;
}): number | undefined {
const model = cleanModelString(p.model);
if (
p.text === null ||
p.text === undefined ||
(Array.isArray(p.text) && p.text.length === 0)
) {
return undefined;
} else if (isOpenAiModel(model)) {
return isChatMessageArray(p.text)
? openAiChatTokenCount({
model: model,
messages: p.text,
})
: isString(p.text)
? openAiStringTokenCount({ model: model, text: p.text })
: openAiStringTokenCount({
model: model,
text: JSON.stringify(p.text),
});
} else if (isClaudeModel(model)) {
return isString(p.text)
? claudeStringTokenCount({ model: model, text: p.text })
: claudeStringTokenCount({
model: model,
text: JSON.stringify(p.text),
});
} else {
console.log("Unknown model provider", p.model);
function openAiTokenCount(p: { model: Model; text: unknown }) {
const config = OpenAiTokenConfig.safeParse(p.model.tokenizerConfig);
if (!config.success) {
console.error(
`Invalid tokenizer config for model ${p.model.id}: ${JSON.stringify(
p.model.tokenizerConfig,
)}, ${JSON.stringify(config.error)}`,
);
return undefined;
}
let result = undefined;
if (isChatMessageArray(p.text) && isChatModel(config.data.tokenizerModel)) {
// check if the tokenizerConfig is a valid chat config
const parsedConfig = OpenAiChatTokenConfig.safeParse(
p.model.tokenizerConfig,
);
if (!parsedConfig.success) {
console.error(
`Invalid tokenizer config for chat model ${
p.model.id
}: ${JSON.stringify(p.model.tokenizerConfig)}`,
);
return undefined;
}
result = openAiChatTokenCount({
messages: p.text,
config: parsedConfig.data,
});
} else {
result = isString(p.text)
? getTokensByModel(config.data.tokenizerModel, p.text)
: getTokensByModel(config.data.tokenizerModel, JSON.stringify(p.text));
}
return result;
}
function openAiChatTokenCount(params: TokenCalculationParams) {
let encoding: Tiktoken;
try {
encoding = encoding_for_model(params.model);
} catch (KeyError) {
console.log("Warning: model not found. Using cl100k_base encoding.");
encoding = get_encoding("cl100k_base");
}
let tokens_per_message = 0;
let tokens_per_name = 0;
function claudeTokenCount(text: unknown) {
return isString(text) ? countTokens(text) : countTokens(JSON.stringify(text));
}
if (
[
"gpt-3.5-turbo-0613",
"gpt-3.5-turbo-16k-0613",
"gpt-4-0314",
"gpt-4-32k-0314",
"gpt-4-0613",
"gpt-4-32k-0613",
].includes(params.model)
) {
tokens_per_message = 3;
tokens_per_name = 1;
} else if (params.model === "gpt-3.5-turbo-0301") {
tokens_per_message = 4; // every message follows <|start|>{role/name}\n{content}<|end|>\n
tokens_per_name = -1; // if there's a name, the role is omitted
} else if (
params.model.includes("gpt-3.5-turbo") ||
params.model.startsWith("gpt-3.5")
) {
return openAiChatTokenCount({ ...params, model: "gpt-3.5-turbo-0613" });
} else if (params.model.includes("gpt-4")) {
return openAiChatTokenCount({ ...params, model: "gpt-4-0613" });
} else {
console.error(`Not implemented for model ${params.model}`);
throw new Error(`Not implemented for model ${params.model}`);
}
let num_tokens = 0;
function openAiChatTokenCount(params: {
messages: ChatMessage[];
config: z.infer<typeof OpenAiChatTokenConfig>;
}) {
const model = params.config.tokenizerModel;
if (!isTiktokenModel(model)) return undefined;
let numTokens = 0;
params.messages.forEach((message) => {
num_tokens += tokens_per_message;
numTokens += params.config.tokensPerMessage;
Object.keys(message).forEach((key) => {
const value = message[key as keyof typeof message];
if (value) {
num_tokens += encoding.encode(value).length;
if (
// check API docs for available keys: https://platform.openai.com/docs/api-reference/chat/create?lang=node.js
// memory access out of bounds error in tiktoken if unexpected key and value of type boolean
// expected keys with booleans work
value &&
[
"content",
"role",
"name",
"tool_calls",
"function_call",
"toolCalls",
"functionCall",
].some((k) => k === key)
) {
const tokens = getTokensByModel(model, value);
if (tokens) numTokens += tokens;
}
if (key === "name") {
num_tokens += tokens_per_name;
numTokens += params.config.tokensPerName;
}
});
});
num_tokens += 3; // every reply is primed with <| start |> assistant <| message |>
return num_tokens;
numTokens += 3; // every reply is primed with <| start |> assistant <| message |>
return numTokens;
}
const openAiStringTokenCount = (p: { model: string; text: string }) => {
if (
p.model.toLowerCase().startsWith("gpt") ||
p.model.toLowerCase().includes("ada")
) {
return getTokens("cl100k_base", p.text);
const getTokensByModel = (model: TiktokenModel, text: string) => {
// encoiding should be kept in memory to avoid re-creating it
let encoding: Tiktoken | undefined;
try {
cachedTokenizerByModel[model] =
cachedTokenizerByModel[model] || encodingForModel(model);
encoding = cachedTokenizerByModel[model];
} catch (KeyError) {
console.log("Warning: model not found. Using cl100k_base encoding.");
encoding = getEncoding("cl100k_base");
}
if (p.model.toLowerCase().startsWith("text-davinci")) {
return getTokens("p50k_base", p.text);
}
console.log("Unknown model", p.model);
return undefined;
const cleandedText = unicodeToBytesInString(text);
return encoding?.encode(cleandedText).length;
};
const claudeStringTokenCount = (p: { model: string; text: string }) => {
return countTokens(p.text);
};
const getTokens = (name: TiktokenEncoding, text: string) => {
const encoding = get_encoding(name);
const tokens = encoding.encode(text);
encoding.free();
return tokens.length;
};
interface Tokenizer {
[model: string]: Tiktoken;
}
const cachedTokenizerByModel: Tokenizer = {};
function isString(value: unknown): value is string {
return typeof value === "string";
}
function isClaudeModel(model: string) {
return model.toLowerCase().startsWith("claude");
}
function isOpenAiModel(model: string): model is TiktokenModel {
return (
[
"text-davinci-003",
"text-davinci-002",
"text-davinci-001",
"text-curie-001",
"text-babbage-001",
"text-ada-001",
"davinci",
"curie",
"babbage",
"ada",
"code-davinci-002",
"code-davinci-001",
"code-cushman-002",
"code-cushman-001",
"davinci-codex",
"cushman-codex",
"text-davinci-edit-001",
"code-davinci-edit-001",
"text-embedding-ada-002",
"text-similarity-davinci-001",
"text-similarity-curie-001",
"text-similarity-babbage-001",
"text-similarity-ada-001",
"text-search-davinci-doc-001",
"text-search-curie-doc-001",
"text-search-babbage-doc-001",
"text-search-ada-doc-001",
"code-search-babbage-code-001",
"code-search-ada-code-001",
"gpt2",
"gpt-4",
"gpt-4-0314",
"gpt-4-0613",
"gpt-4-32k",
"gpt-4-32k-0314",
"gpt-4-32k-0613",
"gpt-4-1106-preview",
"gpt-4-vision-preview",
"gpt-3.5",
"gpt-3.5-turbo",
"gpt-3.5-turbo-0301",
"gpt-3.5-turbo-0613",
"gpt-3.5-turbo-16k",
"gpt-3.5-turbo-16k-0613",
"gpt-3.5-turbo-1106",
].find((m) => m === model) !== undefined
);
}
function isChatMessageArray(value: unknown): value is ChatMessage[] {
if (!Array.isArray(value)) {
return false;
@@ -213,93 +182,23 @@ function isChatMessageArray(value: unknown): value is ChatMessage[] {
);
}
export function calculateTokenCost(
pricingList: Pricing[],
input: {
model: string;
input: unknown;
output: unknown;
totalTokens: Decimal;
promptTokens: Decimal;
completionTokens: Decimal;
},
): Decimal | undefined {
const model = cleanModelString(input.model);
const pricing = pricingList.filter((p) => p.modelName === model);
if (pricing.length === 0) {
console.log("no pricing found for model", input.model);
return undefined;
} else {
if (pricing.length === 1 && pricing[0]?.tokenType === "TOTAL") {
return calculateValue(
pricing[0].price,
pricing[0].pricingUnit,
input.totalTokens,
JSON.stringify(input.input) + JSON.stringify(input.output),
);
function unicodeToBytesInString(input: string): string {
let result = "";
for (let i = 0; i < input.length; i++) {
const char = input[i];
if (char && /[\u{10000}-\u{10FFFF}]/u.test(char)) {
const bytes = unicodeToBytes(char);
result += Array.from(bytes)
.map((b) => b.toString(16))
.join("");
} else {
result += char;
}
if (pricing.length === 2) {
let promptPrice: Decimal = new Decimal(0);
let completionPrice: Decimal = new Decimal(0);
const promptPricing = pricing.find((p) => p.tokenType === "PROMPT");
const completionPricing = pricing.find(
(p) => p.tokenType === "COMPLETION",
);
if (promptPricing) {
promptPrice =
calculateValue(
promptPricing.price,
promptPricing.pricingUnit,
input.promptTokens,
JSON.stringify(input.input),
) ?? new Decimal(0);
}
if (completionPricing) {
completionPrice =
calculateValue(
completionPricing.price,
completionPricing.pricingUnit,
input.completionTokens,
JSON.stringify(input.output),
) ?? new Decimal(0);
}
return promptPrice.plus(completionPrice);
}
console.log("unknown model", input.model);
return undefined;
}
return result;
}
const calculateValue = (
price: Decimal,
unit: PricingUnit,
tokens: Decimal,
characters: string,
) => {
switch (unit) {
case "PER_1000_TOKENS":
return price
.times(tokens.dividedBy(new Decimal(1000)))
.toDecimalPlaces(5);
case "PER_1000_CHARS":
return (
price
// strip whitespace, default for google bison, only character based model for now
.times(new Decimal(characters.replace(/\s/g, "").length))
.dividedBy(new Decimal(1000))
.toDecimalPlaces(5)
);
default:
console.log("unknown pricing unit", unit);
return undefined;
}
};
const cleanModelString = (model: string) =>
model.toLowerCase().replaceAll("gpt-35", "gpt-3.5");
function unicodeToBytes(input: string): Uint8Array {
const encoder = new TextEncoder();
return encoder.encode(input);
}
@@ -0,0 +1,376 @@
import { usePostHog } from "posthog-js/react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import JsonView from "react18-json-view";
import * as z from "zod";
import { DatePicker } from "@/src/components/date-picker";
import Header from "@/src/components/layouts/header";
import { Button } from "@/src/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/src/components/ui/form";
import { Input } from "@/src/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/src/components/ui/select";
import { ModelUsageUnit } from "@/src/constants";
import { AutoComplete } from "@/src/features/prompts/components/auto-complete";
import { api } from "@/src/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
const formSchema = z.object({
modelName: z.string().min(1),
matchPattern: z.string(),
startDate: z.date().optional(),
inputPrice: z
.string()
.refine((value) => value === "" || isFinite(parseFloat(value)), {
message: "Price needs to be numeric",
})
.optional(),
outputPrice: z
.string()
.refine((value) => value === "" || isFinite(parseFloat(value)), {
message: "Price needs to be numeric",
})
.optional(),
totalPrice: z
.string()
.refine((value) => value === "" || isFinite(parseFloat(value)), {
message: "Price needs to be numeric",
})
.optional(),
unit: z.nativeEnum(ModelUsageUnit),
tokenizerId: z.enum(["openai", "claude", "None"]),
tokenizerConfig: z.string().refine(
(value) => {
try {
JSON.parse(value);
return true;
} catch (e) {
return false;
}
},
{
message: "Tokenizer config needs to be valid JSON",
},
),
});
export const NewModelForm = (props: {
projectId: string;
onFormSuccess?: () => void;
}) => {
const [formError, setFormError] = useState<string | null>(null);
const posthog = usePostHog();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
modelName: "",
matchPattern: "",
startDate: undefined,
inputPrice: "",
outputPrice: "",
totalPrice: "",
unit: ModelUsageUnit.Tokens,
tokenizerId: "None",
tokenizerConfig: "{}",
},
});
const utils = api.useUtils();
const createModelMutation = api.models.create.useMutation({
onSuccess: () => utils.models.invalidate(),
onError: (error) => setFormError(error.message),
});
const modelNames = api.models.modelNames.useQuery({
projectId: props.projectId,
});
function onSubmit(values: z.infer<typeof formSchema>) {
posthog.capture("models:new_model_form_submit");
createModelMutation
.mutateAsync({
projectId: props.projectId,
modelName: values.modelName,
matchPattern: values.matchPattern,
inputPrice: !!values.inputPrice
? parseFloat(values.inputPrice)
: undefined,
outputPrice: !!values.outputPrice
? parseFloat(values.outputPrice)
: undefined,
totalPrice: !!values.totalPrice
? parseFloat(values.totalPrice)
: undefined,
unit: values.unit,
tokenizerId:
values.tokenizerId === "None" ? undefined : values.tokenizerId,
tokenizerConfig:
values.tokenizerConfig &&
typeof JSON.parse(values.tokenizerConfig) === "object"
? (JSON.parse(values.tokenizerConfig) as Record<string, number>)
: undefined,
})
.then(() => {
props.onFormSuccess?.();
form.reset();
})
.catch((error) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if ("message" in error && typeof error.message === "string") {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
setFormError(error.message as string);
return;
} else {
setFormError(JSON.stringify(error));
console.error(error);
}
});
}
return (
<Form {...form}>
<form
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<Header level="h3" title="Name" />
<FormField
control={form.control}
name="modelName"
render={({ field }) => (
<FormItem>
<FormLabel>Model Name</FormLabel>
<FormControl>
<AutoComplete
{...field}
options={
modelNames.data?.map((model) => ({
value: model,
label: model,
})) ?? []
}
placeholder=""
onValueChange={(option) => field.onChange(option.value)}
value={{ value: field.value, label: field.value }}
disabled={false}
createLabel="Create a new model name"
/>
</FormControl>
<FormDescription>
The name of the model. This will be used to reference the model
in the API. You can track price changes of models by using the
same name and match pattern.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Header level="h3" title="Scope" />
<FormField
control={form.control}
name="matchPattern"
render={({ field }) => (
<FormItem>
<FormLabel>Match pattern</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
Regular expression (Postgres syntax) to match ingested
generations (model attribute) to this model definition. For an
exact, case-insensitive match to a model name, use the
expression: (?i)^modelname$
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="startDate"
render={({ field }) => (
<FormItem>
<FormLabel>Start date</FormLabel>
<FormControl>
<DatePicker
date={field.value}
onChange={(date) => field.onChange(date)}
clearable
/>
</FormControl>
<FormDescription>
If set, the model will only be used for generations after this
date.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Header level="h3" title="Pricing" />
<FormField
control={form.control}
name="unit"
render={({ field }) => (
<FormItem>
<FormLabel>Unit</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a unit" />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.values(ModelUsageUnit).map((unit) => (
<SelectItem value={unit} key={unit}>
{unit}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
The unit of measurement for the model.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-3 gap-2">
<FormField
control={form.control}
name="inputPrice"
render={({ field }) => (
<FormItem>
<FormLabel>Input price (USD)</FormLabel>
<FormControl>
<Input {...field} type="number" />
</FormControl>
<FormDescription>Cost per input unit.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="outputPrice"
render={({ field }) => (
<FormItem>
<FormLabel>Output price (USD)</FormLabel>
<FormControl>
<Input {...field} type="number" />
</FormControl>
<FormDescription>Cost per output unit.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="totalPrice"
render={({ field }) => (
<FormItem>
<FormLabel>Total price (USD)</FormLabel>
<FormControl>
<Input {...field} type="number" />
</FormControl>
<FormDescription>
Cost per unit, if no separate input/output prices.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<Header level="h3" title="Tokenization" />
<FormField
control={form.control}
name="tokenizerId"
render={({ field }) => (
<FormItem>
<FormLabel>Tokenizer</FormLabel>
<Select
onValueChange={(tokenizerId) => {
field.onChange(tokenizerId);
if (tokenizerId === "None") {
form.setValue("tokenizerConfig", "{}");
}
}}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a unit" />
</SelectTrigger>
</FormControl>
<SelectContent>
{["openai", "claude", "None"].map((unit) => (
<SelectItem value={unit} key={unit}>
{unit}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
Optionally, Langfuse can tokenize the input and output of a
generation if no unit counts are ingested. This is useful for
e.g. streamed OpenAI completions. For details on the supported
tokenizers, see the docs.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{form.watch("tokenizerId") !== "None" && (
<FormField
control={form.control}
name="tokenizerConfig"
render={({ field }) => (
<FormItem>
<FormLabel>Tokenizer Config</FormLabel>
<JsonView
src={JSON.parse(field.value) as unknown}
onEdit={(edit) => {
field.onChange(JSON.stringify(edit.src));
}}
editable
className="rounded-md border border-gray-200 p-2 text-sm"
/>
<FormDescription>
The config for the tokenizer. Required for openai. See the
docs for details.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<Button
type="submit"
loading={createModelMutation.isLoading}
className="mt-3"
>
Save
</Button>
</form>
{formError ? (
<p className="text-red text-center">
<span className="font-bold">Error:</span> {formError}
</p>
) : null}
</Form>
);
};
@@ -0,0 +1,47 @@
import { X } from "lucide-react";
export interface TNotification {
id: number;
releaseDate: Date;
message: string | JSX.Element;
description?: JSX.Element | string;
}
interface NotificationProps {
notification: TNotification;
setLastSeenId: (id: number) => void;
dismissToast: (t?: string | number | undefined) => void;
toast: string | number;
}
const Notification: React.FC<NotificationProps> = ({
notification,
setLastSeenId,
dismissToast,
toast,
}) => (
<div className="flex justify-between">
<div className="flex min-w-[300px] flex-1 flex-col justify-center">
<div className="m-0 text-sm font-medium leading-tight text-gray-800">
{notification.message}
</div>
{notification.description && (
<div className="mt-2 flex-1 text-sm leading-tight text-gray-800">
{notification.description}
</div>
)}
</div>
<button
className="flex h-6 w-6 cursor-pointer items-start justify-end border-none bg-transparent p-0 text-gray-800 transition-colors duration-200"
onClick={() => {
setLastSeenId(notification.id);
dismissToast(toast);
}}
aria-label="Close"
>
<X size={14} />
</button>
</div>
);
export default Notification;
@@ -0,0 +1,95 @@
import { useEffect } from "react";
import Link from "next/link";
import { toast } from "sonner";
import useLocalStorage from "@/src/components/useLocalStorage";
import Notification, {
type TNotification,
} from "@/src/features/notifications/Notification";
import { Button } from "@/src/components/ui/button";
import { env } from "@/src/env.mjs";
export const NOTIFICATIONS: TNotification[] = [
{
id: 1,
releaseDate: new Date("2024-01-29"),
message: "New: Custom model prices",
description: (
<div>
<p>
Langfuse now supports any LLM model for usage and cost tracking. The
highlights:
</p>
<ul className="ms-4 mt-2 list-outside list-disc">
<li>Define your model definitions (price, usage).</li>
<li>Optionally, ingest cost via the API/SDK</li>
<li>Support for usage in tokens, seconds and characters</li>
</ul>
{env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === undefined && (
<p className="mt-2">
Self-hosted: The upgrade to v2 includes a breaking change. After
upgrading, you must run a migration script to ensure accurate usage
and cost calculation for previously ingested traces. Please refer to
the post for details.
</p>
)}
<Button size="sm" variant="secondary" className="mt-3">
<Link href="https://langfuse.com/changelog/2024-01-29-custom-model-prices">
Changelog post
</Link>
</Button>
</div>
),
},
];
export const useCheckNotification = (
notification: TNotification[],
authenticated: boolean,
) => {
const [lastSeenId, setLastSeenId] = useLocalStorage<number>(
"lastSeenNotificationId",
0,
);
useEffect(() => {
if (!authenticated) {
return;
}
const timeoutId = setTimeout(() => {
notification
.reverse()
.filter(
(n) =>
// only show notifications that are less than 30 days old
(new Date().getTime() - n.releaseDate.getTime()) /
(1000 * 60 * 60 * 24) <=
30,
)
.forEach((n) => {
if (n.id > lastSeenId) {
toast.custom(
(t) => (
<Notification
notification={n}
setLastSeenId={setLastSeenId}
dismissToast={toast.dismiss}
toast={t}
/>
),
{
// needed to upsert toasts in case it is rendered multiple times
id: n.id.toString(),
duration: 600_000, // 10 minutes
style: {
padding: "1rem",
border: "1px solid #e2e8f0",
borderRadius: "0.5rem",
},
},
);
}
});
}, 1500);
return () => clearTimeout(timeoutId);
}, [lastSeenId, notification, setLastSeenId, authenticated]);
};
+13 -2
View File
@@ -1,6 +1,7 @@
import { type OrderByState } from "@/src/features/orderBy/types";
import { type ColumnDefinition } from "@/src/server/api/interfaces/tableDefinition";
import { Prisma } from "@prisma/client";
import { z } from "zod";
/**
* Convert orderBy to SQL ORDER BY clause
@@ -13,7 +14,7 @@ export function orderByToPrismaSql(
tableColumns: ColumnDefinition[],
): Prisma.Sql {
if (!orderBy) {
return Prisma.sql([`ORDER BY t.timestamp DESC`]);
return Prisma.sql`ORDER BY t.timestamp DESC`;
}
// Get column definition to map column to internal name, e.g. "t.id"
const col = tableColumns.find(
@@ -21,10 +22,20 @@ export function orderByToPrismaSql(
// It's less error-prone & decouples data fetching from the human-readable UI labels
(c) => c.name === orderBy.column || c.id === orderBy.column,
);
if (!col) {
console.log("Invalid filter column", orderBy.column);
throw new Error("Invalid filter column: " + orderBy.column);
}
return Prisma.sql([`ORDER BY ${col.internal} ${orderBy.order}`]);
// Assert that orderBy.order is either "asc" or "desc"
const orderByOrder = z.enum(["ASC", "DESC"]);
const order = orderByOrder.safeParse(orderBy.order);
if (!order.success) {
console.log("Invalid order", orderBy.order);
throw new Error("Invalid order: " + orderBy.order);
}
// Both column and order are safe, can use raw SQL
return Prisma.raw(`ORDER BY ${col.internal} ${order.data}`);
}

Some files were not shown because too many files have changed in this diff Show More