Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2c792d3dc | ||
|
|
0527fcf097 | ||
|
|
85737221d9 | ||
|
|
ffe95cf427 | ||
|
|
adec1476ea | ||
|
|
c5d7772fe6 | ||
|
|
a88a6b8026 | ||
|
|
5d77e09eed | ||
|
|
85e5ea4661 | ||
|
|
366df5a5c9 | ||
|
|
4441383dfd | ||
|
|
7f7696e9ff | ||
|
|
a829447410 | ||
|
|
53e7ff4e16 | ||
|
|
4a85b4a4f2 | ||
|
|
d2e6dca3d3 | ||
|
|
cdbdc0af67 | ||
|
|
2af9b52213 | ||
|
|
894a3d1b32 | ||
|
|
82f0134368 | ||
|
|
e52ba46b0e | ||
|
|
6aca4954ef | ||
|
|
9d5fa4e2f1 | ||
|
|
c1db242edd | ||
|
|
91a2c63c88 | ||
|
|
f7d9bd7297 | ||
|
|
d94e880520 | ||
|
|
74e9053f85 | ||
|
|
ae5260d808 | ||
|
|
f72f8b7e2c | ||
|
|
43108eccde | ||
|
|
c93c9560e1 | ||
|
|
ddad1afd7b | ||
|
|
2b70ae3742 | ||
|
|
b110fd7932 |
@@ -16,8 +16,17 @@ NEXTAUTH_SECRET="secret"
|
||||
|
||||
# Langfuse experimental features
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
|
||||
|
||||
# Salt for API key hashing
|
||||
SALT="salt"
|
||||
|
||||
# Email
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
# S3 storage
|
||||
S3_ENDPOINT=
|
||||
S3_ACCESS_KEY_ID=
|
||||
S3_SECRET_ACCESS_KEY=
|
||||
S3_BUCKET_NAME=
|
||||
S3_REGION=
|
||||
+1
-8
@@ -1,21 +1,14 @@
|
||||
# When adding additional environment variables, the schema in "/src/env.mjs"
|
||||
# should be updated accordingly.
|
||||
|
||||
# Prisma
|
||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
||||
DIRECT_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
DATABASE_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
# Next Auth
|
||||
# You can generate a new secret on the command line with:
|
||||
# openssl rand -base64 32
|
||||
# https://next-auth.js.org/configuration/options#secret
|
||||
NEXTAUTH_SECRET="secret"
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# feature flag to enable experimental features locally
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
|
||||
SALT="salt"
|
||||
|
||||
|
||||
# Email
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
+66
-34
@@ -1,32 +1,72 @@
|
||||
# Assuming deployment on Vercel with Postgres database on Supabase
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="US"
|
||||
NEXTAUTH_COOKIE_DOMAIN=".langfuse.com"
|
||||
# More information: https://langfuse.com/docs/deployment/self-host
|
||||
|
||||
# When adding additional environment variables, the schema in "/src/env.mjs"
|
||||
# should be updated accordingly.
|
||||
|
||||
# Prisma
|
||||
|
||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
||||
DIRECT_URL="postgresql://postgres:[pw]@db.[db_id].supabase.co:5432/postgres"
|
||||
DATABASE_URL="postgres://postgres:[pw]@db.[db_id].supabase.co:6543/postgres?pgbouncer=true&connection_limit=1"
|
||||
# DATABASE_URL supports pooled connections, but then you need to set DIRECT_URL
|
||||
DATABASE_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
# DIRECT_URL="postgresql://postgres:postgres@db:5432/postgres"
|
||||
# SHADOW_DATABASE_URL=
|
||||
|
||||
# Next Auth
|
||||
|
||||
# NEXTAUTH_URL does not need to be set when deploying on Vercel
|
||||
# NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# AUTH_REDIRECT_PROXY_URL used to proxy oauth callbacks on e.g. preview deployments. optional.
|
||||
# AUTH_REDIRECT_PROXY_URL="https://example.com/api/auth"
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
|
||||
# You can generate a new secret on the command line with:
|
||||
# openssl rand -base64 32
|
||||
# https://next-auth.js.org/configuration/options#secret
|
||||
NEXTAUTH_SECRET="secret"
|
||||
SALT="salt"
|
||||
# Sentry; set via Vercel integration
|
||||
# NEXT_PUBLIC_SENTRY_DSN=
|
||||
# NEXT_SENTRY_ORG=
|
||||
# NEXT_SENTRY_PROJECT=
|
||||
# SENTRY_AUTH_TOKEN=
|
||||
|
||||
# Docker only, optional
|
||||
# PORT=3000
|
||||
# HOSTNAME=localhost
|
||||
|
||||
# Default project, optional
|
||||
# LANGFUSE_DEFAULT_PROJECT_ID=
|
||||
# LANGFUSE_DEFAULT_PROJECT_ROLE=
|
||||
|
||||
# Enable experimental features, optional
|
||||
# NEXT_PUBLIC_ENABLE_EXPERIMENTAL_FEATURES
|
||||
|
||||
# Auth, optional configuration
|
||||
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
|
||||
# AUTH_DISABLE_USERNAME_PASSWORD=true
|
||||
|
||||
# SSO, each group is optional
|
||||
# AUTH_GOOGLE_CLIENT_ID=
|
||||
# AUTH_GOOGLE_CLIENT_SECRET=
|
||||
# AUTH_GITHUB_CLIENT_ID=
|
||||
# AUTH_GITHUB_CLIENT_SECRET=
|
||||
# AUTH_AZURE_AD_CLIENT_ID=
|
||||
# AUTH_AZURE_AD_CLIENT_SECRET=
|
||||
# AUTH_AZURE_AD_TENANT_ID=
|
||||
|
||||
# Transactional email, optional
|
||||
# Defines the email address to use as the from address.
|
||||
# EMAIL_FROM_ADDRESS=
|
||||
# Defines the connection url for smtp server.
|
||||
# SMTP_CONNECTION_URL=
|
||||
|
||||
# S3 storage, optional, used for exports from the UI
|
||||
# S3_ENDPOINT=
|
||||
# S3_ACCESS_KEY_ID=
|
||||
# S3_SECRET_ACCESS_KEY=
|
||||
# S3_BUCKET_NAME=
|
||||
# S3_REGION=
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### START Langfuse Cloud Config
|
||||
# Used for Langfuse Cloud deployments
|
||||
# Not recommended for self-hosted deployments as these are NOT COVERED BY SEMVER
|
||||
|
||||
# NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="US"
|
||||
# NEXTAUTH_COOKIE_DOMAIN=".langfuse.com"
|
||||
|
||||
# LANGFUSE_TEAM_SLACK_WEBHOOK=
|
||||
# LANGFUSE_NEW_USER_SIGNUP_WEBHOOK=
|
||||
@@ -35,24 +75,16 @@ SALT="salt"
|
||||
# NEXT_PUBLIC_POSTHOG_HOST=
|
||||
# NEXT_PUBLIC_POSTHOG_KEY=
|
||||
|
||||
# Id of demo project to automatically assign new users to
|
||||
# Sentry
|
||||
# NEXT_PUBLIC_SENTRY_DSN=
|
||||
# NEXT_SENTRY_ORG=
|
||||
# NEXT_SENTRY_PROJECT=
|
||||
# SENTRY_AUTH_TOKEN=
|
||||
|
||||
# Demo project that users can use to try the platform
|
||||
# NEXT_PUBLIC_DEMO_PROJECT_ID=
|
||||
|
||||
# Auth, each group is optional
|
||||
AUTH_GOOGLE_CLIENT_ID=
|
||||
AUTH_GOOGLE_CLIENT_SECRET=
|
||||
AUTH_GITHUB_CLIENT_ID=
|
||||
AUTH_GITHUB_CLIENT_SECRET=
|
||||
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
|
||||
# AUTH_DISABLE_USERNAME_PASSWORD=true
|
||||
# Crisp chat
|
||||
# NEXT_PUBLIC_CRISP_WEBSITE_ID=
|
||||
|
||||
# Email
|
||||
EMAIL_FROM_ADDRESS="" # Defines the email address to use as the from address.
|
||||
SMTP_CONNECTION_URL="" # Defines the connection url for smtp server.
|
||||
|
||||
# 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
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature or potential improvement
|
||||
description: Please describe the change as clear and concise as possible. Remember to add context as to why you believe this is needed.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Add any other information related to the change here. If your idea is related to any issues or discussions, link them here.
|
||||
- type: checkboxes
|
||||
id: contribute
|
||||
attributes:
|
||||
label: Contribute
|
||||
description: Are you willing to contribute to the implementation of this idea?
|
||||
options:
|
||||
- label: Yes, I can implement this and raise a PR
|
||||
+12
-4
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
[](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 |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| [](https://pypi.python.org/pypi/langfuse) | Python | [docs](https://langfuse.com/docs/integrations/sdk/python), [repo](https://github.com/langfuse/langfuse-python) |
|
||||
| [](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) |
|
||||
| [](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:
|
||||
|
||||
|
||||
@@ -132,6 +132,9 @@ types:
|
||||
enum:
|
||||
- CHARACTERS
|
||||
- TOKENS
|
||||
- MILLISECONDS
|
||||
- SECONDS
|
||||
- IMAGES
|
||||
ObservationLevel:
|
||||
enum:
|
||||
- DEBUG
|
||||
|
||||
@@ -1311,6 +1311,9 @@ components:
|
||||
enum:
|
||||
- CHARACTERS
|
||||
- TOKENS
|
||||
- MILLISECONDS
|
||||
- SECONDS
|
||||
- IMAGES
|
||||
ObservationLevel:
|
||||
title: ObservationLevel
|
||||
type: string
|
||||
|
||||
Generated
+443
-290
File diff suppressed because it is too large
Load Diff
+18
-15
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse-core",
|
||||
"version": "2.1.2",
|
||||
"version": "2.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"prebuild": "cp generated/openapi-client/openapi.yml public/openapi-client.yml && cp generated/openapi-server/openapi.yml public/openapi-server.yml",
|
||||
@@ -16,6 +16,8 @@
|
||||
"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",
|
||||
@@ -28,13 +30,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/tokenizer": "^0.0.4",
|
||||
"@aws-sdk/client-s3": "^3.501.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.501.0",
|
||||
"@aws-sdk/client-s3": "^3.507.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.507.0",
|
||||
"@headlessui/react": "^1.7.18",
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^5.8.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",
|
||||
@@ -63,7 +65,7 @@
|
||||
"@t3-oss/env-nextjs": "^0.8.0",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.11.7",
|
||||
"@tanstack/react-table": "^8.11.8",
|
||||
"@tremor/react": "^3.11.1",
|
||||
"@trpc/client": "^10.45.0",
|
||||
"@trpc/next": "^10.45.0",
|
||||
@@ -81,20 +83,21 @@
|
||||
"exponential-backoff": "^3.1.1",
|
||||
"js-tiktoken": "^1.0.10",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.321.0",
|
||||
"lucide-react": "^0.323.0",
|
||||
"next": "^14.1.0",
|
||||
"next-auth": "^4.24.5",
|
||||
"next-query-params": "^5.0.0",
|
||||
"nodemailer": "^6.9.9",
|
||||
"posthog-js": "^1.103.1",
|
||||
"posthog-node": "^3.6.1",
|
||||
"posthog-js": "^1.104.4",
|
||||
"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.3",
|
||||
"react-hook-form": "^7.50.1",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react18-json-view": "^0.2.7",
|
||||
"sonner": "^1.4.0",
|
||||
"superjson": "2.2.1",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
@@ -107,7 +110,7 @@
|
||||
"@mermaid-js/mermaid-cli": "^10.7.0",
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@release-it/bumper": "^6.0.1",
|
||||
"@testing-library/jest-dom": "^6.4.1",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/react": "^14.2.1",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
@@ -116,21 +119,21 @@
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "20.10.5",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/react": "^18.2.51",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.20.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"postcss": "^8.4.33",
|
||||
"prettier": "^3.2.4",
|
||||
"postcss": "^8.4.34",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"prisma": "^5.8.1",
|
||||
"prisma": "^5.9.1",
|
||||
"prisma-erd-generator": "^1.11.2",
|
||||
"release-it": "^17.0.3",
|
||||
"tailwindcss": "^3.4.1",
|
||||
|
||||
@@ -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" }')
|
||||
@@ -440,7 +440,7 @@ model Model {
|
||||
inputPrice Decimal? @map("input_price")
|
||||
outputPrice Decimal? @map("output_price")
|
||||
totalPrice Decimal? @map("total_price")
|
||||
unit String // TOKENS or CHARACTERS
|
||||
unit String // TOKENS, CHARACTERS, MILLISECONDS, SECONDS, or IMAGES
|
||||
tokenizerId String? @map("tokenizer_id")
|
||||
tokenizerConfig Json? @map("tokenizer_config")
|
||||
|
||||
|
||||
+2
-1
@@ -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" },
|
||||
@@ -487,7 +488,7 @@ async function main() {
|
||||
? { prompt: { connect: { id: prompt.id } } }
|
||||
: {}),
|
||||
},
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
if (Math.random() > 0.6)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** @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", () => {
|
||||
@@ -111,7 +112,7 @@ describe("cost retrieval tests", () => {
|
||||
projectId: null,
|
||||
startDate: new Date("2023-12-01"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -130,7 +131,7 @@ describe("cost retrieval tests", () => {
|
||||
model: "gpt-3.5-turbo",
|
||||
internalModel: "gpt-3.5-turbo",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
promptTokens: input.promptTokens,
|
||||
completionTokens: input.completionTokens,
|
||||
totalTokens: input.totalTokens,
|
||||
@@ -181,7 +182,7 @@ describe("cost retrieval tests", () => {
|
||||
projectId: null,
|
||||
startDate: new Date("2023-12-01"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
await prisma.model.create({
|
||||
@@ -194,7 +195,7 @@ describe("cost retrieval tests", () => {
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
startDate: new Date("2023-12-01"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
},
|
||||
});
|
||||
@@ -214,7 +215,7 @@ describe("cost retrieval tests", () => {
|
||||
model: "gpt-3.5-turbo",
|
||||
internalModel: "gpt-3.5-turbo",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
promptTokens: 200,
|
||||
completionTokens: 3000,
|
||||
totalTokens: undefined,
|
||||
@@ -252,7 +253,7 @@ describe("cost retrieval tests", () => {
|
||||
projectId: null,
|
||||
startDate: new Date("2023-12-01"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
await prisma.model.create({
|
||||
@@ -265,7 +266,7 @@ describe("cost retrieval tests", () => {
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
startDate: new Date("2023-12-02"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -284,7 +285,7 @@ describe("cost retrieval tests", () => {
|
||||
model: "gpt-3.5-turbo",
|
||||
internalModel: "gpt-3.5-turbo",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
promptTokens: 200,
|
||||
completionTokens: 3000,
|
||||
totalTokens: undefined,
|
||||
@@ -317,7 +318,7 @@ describe("cost retrieval tests", () => {
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
startDate: new Date("2023-12-02"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
await prisma.model.create({
|
||||
@@ -331,7 +332,7 @@ describe("cost retrieval tests", () => {
|
||||
startDate: new Date("2023-12-01"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -350,7 +351,7 @@ describe("cost retrieval tests", () => {
|
||||
model: "gpt-3.5-turbo",
|
||||
internalModel: "gpt-3.5-turbo",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
promptTokens: 200,
|
||||
completionTokens: 3000,
|
||||
totalTokens: undefined,
|
||||
@@ -383,7 +384,7 @@ describe("cost retrieval tests", () => {
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
startDate: new Date("2023-12-02"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
await prisma.model.create({
|
||||
@@ -397,7 +398,7 @@ describe("cost retrieval tests", () => {
|
||||
startDate: new Date("2023-12-01"),
|
||||
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
|
||||
project: { connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" } },
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -416,7 +417,7 @@ describe("cost retrieval tests", () => {
|
||||
model: "gpt-3.5-turbo",
|
||||
internalModel: "gpt-3.5-turbo",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
promptTokens: 200,
|
||||
completionTokens: 3000,
|
||||
totalTokens: undefined,
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
@@ -48,7 +50,7 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
expectedPromptTokens: 100,
|
||||
expectedCompletionTokens: 200,
|
||||
expectedTotalTokens: 100,
|
||||
expectedUnit: "TOKENS",
|
||||
expectedUnit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
@@ -57,7 +59,7 @@ describe("/api/public/generations API Endpoint", () => {
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
expectedUnit: "TOKENS",
|
||||
expectedUnit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
{
|
||||
usage: undefined,
|
||||
|
||||
@@ -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,12 +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,
|
||||
@@ -27,13 +29,45 @@ 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,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
total: 100,
|
||||
unit: ModelUsageUnit.Milliseconds,
|
||||
},
|
||||
expectedUnit: ModelUsageUnit.Milliseconds,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
input: 1,
|
||||
output: 2,
|
||||
unit: ModelUsageUnit.Images,
|
||||
},
|
||||
expectedUnit: ModelUsageUnit.Images,
|
||||
expectedPromptTokens: 1,
|
||||
expectedCompletionTokens: 2,
|
||||
expectedTotalTokens: 3,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
input: 30,
|
||||
output: 10,
|
||||
unit: ModelUsageUnit.Seconds,
|
||||
},
|
||||
expectedUnit: ModelUsageUnit.Seconds,
|
||||
expectedPromptTokens: 30,
|
||||
expectedCompletionTokens: 10,
|
||||
expectedTotalTokens: 40,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
total: 100,
|
||||
@@ -52,7 +86,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expectedPromptTokens: 100,
|
||||
expectedCompletionTokens: 200,
|
||||
expectedTotalTokens: 100,
|
||||
expectedUnit: "TOKENS",
|
||||
expectedUnit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
{
|
||||
usage: {
|
||||
@@ -61,7 +95,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
expectedTotalTokens: 100,
|
||||
expectedUnit: "TOKENS",
|
||||
expectedUnit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
{
|
||||
usage: undefined,
|
||||
@@ -256,7 +290,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
{
|
||||
observationExternalModel: "gpt-3.5",
|
||||
observationStartTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
modelUnit: "TOKENS",
|
||||
modelUnit: ModelUsageUnit.Tokens,
|
||||
expectedInternalModel: "gpt-3.5-turbo",
|
||||
expectedPromptTokens: 5,
|
||||
expectedCompletionTokens: 7,
|
||||
@@ -265,7 +299,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
modelName: "gpt-3.5-turbo",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
],
|
||||
@@ -273,7 +307,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
{
|
||||
observationExternalModel: "gpt-3.5",
|
||||
observationStartTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
modelUnit: "TOKENS",
|
||||
modelUnit: ModelUsageUnit.Tokens,
|
||||
expectedInternalModel: "gpt-3.5-turbo",
|
||||
expectedPromptTokens: 5,
|
||||
expectedCompletionTokens: 7,
|
||||
@@ -282,7 +316,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
modelName: "gpt-3.5-turbo",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: null,
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
],
|
||||
@@ -290,7 +324,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
{
|
||||
observationExternalModel: "GPT-3.5",
|
||||
observationStartTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
modelUnit: "TOKENS",
|
||||
modelUnit: ModelUsageUnit.Tokens,
|
||||
expectedInternalModel: "gpt-3.5-turbo",
|
||||
expectedPromptTokens: 5,
|
||||
expectedCompletionTokens: 7,
|
||||
@@ -299,7 +333,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
modelName: "gpt-3.5-turbo",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
],
|
||||
@@ -307,7 +341,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
{
|
||||
observationExternalModel: "GPT-3.5",
|
||||
observationStartTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
modelUnit: "TOKENS",
|
||||
modelUnit: ModelUsageUnit.Tokens,
|
||||
expectedInternalModel: "gpt-3.5-turbo",
|
||||
expectedPromptTokens: 5,
|
||||
expectedCompletionTokens: 7,
|
||||
@@ -316,14 +350,14 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
modelName: "gpt-3.5-turbo",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
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: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
],
|
||||
@@ -331,7 +365,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
{
|
||||
observationExternalModel: "GPT-3.5",
|
||||
observationStartTime: new Date("2021-01-02T00:00:00.000Z"),
|
||||
modelUnit: "TOKENS",
|
||||
modelUnit: ModelUsageUnit.Tokens,
|
||||
expectedInternalModel: "gpt-3.5-turbo",
|
||||
expectedPromptTokens: 5,
|
||||
expectedCompletionTokens: 7,
|
||||
@@ -340,14 +374,14 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
modelName: "gpt-3.5-turbo-new",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
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: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
tokenizerModel: "gpt-3.5-turbo",
|
||||
},
|
||||
@@ -356,7 +390,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
{
|
||||
observationExternalModel: "ft:gpt-3.5-turbo-1106:my-org:custom_suffix:id",
|
||||
observationStartTime: new Date("2022-01-01T10:00:00.000Z"),
|
||||
modelUnit: "TOKENS",
|
||||
modelUnit: ModelUsageUnit.Tokens,
|
||||
expectedInternalModel: "ft:gpt-3.5-turbo-1106",
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
@@ -365,7 +399,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
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: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
],
|
||||
@@ -373,7 +407,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
{
|
||||
observationExternalModel: "ft:babbage-002:my-org#2:custom_suffix-2:id",
|
||||
observationStartTime: new Date("2022-01-01T10:00:00.000Z"),
|
||||
modelUnit: "TOKENS",
|
||||
modelUnit: ModelUsageUnit.Tokens,
|
||||
expectedInternalModel: "ft:babbage-002",
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
@@ -382,7 +416,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
modelName: "ft:babbage-002",
|
||||
matchPattern: "(?i)^(ft:)(babbage-002:)(.+)(:)(.*)(:)(.+)$",
|
||||
startDate: new Date("2022-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
],
|
||||
@@ -390,7 +424,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
{
|
||||
observationExternalModel: "GPT-4",
|
||||
observationStartTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
modelUnit: "TOKENS",
|
||||
modelUnit: ModelUsageUnit.Tokens,
|
||||
expectedInternalModel: null,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
@@ -399,7 +433,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
modelName: "gpt-3.5-turbo",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
],
|
||||
@@ -407,7 +441,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
{
|
||||
observationExternalModel: "GPT-3",
|
||||
observationStartTime: new Date("2021-01-01T00:00:00.000Z"),
|
||||
modelUnit: "CHARACTERS",
|
||||
modelUnit: ModelUsageUnit.Characters,
|
||||
expectedInternalModel: null,
|
||||
expectedPromptTokens: 0,
|
||||
expectedCompletionTokens: 0,
|
||||
@@ -416,7 +450,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
modelName: "gpt-3.5-turbo",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
},
|
||||
],
|
||||
@@ -761,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: {
|
||||
@@ -813,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: {
|
||||
@@ -863,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: {
|
||||
@@ -886,7 +920,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
modelName: "gpt-3.5",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
tokenizerConfig: {
|
||||
tokensPerMessage: 3,
|
||||
@@ -968,7 +1002,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
modelName: "gpt-3.5",
|
||||
matchPattern: "(?i)^(gpt-)(35|3.5)(-turbo)?$",
|
||||
startDate: new Date("2021-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "openai",
|
||||
tokenizerConfig: {
|
||||
tokensPerMessage: 3,
|
||||
@@ -1380,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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
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", () => {
|
||||
@@ -17,7 +18,7 @@ describe("model match", () => {
|
||||
totalPrice: "0.1",
|
||||
matchPattern: "(.*)(gpt-3.5-turbo)?(.*)",
|
||||
projectId: null,
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerConfig: {
|
||||
tokensPerMessage: 3,
|
||||
tokensPerName: 1,
|
||||
@@ -35,7 +36,7 @@ describe("model match", () => {
|
||||
totalPrice: "0.1",
|
||||
matchPattern: "(.*)(claude-1.3)?(.*)",
|
||||
projectId: null,
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "claude",
|
||||
},
|
||||
});
|
||||
@@ -48,7 +49,7 @@ describe("model match", () => {
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
model: "gpt-3.5-turbo",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
promptTokens: 200,
|
||||
completionTokens: 3000,
|
||||
input: "I am a prompt",
|
||||
@@ -59,7 +60,7 @@ describe("model match", () => {
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
model: "claude-1.3",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
input: "I am a prompt",
|
||||
output: "I am a completion",
|
||||
},
|
||||
@@ -68,7 +69,7 @@ describe("model match", () => {
|
||||
projectId: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a",
|
||||
model: "claude-1.3",
|
||||
startTime: new Date("2024-01-01T00:00:00.000Z"),
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
input: "I am a prompt",
|
||||
output: "I am a completion",
|
||||
},
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/** @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";
|
||||
import { type ObservationView } from "@prisma/client";
|
||||
|
||||
describe("/api/public/observations API Endpoint", () => {
|
||||
@@ -35,7 +37,7 @@ describe("/api/public/observations API Endpoint", () => {
|
||||
totalPrice: "0.1",
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
projectId: null,
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -59,7 +61,7 @@ describe("/api/public/observations API Endpoint", () => {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
},
|
||||
internalModel: "gpt-3.5-turbo",
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -120,7 +122,7 @@ describe("/api/public/observations API Endpoint", () => {
|
||||
totalPrice: "0.1",
|
||||
matchPattern: "(.*)(gpt-)(35|3.5)(-turbo)?(.*)",
|
||||
projectId: null,
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -140,7 +142,7 @@ describe("/api/public/observations API Endpoint", () => {
|
||||
completionTokens: 20,
|
||||
totalTokens: 30,
|
||||
version: "2.0.0",
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
type: "GENERATION",
|
||||
project: {
|
||||
connect: { id: "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a" },
|
||||
|
||||
@@ -27,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" &&
|
||||
@@ -50,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 () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ModelUsageUnit } from "@/src/constants";
|
||||
import { tokenCount } from "@/src/features/ingest/lib/usage";
|
||||
|
||||
describe("Token Count Functions", () => {
|
||||
@@ -19,7 +20,7 @@ describe("Token Count Functions", () => {
|
||||
inputPrice: null,
|
||||
outputPrice: null,
|
||||
totalPrice: null,
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
|
||||
export const LangfuseIcon = ({
|
||||
size = 32,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -503,6 +512,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
</div>
|
||||
) : null}
|
||||
<main className="p-4">{props.children}</main>
|
||||
<Toaster visibleToasts={1} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -522,29 +532,11 @@ const MainNavigation: React.FC<{
|
||||
nav: NavigationItem[];
|
||||
onNavitemClick?: () => void;
|
||||
}> = ({ nav, onNavitemClick }) => {
|
||||
const STORAGE_KEY = "sidebar-tracing-default-open";
|
||||
const getDefaultOpen = () => {
|
||||
const savedState = localStorage.getItem(STORAGE_KEY);
|
||||
if (savedState !== null) {
|
||||
try {
|
||||
return JSON.parse(savedState) as boolean;
|
||||
} catch (e) {
|
||||
console.error("Error parsing saved state: ", e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const [isOpen, setIsOpen] = useLocalStorage(
|
||||
"sidebar-tracing-default-open",
|
||||
false,
|
||||
);
|
||||
|
||||
const handleDropDownClick = () => {
|
||||
const savedState = localStorage.getItem(STORAGE_KEY);
|
||||
const isOpen =
|
||||
savedState !== null ? (JSON.parse(savedState) as boolean) : false;
|
||||
const newState = !isOpen;
|
||||
localStorage.setItem(
|
||||
"sidebar-tracing-default-open",
|
||||
JSON.stringify(newState),
|
||||
);
|
||||
};
|
||||
return (
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
@@ -590,15 +582,14 @@ const MainNavigation: React.FC<{
|
||||
<Disclosure
|
||||
as="div"
|
||||
defaultOpen={
|
||||
item.children.some((child) => child.current) ||
|
||||
getDefaultOpen()
|
||||
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={handleDropDownClick}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{item.icon && (
|
||||
<item.icon
|
||||
@@ -656,7 +647,12 @@ const MainNavigation: React.FC<{
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
<FeedbackButtonWrapper className="w-full">
|
||||
<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"
|
||||
|
||||
@@ -31,11 +31,11 @@ const modelConfigDescriptions = {
|
||||
"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 unit of input",
|
||||
outputPrice: "Price per unit of output",
|
||||
inputPrice: "Price per 1000 units of input",
|
||||
outputPrice: "Price per 1000 units of output",
|
||||
totalPrice:
|
||||
"Price per unit, for models that don't have input/output specific prices",
|
||||
unit: "Unit of measurement for model, can be TOKENS or CHARACTERS.",
|
||||
"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:
|
||||
@@ -107,7 +107,14 @@ export default function ModelTable({ projectId }: { projectId: string }) {
|
||||
{
|
||||
accessorKey: "inputPrice",
|
||||
id: "inputPrice",
|
||||
header: "Input Price",
|
||||
header: () => {
|
||||
return (
|
||||
<>
|
||||
Input Price{" "}
|
||||
<span className="text-xs text-gray-400">/ 1k units</span>
|
||||
</>
|
||||
);
|
||||
},
|
||||
headerTooltip: {
|
||||
description: modelConfigDescriptions.inputPrice,
|
||||
},
|
||||
@@ -116,7 +123,7 @@ export default function ModelTable({ projectId }: { projectId: string }) {
|
||||
|
||||
return value ? (
|
||||
<span className="text-xs">
|
||||
{usdFormatter(value.toNumber(), 2, 8)}
|
||||
{usdFormatter(value.toNumber() * 1000, 2, 8)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs">-</span>
|
||||
@@ -129,13 +136,20 @@ export default function ModelTable({ projectId }: { projectId: string }) {
|
||||
headerTooltip: {
|
||||
description: modelConfigDescriptions.outputPrice,
|
||||
},
|
||||
header: "Output Price",
|
||||
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(), 2, 8)}
|
||||
{usdFormatter(value.toNumber() * 1000, 2, 8)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs">-</span>
|
||||
@@ -145,7 +159,14 @@ export default function ModelTable({ projectId }: { projectId: string }) {
|
||||
{
|
||||
accessorKey: "totalPrice",
|
||||
id: "totalPrice",
|
||||
header: "Total Price",
|
||||
header: () => {
|
||||
return (
|
||||
<>
|
||||
Total Price{" "}
|
||||
<span className="text-xs text-gray-400">/ 1k units</span>
|
||||
</>
|
||||
);
|
||||
},
|
||||
headerTooltip: {
|
||||
description: modelConfigDescriptions.totalPrice,
|
||||
},
|
||||
@@ -154,7 +175,7 @@ export default function ModelTable({ projectId }: { projectId: string }) {
|
||||
|
||||
return value ? (
|
||||
<span className="text-xs">
|
||||
{usdFormatter(value.toNumber(), 2, 8)}
|
||||
{usdFormatter(value.toNumber() * 1000, 2, 8)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs">-</span>
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 @@
|
||||
export const VERSION = "v2.1.2";
|
||||
export const VERSION = "v2.3.0";
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export { VERSION } from "./VERSION";
|
||||
|
||||
export enum ModelUsageUnit {
|
||||
Characters = "CHARACTERS",
|
||||
Tokens = "TOKENS",
|
||||
Seconds = "SECONDS",
|
||||
Milliseconds = "MILLISECONDS",
|
||||
Images = "IMAGES",
|
||||
}
|
||||
@@ -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(),
|
||||
@@ -91,6 +92,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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,12 +85,12 @@ 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 className="mb-5">{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -102,9 +103,7 @@ export function FeedbackButtonWrapper(
|
||||
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);
|
||||
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":
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import * as z from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
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,
|
||||
@@ -11,6 +16,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/src/components/ui/form";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -18,14 +24,10 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/src/components/ui/select";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { useState } from "react";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { Input } from "@/src/components/ui/input";
|
||||
import { DatePicker } from "@/src/components/date-picker";
|
||||
import Header from "@/src/components/layouts/header";
|
||||
import { ModelUsageUnit } from "@/src/constants";
|
||||
import { AutoComplete } from "@/src/features/prompts/components/auto-complete";
|
||||
import JsonView from "react18-json-view";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
const formSchema = z.object({
|
||||
modelName: z.string().min(1),
|
||||
@@ -49,7 +51,7 @@ const formSchema = z.object({
|
||||
message: "Price needs to be numeric",
|
||||
})
|
||||
.optional(),
|
||||
unit: z.enum(["TOKENS", "CHARACTERS"]),
|
||||
unit: z.nativeEnum(ModelUsageUnit),
|
||||
tokenizerId: z.enum(["openai", "claude", "None"]),
|
||||
tokenizerConfig: z.string().refine(
|
||||
(value) => {
|
||||
@@ -81,7 +83,7 @@ export const NewModelForm = (props: {
|
||||
inputPrice: "",
|
||||
outputPrice: "",
|
||||
totalPrice: "",
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
tokenizerId: "None",
|
||||
tokenizerConfig: "{}",
|
||||
},
|
||||
@@ -233,7 +235,7 @@ export const NewModelForm = (props: {
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{["TOKENS", "CHARACTERS"].map((unit) => (
|
||||
{Object.values(ModelUsageUnit).map((unit) => (
|
||||
<SelectItem value={unit} key={unit}>
|
||||
{unit}
|
||||
</SelectItem>
|
||||
|
||||
@@ -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]);
|
||||
};
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { CodeView } from "@/src/components/ui/code";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { QuickstartExamples } from "@/src/features/public-api/components/QuickstartExamples";
|
||||
|
||||
export function CreateApiKeyButton(props: { projectId: string }) {
|
||||
const utils = api.useUtils();
|
||||
@@ -21,8 +21,7 @@ export function CreateApiKeyButton(props: { projectId: string }) {
|
||||
scope: "apiKeys:create",
|
||||
});
|
||||
|
||||
const hostname =
|
||||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== "EU" ? window.origin : undefined;
|
||||
const hostname = window.origin;
|
||||
|
||||
const mutCreateApiKey = api.apiKeys.create.useMutation({
|
||||
onSuccess: () => utils.apiKeys.invalidate(),
|
||||
@@ -66,28 +65,15 @@ export function CreateApiKeyButton(props: { projectId: string }) {
|
||||
Create new API keys
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent onPointerDownOutside={(e) => e.preventDefault()}>
|
||||
{hostname ? (
|
||||
<>
|
||||
<DialogTitle>Hostname</DialogTitle>
|
||||
<div className="mb-6">
|
||||
<div className="my-2">
|
||||
When connecting to Langfuse, use this hostname / baseurl.
|
||||
</div>
|
||||
<CodeView content={hostname} />
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<DialogContent
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
className="max-w-full md:max-w-xl"
|
||||
>
|
||||
<DialogTitle>API Keys</DialogTitle>
|
||||
<div className="mb-2">
|
||||
<div className="text-md font-semibold">Secret Key</div>
|
||||
<div className="my-2">
|
||||
Please save this secret key.{" "}
|
||||
<span className="font-semibold">
|
||||
You will not be able to view it again
|
||||
</span>
|
||||
. If you lose it, you will need to generate a new one.
|
||||
This key can only be viewed once. You can always generate a new key.
|
||||
</div>
|
||||
<CodeView content={generatedKeys?.secretKey ?? "Loading ..."} />
|
||||
</div>
|
||||
@@ -95,6 +81,20 @@ export function CreateApiKeyButton(props: { projectId: string }) {
|
||||
<div className="text-md mb-2 font-semibold">Public Key</div>
|
||||
<CodeView content={generatedKeys?.publicKey ?? "Loading ..."} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-md mb-2 font-semibold">Host</div>
|
||||
<CodeView content={hostname} />
|
||||
</div>
|
||||
{generatedKeys && (
|
||||
<div className="mb-2">
|
||||
<div className="text-md my-2 font-semibold">Usage</div>
|
||||
<QuickstartExamples
|
||||
secretKey={generatedKeys.secretKey}
|
||||
publicKey={generatedKeys.publicKey}
|
||||
host={hostname}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import { CodeView } from "@/src/components/ui/code";
|
||||
import {
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsContent,
|
||||
TabsTrigger,
|
||||
} from "@/src/components/ui/tabs";
|
||||
|
||||
export const QuickstartExamples = ({
|
||||
secretKey,
|
||||
publicKey,
|
||||
host,
|
||||
}: {
|
||||
secretKey: string;
|
||||
publicKey: string;
|
||||
host: string;
|
||||
}) => (
|
||||
<Tabs defaultValue="python">
|
||||
<TabsList>
|
||||
<TabsTrigger value="python">Python</TabsTrigger>
|
||||
<TabsTrigger value="js">JS/TS</TabsTrigger>
|
||||
<TabsTrigger value="openai">OpenAI</TabsTrigger>
|
||||
<TabsTrigger value="langchain">Langchain</TabsTrigger>
|
||||
<TabsTrigger value="langchain-js">Langchain JS</TabsTrigger>
|
||||
<TabsTrigger value="other">Other</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="python">
|
||||
<CodeView content="pip install langfuse" className="mb-2 bg-blue-50" />
|
||||
<CodeView
|
||||
className="bg-blue-50"
|
||||
content={`from langfuse import Langfuse\n\nlangfuse = Langfuse(\n secret_key="${secretKey}",\n public_key="${publicKey}",\n host="${host}"\n)`}
|
||||
/>
|
||||
<p className="mt-3 text-xs text-gray-600">
|
||||
See{" "}
|
||||
<a
|
||||
href="https://langfuse.com/docs/get-started"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Quickstart
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a
|
||||
href="https://langfuse.com/docs/sdk/python"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Python docs
|
||||
</a>{" "}
|
||||
for more details.
|
||||
</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="js">
|
||||
<CodeView content="npm install langfuse" className="mb-2 bg-blue-50" />
|
||||
<CodeView
|
||||
className="bg-blue-50"
|
||||
content={`import { Langfuse } from "langfuse";\n\nconst langfuse = new Langfuse({\n secretKey: "${secretKey}",\n publicKey: "${publicKey}",\n baseUrl: "${host}"\n});`}
|
||||
/>
|
||||
<p className="mt-3 text-xs text-gray-600">
|
||||
See{" "}
|
||||
<a
|
||||
href="https://langfuse.com/docs/get-started"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Quickstart
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a
|
||||
href="https://langfuse.com/docs/sdk/typescript"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
JS/TS docs
|
||||
</a>{" "}
|
||||
for more details.
|
||||
</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="openai">
|
||||
<p className="mt-2 text-xs text-gray-600">
|
||||
The integration is a drop-in replacement for the OpenAI Python SDK. By
|
||||
changing the import, Langfuse will capture all LLM calls and send them
|
||||
to Langfuse asynchronously.
|
||||
</p>
|
||||
<CodeView content="pip install langfuse" className="my-2 bg-blue-50" />
|
||||
<CodeView
|
||||
title=".env"
|
||||
content={`LANGFUSE_SECRET_KEY=${secretKey};\nLANGFUSE_PUBLIC_KEY=${publicKey};\nLANGFUSE_HOST="${host}";`}
|
||||
className="my-2 bg-blue-50"
|
||||
/>
|
||||
<CodeView
|
||||
content={`# remove: import openai\n\nfrom langfuse.openai import openai`}
|
||||
className="my-2 bg-blue-50"
|
||||
/>
|
||||
<p className="mt-2 text-xs text-gray-600">
|
||||
Use the OpenAI SDK as you would normally. See the{" "}
|
||||
<a
|
||||
href="https://langfuse.com/docs/integrations/openai"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
OpenAI Integration docs
|
||||
</a>{" "}
|
||||
for more details.
|
||||
</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="langchain">
|
||||
<p className="mt-2 text-xs text-gray-600">
|
||||
The integration uses the Langchain callback system to automatically
|
||||
capture detailed traces of your Langchain executions.
|
||||
</p>
|
||||
<CodeView content="pip install langfuse" className="my-2 bg-blue-50" />
|
||||
<CodeView
|
||||
title=".env"
|
||||
content={`LANGFUSE_SECRET_KEY=${secretKey};\nLANGFUSE_PUBLIC_KEY=${publicKey};\nLANGFUSE_HOST="${host}";`}
|
||||
className="my-2 bg-blue-50"
|
||||
/>
|
||||
<CodeView content={LANGCHAIN_PYTHON_CODE} className="my-2 bg-blue-50" />
|
||||
<p className="mt-2 text-xs text-gray-600">
|
||||
See the{" "}
|
||||
<a
|
||||
href="https://langfuse.com/docs/integrations/langchain/python"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Langchain Integration docs
|
||||
</a>{" "}
|
||||
for more details.
|
||||
</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="langchain-js">
|
||||
<p className="mt-2 text-xs text-gray-600">
|
||||
The integration uses the Langchain callback system to automatically
|
||||
capture detailed traces of your Langchain executions.
|
||||
</p>
|
||||
<CodeView
|
||||
content="npm install langfuse-langchain"
|
||||
className="my-2 bg-blue-50"
|
||||
/>
|
||||
<CodeView
|
||||
title=".env"
|
||||
content={`LANGFUSE_SECRET_KEY=${secretKey};\nLANGFUSE_PUBLIC_KEY=${publicKey};\LANGFUSE_BASEURL="${host}";`}
|
||||
className="my-2 bg-blue-50"
|
||||
/>
|
||||
<CodeView content={LANGCHAIN_JS_CODE} className="my-2 bg-blue-50" />
|
||||
<p className="mt-2 text-xs text-gray-600">
|
||||
See the{" "}
|
||||
<a
|
||||
href="https://langfuse.com/docs/integrations/langchain/typescript"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Langchain Integration docs
|
||||
</a>{" "}
|
||||
for more details.
|
||||
</p>
|
||||
</TabsContent>
|
||||
<TabsContent value="other">
|
||||
<p className="mt-2 text-xs text-gray-600">
|
||||
Use the{" "}
|
||||
<a
|
||||
href="https://api.reference.langfuse.com/"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
API
|
||||
</a>{" "}
|
||||
or one of the{" "}
|
||||
<a
|
||||
href="https://langfuse.com/docs/integrations"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
native integrations
|
||||
</a>{" "}
|
||||
(e.g. LiteLLM, Flowise, and Langflow) to integrate with Langfuse.
|
||||
</p>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
|
||||
const LANGCHAIN_PYTHON_CODE = `from langfuse.callback import CallbackHandler
|
||||
|
||||
# <Your Langchain code here>
|
||||
|
||||
# Add handler to run/invoke/call/chat
|
||||
agent.run("<user_input>", callbacks=[handler])`;
|
||||
|
||||
const LANGCHAIN_JS_CODE = `import { CallbackHandler } from "langfuse-langchain";
|
||||
|
||||
// Initialize Langfuse callback handler
|
||||
const handler = new CallbackHandler();
|
||||
|
||||
// Your Langchain implementation
|
||||
const chain = new LLMChain(...);
|
||||
|
||||
// Add handler as callback when running the Langchain agent
|
||||
await chain.call(
|
||||
{ input: "<user_input>" },
|
||||
{ callbacks: [handler] }
|
||||
);`;
|
||||
@@ -91,7 +91,9 @@ export async function verifyAuthHeaderAndReturnScope(
|
||||
console.error("Error verifying auth header: ", error);
|
||||
return {
|
||||
validKey: false,
|
||||
error: error instanceof Error ? error.message : "Authorization error",
|
||||
error:
|
||||
(error instanceof Error ? error.message : "Authorization error") +
|
||||
". Confirm that you've configured the correct host.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { ObservationLevel } from "@prisma/client";
|
||||
import { jsonSchema } from "@/src/utils/zod";
|
||||
import { z } from "zod";
|
||||
import lodash from "lodash";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ModelUsageUnit } from "@/src/constants";
|
||||
import { jsonSchema } from "@/src/utils/zod";
|
||||
import { ObservationLevel } from "@prisma/client";
|
||||
|
||||
export const Usage = z.object({
|
||||
input: z.number().int().nullish(),
|
||||
output: z.number().int().nullish(),
|
||||
total: z.number().int().nullish(),
|
||||
unit: z.enum(["TOKENS", "CHARACTERS"]).nullish(),
|
||||
unit: z.nativeEnum(ModelUsageUnit).nullish(),
|
||||
inputCost: z.number().nullish(),
|
||||
outputCost: z.number().nullish(),
|
||||
totalCost: z.number().nullish(),
|
||||
@@ -17,7 +19,7 @@ const MixedUsage = z.object({
|
||||
input: z.number().int().nullish(),
|
||||
output: z.number().int().nullish(),
|
||||
total: z.number().int().nullish(),
|
||||
unit: z.enum(["TOKENS", "CHARACTERS"]).nullish(),
|
||||
unit: z.nativeEnum(ModelUsageUnit).nullish(),
|
||||
promptTokens: z.number().int().nullish(),
|
||||
completionTokens: z.number().int().nullish(),
|
||||
totalTokens: z.number().int().nullish(),
|
||||
@@ -40,7 +42,7 @@ export const usage = MixedUsage.nullish()
|
||||
input: v.promptTokens,
|
||||
output: v.completionTokens,
|
||||
total: v.totalTokens,
|
||||
unit: "TOKENS",
|
||||
unit: ModelUsageUnit.Tokens,
|
||||
};
|
||||
}
|
||||
// if we get the new generic format, we do not set a default
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { env } from "@/src/env.mjs";
|
||||
|
||||
export const sendToSlack = async (message: unknown) => {
|
||||
if (!env.LANGFUSE_TEAM_SLACK_WEBHOOK)
|
||||
throw new Error("LANGFUSE_TEAM_SLACK_WEBHOOK is not set");
|
||||
|
||||
return await fetch(env.LANGFUSE_TEAM_SLACK_WEBHOOK, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ rawBody: JSON.stringify(message, null, 2) }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
PopoverContent,
|
||||
} from "@/src/components/ui/popover";
|
||||
import { Command, CommandList, CommandGroup } from "cmdk";
|
||||
import { cn } from "@/src/utils/tailwind";
|
||||
|
||||
type TagManagerProps = {
|
||||
tags: string[];
|
||||
@@ -61,7 +62,7 @@ const TagManager = ({
|
||||
setSelectedTags={setSelectedTags}
|
||||
/>
|
||||
<CommandList
|
||||
className={availableTags.length > 0 ? "mt-2" : undefined}
|
||||
className={cn("overflow-auto", availableTags.length > 0 && "mt-2")}
|
||||
>
|
||||
<CommandGroup>
|
||||
{availableTags.slice(0, 5).map((value: string) => (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { VERSION } from "@/src/constants/VERSION";
|
||||
import { VERSION } from "@/src/constants";
|
||||
import { prisma } from "@/src/server/db";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { PostHog } from "posthog-node";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { VERSION } from "@/src/constants/VERSION";
|
||||
import { VERSION } from "@/src/constants";
|
||||
import { cors, runMiddleware } from "@/src/features/public-api/server/cors";
|
||||
import { telemetry } from "@/src/features/telemetry";
|
||||
import { prisma } from "@/src/server/db";
|
||||
|
||||
@@ -30,7 +30,7 @@ import * as Sentry from "@sentry/nextjs";
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: {
|
||||
sizeLimit: "3mb",
|
||||
sizeLimit: "4.5mb",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -56,11 +56,6 @@ export default async function handler(
|
||||
message: authCheck.error,
|
||||
});
|
||||
|
||||
if (authCheck.scope.accessLevel !== "all")
|
||||
return res.status(403).json({
|
||||
message: "Access denied",
|
||||
});
|
||||
|
||||
const batchType = z.object({
|
||||
batch: z.array(z.unknown()),
|
||||
metadata: jsonSchema.nullish(),
|
||||
@@ -246,6 +241,12 @@ const handleSingleEvent = async (
|
||||
processor = new SdkLogProcessor(cleanedEvent);
|
||||
}
|
||||
|
||||
// Deny access to non-score events if the access level is not "all"
|
||||
// This is an additional safeguard to auth checks in EventProcessor
|
||||
if (apiScope.accessLevel !== "all" && type !== eventTypes.SCORE_CREATE) {
|
||||
throw new AuthenticationError("Access denied. Event type not allowed.");
|
||||
}
|
||||
|
||||
return await processor.process(apiScope);
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ import {
|
||||
import { isValidOption } from "@/src/utils/types";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { FeedbackButtonWrapper } from "@/src/features/feedback/component/FeedbackButton";
|
||||
import { BarChart2 } from "lucide-react";
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
|
||||
export type DashboardDateRange = {
|
||||
from: Date;
|
||||
@@ -92,13 +95,35 @@ export default function Start() {
|
||||
return (
|
||||
<div className="md:container">
|
||||
<Header title={project?.name ?? "Dashboard"} />
|
||||
<DatePickerWithRange
|
||||
dateRange={dateRange}
|
||||
setAgg={setAgg}
|
||||
setDateRangeAndOption={setDateRangeAndOption}
|
||||
selectedOption={selectedOption}
|
||||
className=" max-w-full overflow-x-auto"
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<DatePickerWithRange
|
||||
dateRange={dateRange}
|
||||
setAgg={setAgg}
|
||||
setDateRangeAndOption={setDateRangeAndOption}
|
||||
selectedOption={selectedOption}
|
||||
className=" max-w-full overflow-x-auto"
|
||||
/>
|
||||
<FeedbackButtonWrapper
|
||||
className="border-box"
|
||||
title="Request Chart"
|
||||
description="Your feedback matters! Let us know what additional data or metrics you'd like to see in your dashboard."
|
||||
type="dashboard"
|
||||
>
|
||||
<Button
|
||||
id="date"
|
||||
variant={"outline"}
|
||||
className={
|
||||
"group justify-start gap-x-3 text-left font-semibold text-gray-700 hover:bg-gray-50 hover:text-indigo-600"
|
||||
}
|
||||
>
|
||||
<BarChart2
|
||||
className="h-6 w-6 shrink-0 text-gray-700 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Request Chart
|
||||
</Button>
|
||||
</FeedbackButtonWrapper>
|
||||
</div>
|
||||
<div className="grid w-full grid-cols-1 gap-4 overflow-hidden lg:grid-cols-2 xl:grid-cols-6">
|
||||
<TracesBarListChart
|
||||
className="col-span-1 xl:col-span-2 "
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ModelUsageUnit } from "@/src/constants";
|
||||
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProjectProcedure,
|
||||
} from "@/src/server/api/trpc";
|
||||
import { paginationZod } from "@/src/utils/zod";
|
||||
import { throwIfNoAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
@@ -93,7 +94,7 @@ export const modelRouter = createTRPCRouter({
|
||||
inputPrice: z.number().nonnegative().optional(),
|
||||
outputPrice: z.number().nonnegative().optional(),
|
||||
totalPrice: z.number().nonnegative().optional(),
|
||||
unit: z.enum(["TOKENS", "CHARACTERS"]),
|
||||
unit: z.nativeEnum(ModelUsageUnit),
|
||||
tokenizerId: z.enum(["openai", "claude"]).optional(),
|
||||
tokenizerConfig: z.record(z.union([z.string(), z.number()])).optional(),
|
||||
}),
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import { v4 } from "uuid";
|
||||
import { type z } from "zod";
|
||||
import { jsonSchema } from "@/src/utils/zod";
|
||||
import { sendToBetterstack } from "@/src/features/betterstack/server/betterstack-webhook";
|
||||
|
||||
export interface EventProcessor {
|
||||
process(
|
||||
@@ -562,7 +563,16 @@ export class SdkLogProcessor implements EventProcessor {
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
process() {
|
||||
return undefined;
|
||||
process(apiScope: ApiAccessScope) {
|
||||
try {
|
||||
void sendToBetterstack({
|
||||
type: "sdk-log",
|
||||
event: this.event,
|
||||
projectId: apiScope.projectId,
|
||||
});
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user