Compare commits

...
47 Commits
Author SHA1 Message Date
Max Deichmann b919562eed chore: release v3.65.3 2025-06-04 01:17:23 +02:00
Max DeichmannandGitHub 91c01e7363 chore: allow annotation queues on FOSS (#7130)
chore: move features
2025-06-03 22:17:02 +00:00
Max DeichmannandGitHub 48ec1bccae fix: fix full text search selector (#7129)
* push

* fix: annotation queue count

* chore: move features

* chore: move features

* chore: move features
2025-06-03 22:04:49 +00:00
steffen911 66aa2fd48e chore: release v3.65.2 2025-06-03 22:02:11 +02:00
Steffen SchmitzandGitHub f822bf1049 chore: add opt-out flag from queue length metric checks (#7123) 2025-06-03 19:21:35 +00:00
Hassieb PakzadGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
96ec24aad8 fix(otel): set trace io from root span (#7127)
* fix(otel): set trace io from root span

* Update web/src/features/otel/server/index.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-06-03 16:48:58 +00:00
steffen911 068fbd0cc6 chore: release v3.65.1 2025-06-03 11:33:30 +02:00
Steffen SchmitzandGitHub 7c8b76d573 security: bump tar-fs, rollup version (#7116)
* security: bump tar-fs version

* chore: bump rollup version
2025-06-03 08:13:54 +00:00
Max Deichmann e4e93d64bb chore: release v3.65.0 2025-06-03 10:09:46 +02:00
Max DeichmannandGitHub f5cf8693e9 chore: move core features to OSS (#7091)
* chore: move features

* chore: move features

* chore: move features

* chore: move features

* chore: move features

* chore: move features

* chore: move features

* chore: move features

* chore: move features

* chore: move features

* chore: move features

* chore: move features

* chore: move features

* chore: move features

* chore: move features

* changes to pro plan

* chore: move features
2025-06-03 08:07:45 +00:00
marliessophieandGitHub 5beef28214 style(scores): limit height of grouped scores hovercard (#7106) 2025-06-02 10:59:46 +00:00
marliessophieandGitHub d93712c6ab chore: sort dataset (run) items by created_at and id (#7105)
* `created_at` is not unique, we require a deterministic order of items to ensure data consistency
2025-06-02 10:46:46 +00:00
Hassieb PakzadandGitHub b1b0a966be fix(projects-api-docs): retentionDays to optional (#7103) 2025-06-02 11:49:25 +02:00
b5edadb203 fix: update Google AI Studio model name to preview version (#7086) (#7102)
Co-authored-by: Faris Ibrahim <faris@traba.work>
2025-06-02 11:23:51 +02:00
marliessophieandGitHub 3090882590 fix(default-model): allow set up flow to add LLM API key (#7101) 2025-06-02 07:58:37 +00:00
Max DeichmannandGitHub 787f5d8d10 chore: remove popup from count for historic eval elements (#7098)
* chore: move features

* chore: move features

* chore: move features
2025-05-30 21:25:50 +00:00
Jannik MaierhöferandGitHub 5fa142eda4 chore(ui): changed wording on checkout page (#7045) 2025-05-30 10:05:15 +00:00
steffen911 d96865b019 chore: release v3.64.0 2025-05-30 11:25:55 +02:00
Steffen SchmitzandGitHub 8d808923a1 feat: allow host credentials for blob storage S3 integration (#7072)
* feat: allow host credentials for blob storage S3 integration

* chore: compilation errors
2025-05-30 09:07:52 +00:00
Max DeichmannandGitHub 0f056c1c2d fix: add error message for missing encryption key for self-hosters (#7090)
push
2025-05-30 08:40:50 +00:00
Max DeichmannandGitHub 094a523795 perf: use trace identifier stream to batch delete traces (#7081) 2025-05-29 20:08:27 +02:00
Marc KlingenandGitHub c60e58a568 feat(ui): misc improvements to dashboards including mobile view, big number chart, truncate title/description, enforce min height/width of widgets (#7082)
* truncate title and description to prevent wraps

* remove unnecessary padding from charts

* improve bignumber chart

* add min h/w

* mobile resizing

* fix
2025-05-29 15:58:38 +00:00
Max DeichmannGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
6147e9c518 perf: only read trace identifiers for historic eval read stream (#7080)
* push

* Update packages/shared/src/server/services/traces-ui-table-service.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* perf: add exact timestamp match for eval batch actions

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-05-29 14:35:39 +00:00
Max DeichmannandGitHub f0408b208e perf: add exact timestamp match for eval batch actions (#7077)
* perf: add exact timestamp match for eval batch actions

* perf: add exact timestamp match for eval batch actions

* perf: add exact timestamp match for eval batch actions
2025-05-29 10:54:13 +00:00
Steffen SchmitzandGitHub 55e0bdcac0 feat: allow validation and run now for blob storage integrations (#7064)
* feat: allow validation and run now for blob storage integrations

* chore: error handling and formatting

* lint

* chore: linting
2025-05-28 16:47:23 +00:00
Hassieb PakzadandGitHub 05e15c9f53 fix(otel): update trace name only for root spans (#7061)
* fix(otel): update trace name only for root spans

* push
2025-05-28 15:30:37 +00:00
Max DeichmannandGitHub ce6249b266 perf: data retention concurrency (#7065) 2025-05-28 17:36:20 +02:00
Max DeichmannandGitHub 83e91e5bf5 chore: add logging to retry queue (#7062)
fix: do not wrap traces toolbar
2025-05-28 13:58:46 +00:00
Hassieb PakzadandGitHub edf073cc18 feat(api-models): return model prices on public API (#7046)
* feat(api-models): return model prices on public API

* add tests

* add openapi spec

* return map instead of list
2025-05-28 13:24:20 +00:00
Hassieb Pakzad 65343a8e9b chore: release v3.63.1 2025-05-28 14:17:40 +02:00
Max DeichmannandGitHub 0978055bdc chore: rename dead letter retry queue (#7058)
* push

* push

* push
2025-05-28 11:54:11 +00:00
Max DeichmannandGitHub 76aaf2a3d0 chore: upgrade clickhouse node client (#6806)
* push

* fix: do not wrap traces toolbar

* fix: do not wrap traces toolbar

* fix: do not wrap traces toolbar
2025-05-28 07:57:14 +00:00
Max DeichmannandGitHub 8ed61bf705 perf: increase retry batch action queue (#7049)
push
2025-05-27 19:59:47 +00:00
Steffen SchmitzandGitHub 45436530b4 chore: add analytic views in ClickHouse (#7034)
* chore: add analytic views in ClickHouse

* chore: add additional properties

* chore: add filtering and order changes
2025-05-27 15:59:12 +00:00
Hassieb PakzadandGitHub 7a3f01fc0a fix(playground): resolve prompts when jumping to playground (#7041)
* fix(playground): resolve prompts when jumping to playground

* push
2025-05-27 14:36:40 +00:00
marliessophieandGitHub 468fc4d079 chore(evals): display default model on peek (#7042) 2025-05-27 14:19:11 +00:00
Hassieb PakzadandGitHub 945d13dac0 fix(playground): update tools and schemas if name already exists (#7039)
* fix(playground): update tools and schemas if name already exists

* push
2025-05-27 13:38:59 +00:00
Marc KlingenandGitHub 3646eaa5a8 feat(model): add regex for claude-sonnet-4@20250514 (#7035) 2025-05-27 00:50:10 +00:00
marliessophieandGitHub feb5af11b8 style: default evaluation model management components (#7028) 2025-05-26 16:57:08 +00:00
marliessophieandGitHub 3711af5663 fix: prompt variables of SQL Semantic Equivalence evaluator (#7029) 2025-05-26 14:56:35 +00:00
marliessophieandGitHub 9ec798f393 feat(evaluation): allow filtering by log status (#7018) 2025-05-26 14:43:57 +00:00
Hassieb PakzadandGitHub f71fd43d91 fix(llm-tool-dialog): do not propagate textarea keystrokes up (#7023)
* fix(llm-tool-dialog): do not propagate textarea keystrokes up

* push
2025-05-26 13:51:51 +00:00
marliessophieandGitHub 0303596420 fix(scores): conditionally render metadata in score badges (#7020) 2025-05-26 11:31:50 +00:00
marliessophieandGitHub cfaccc46d3 chore: ensure eval config modal closes after successful delete (#7015)
* chore: ensure eval config modal closes after successful delete

* chore: eslint
2025-05-26 10:21:47 +00:00
Max DeichmannandGitHub 907f764adc fix: do not wrap traces toolbar (#7019)
* fix: do not wrap traces toolbar

* fix: do not wrap traces toolbar
2025-05-26 10:18:23 +00:00
Hassieb PakzadandGitHub 652e9e3fe5 fix(otel): set trace name only if root span or attr provided (#7017) 2025-05-26 09:59:59 +00:00
Steffen SchmitzGitHubellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
48673e43d7 feat: allow propagation of request headers through logs (#6979)
* feat: allow propagation of request headers through logs

* chore: propagate under correct name

* chore: take all header values

* chore: allow more properties in context propagation

* chore: update trpc propagation

* Update packages/shared/src/env.ts

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* chore: trace header props for ingestion api

* chore: rename env variable

* chore: remove unused comments

* chore: remove outdated addUserToSpan method

---------

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-05-26 07:33:02 +00:00
233 changed files with 2420 additions and 1588 deletions
+10 -3
View File
@@ -457,13 +457,13 @@ types:
docs: Unit used by this model.
type: optional<ModelUsageUnit>
inputPrice:
docs: Price (USD) per input unit
docs: Deprecated. See 'prices' instead. Price (USD) per input unit
type: optional<double>
outputPrice:
docs: Price (USD) per output unit
docs: Deprecated. See 'prices' instead. Price (USD) per output unit
type: optional<double>
totalPrice:
docs: Price (USD) per total unit. Cannot be set if input or output price is set.
docs: Deprecated. See 'prices' instead. Price (USD) per total unit. Cannot be set if input or output price is set.
type: optional<double>
tokenizerId:
docs: Optional. Tokenizer to be applied to observations which match to this model. See docs for more details.
@@ -473,6 +473,13 @@ types:
type: optional<unknown>
isLangfuseManaged:
type: boolean
prices:
docs: Price (USD) by usage type
type: map<string, ModelPrice>
ModelPrice:
properties:
price: double
# Utilities
ModelUsageUnit:
+7 -7
View File
@@ -11,7 +11,7 @@ service:
method: GET
path: /projects
response: Projects
create:
docs: Create a new project (requires organization-scoped API key)
method: POST
@@ -28,7 +28,7 @@ service:
type: integer
docs: Number of days to retain data. Must be 0 or at least 3 days. Requires data-retention entitlement for non-zero values. Optional.
response: Project
update:
docs: Update a project by ID (requires organization-scoped API key).
method: PUT
@@ -47,7 +47,7 @@ service:
type: integer
docs: Number of days to retain data. Must be 0 or at least 3 days. Requires data-retention entitlement for non-zero values. Optional.
response: Project
delete:
docs: Delete a project by ID (requires organization-scoped API key). Project deletion is processed asynchronously.
method: DELETE
@@ -57,7 +57,7 @@ service:
response:
status-code: 202
type: ProjectDeletionResponse
# API Key endpoints
getApiKeys:
docs: Get all API keys for a project (requires organization-scoped API key)
@@ -104,14 +104,14 @@ types:
type: map<string, unknown>
docs: Metadata for the project
retentionDays:
type: integer
type: optional<integer>
docs: Number of days to retain data. Null or 0 means no retention. Omitted if no retention is configured.
ProjectDeletionResponse:
properties:
success: boolean
message: string
ApiKeyList:
docs: List of API keys for a project
properties:
@@ -137,7 +137,7 @@ types:
secretKey: string
displaySecretKey: string
note: optional<string>
ApiKeyDeletionResponse:
docs: Response for API key deletion
properties:
+4 -2
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "3.63.0",
"version": "3.65.3",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
@@ -86,7 +86,9 @@
"pnpm": {
"overrides": {
"nanoid": "^3.3.8",
"katex": "^0.16.21"
"katex": "^0.16.21",
"tar-fs": "^2.1.2",
"rollup@^4.0.0": "^4.22.4"
},
"patchedDependencies": {
"next-auth@4.24.11": "patches/next-auth@4.24.11.patch"
@@ -0,0 +1 @@
DROP VIEW IF EXISTS analytics_traces ON CLUSTER default;
@@ -0,0 +1,15 @@
CREATE VIEW analytics_traces ON CLUSTER default AS
SELECT
project_id,
toStartOfHour(timestamp) AS hour,
uniq(id) AS countTraces,
max(user_id IS NOT NULL) AS hasUsers,
max(session_id IS NOT NULL) AS hasSessions,
max(if(environment != 'default', 1, 0)) AS hasEnvironments,
max(length(tags) > 0) AS hasTags
FROM
traces
WHERE toStartOfHour(timestamp) <= toStartOfHour(subtractHours(now(), 1))
GROUP BY
project_id,
hour;
@@ -0,0 +1 @@
DROP VIEW IF EXISTS analytics_observations ON CLUSTER default;
@@ -0,0 +1,18 @@
CREATE VIEW analytics_observations ON CLUSTER default AS
SELECT
project_id,
type,
toStartOfHour(start_time) AS hour,
uniq(id) AS countObservations,
max(level != 'DEFAULT') AS hasLevel,
max(provided_model_name IS NOT NULL) AS hasProvidedModelName,
max(length(provided_usage_details) > 0) AS hasProvidedUsageDetails,
max(length(provided_cost_details) > 0) AS hasProvidedCostDetails,
max(prompt_name IS NOT NULL) AS hasPromptName
FROM
observations
WHERE toStartOfHour(start_time) <= toStartOfHour(subtractHours(now(), 1))
GROUP BY
project_id,
type,
hour;
@@ -0,0 +1 @@
DROP VIEW IF EXISTS analytics_scores ON CLUSTER default;
@@ -0,0 +1,21 @@
CREATE VIEW analytics_scores ON CLUSTER default AS
SELECT
project_id,
toStartOfHour(timestamp) AS hour,
uniq(id) AS countScores,
max(source = 'ANNOTATION') AS hasAnnotation,
max(source = 'API') AS hasApi,
max(source = 'EVAL') AS hasEval,
max(observation_id IS NOT NULL) AS hasObservationScore,
max(session_id IS NOT NULL) AS hasSessionScore,
max(dataset_run_id IS NOT NULL) AS hasDatasetRunScore,
max(data_type = 'BOOLEAN') AS hasBoolScore,
max(data_type = 'NUMERIC') AS hasNumericScore,
max(data_type = 'CATEGORICAL') AS hasCategoricalScore,
max(comment IS NOT NULL) AS hasComment
FROM
scores
WHERE toStartOfHour(timestamp) <= toStartOfHour(subtractHours(now(), 1))
GROUP BY
project_id,
hour;
@@ -0,0 +1 @@
DROP VIEW IF EXISTS analytics_traces;
@@ -0,0 +1,15 @@
CREATE VIEW analytics_traces AS
SELECT
project_id,
toStartOfHour(timestamp) AS hour,
uniq(id) AS countTraces,
max(user_id IS NOT NULL) AS hasUsers,
max(session_id IS NOT NULL) AS hasSessions,
max(if(environment != 'default', 1, 0)) AS hasEnvironments,
max(length(tags) > 0) AS hasTags
FROM
traces
WHERE toStartOfHour(timestamp) <= toStartOfHour(subtractHours(now(), 1))
GROUP BY
project_id,
hour;
@@ -0,0 +1 @@
DROP VIEW IF EXISTS analytics_observations;
@@ -0,0 +1,18 @@
CREATE VIEW analytics_observations AS
SELECT
project_id,
type,
toStartOfHour(start_time) AS hour,
uniq(id) AS countObservations,
max(level != 'DEFAULT') AS hasLevel,
max(provided_model_name IS NOT NULL) AS hasProvidedModelName,
max(length(provided_usage_details) > 0) AS hasProvidedUsageDetails,
max(length(provided_cost_details) > 0) AS hasProvidedCostDetails,
max(prompt_name IS NOT NULL) AS hasPromptName
FROM
observations
WHERE toStartOfHour(start_time) <= toStartOfHour(subtractHours(now(), 1))
GROUP BY
project_id,
type,
hour;
@@ -0,0 +1 @@
DROP VIEW IF EXISTS analytics_scores;
@@ -0,0 +1,21 @@
CREATE VIEW analytics_scores AS
SELECT
project_id,
toStartOfHour(timestamp) AS hour,
uniq(id) AS countScores,
max(source = 'ANNOTATION') AS hasAnnotation,
max(source = 'API') AS hasApi,
max(source = 'EVAL') AS hasEval,
max(observation_id IS NOT NULL) AS hasObservationScore,
max(session_id IS NOT NULL) AS hasSessionScore,
max(dataset_run_id IS NOT NULL) AS hasDatasetRunScore,
max(data_type = 'BOOLEAN') AS hasBoolScore,
max(data_type = 'NUMERIC') AS hasNumericScore,
max(data_type = 'CATEGORICAL') AS hasCategoricalScore,
max(comment IS NOT NULL) AS hasComment
FROM
scores
WHERE toStartOfHour(timestamp) <= toStartOfHour(subtractHours(now(), 1))
GROUP BY
project_id,
hour;
+1 -1
View File
@@ -60,7 +60,7 @@
"@aws-sdk/lib-storage": "^3.675.0",
"@aws-sdk/s3-request-presigner": "^3.679.0",
"@azure/storage-blob": "^12.26.0",
"@clickhouse/client": "^1.4.0",
"@clickhouse/client": "^1.11.1",
"@google-cloud/storage": "^7.15.2",
"@langchain/anthropic": "^0.3.12",
"@langchain/aws": "^0.1.3",
+2 -2
View File
@@ -228,8 +228,8 @@ export type BlobStorageIntegration = {
type: BlobStorageIntegrationType;
bucket_name: string;
prefix: string;
access_key_id: string;
secret_access_key: string;
access_key_id: string | null;
secret_access_key: string | null;
region: string;
endpoint: string | null;
force_path_style: boolean;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "blob_storage_integrations" ALTER COLUMN "access_key_id" DROP NOT NULL,
ALTER COLUMN "secret_access_key" DROP NOT NULL;
+2 -2
View File
@@ -951,8 +951,8 @@ model BlobStorageIntegration {
type BlobStorageIntegrationType @map("type")
bucketName String @map("bucket_name")
prefix String @map("prefix")
accessKeyId String @map("access_key_id")
secretAccessKey String @map("secret_access_key")
accessKeyId String? @map("access_key_id")
secretAccessKey String? @map("secret_access_key")
region String @map("region")
endpoint String? @map("endpoint")
forcePathStyle Boolean @map("force_path_style")
+6
View File
@@ -50,6 +50,12 @@ const EnvSchema = z.object({
.enum(["trace", "debug", "info", "warn", "error", "fatal"])
.optional(),
LANGFUSE_LOG_FORMAT: z.enum(["text", "json"]).default("text"),
LANGFUSE_LOG_PROPAGATED_HEADERS: z
.string()
.optional()
.transform((s) =>
s ? s.split(",").map((s) => s.toLowerCase().trim()) : [],
),
ENABLE_AWS_CLOUDWATCH_METRIC_PUBLISHING: z
.enum(["true", "false"])
.default("false"),
@@ -14,7 +14,6 @@ export const planLabels = {
"cloud:pro": "Pro",
"cloud:team": "Team",
"cloud:enterprise": "Enterprise",
"self-hosted:pro": "Pro (self-hosted)",
"self-hosted:enterprise": "Enterprise (self-hosted)",
} as const;
@@ -0,0 +1,44 @@
import * as opentelemetry from "@opentelemetry/api";
import type { IncomingHttpHeaders } from "http";
import { env } from "../env";
export type LangfuseContextProps = {
headers?: IncomingHttpHeaders;
userId?: string;
projectId?: string;
};
/**
* Returns a new context containing baggage entries composed from
* the supplied props (headers, userId, projectId). Existing baggage
* entries are preserved.
*/
export const contextWithLangfuseProps = (
props: LangfuseContextProps,
): opentelemetry.Context => {
const ctx = opentelemetry.context.active();
let baggage =
opentelemetry.propagation.getBaggage(ctx) ??
opentelemetry.propagation.createBaggage();
if (props.headers) {
(env.LANGFUSE_LOG_PROPAGATED_HEADERS as string[]).forEach((name) => {
const value = props.headers![name];
if (!value) return;
const strValue = Array.isArray(value) ? JSON.stringify(value) : value;
baggage = baggage.setEntry(`langfuse.header.${name}`, {
value: strValue,
});
});
}
if (props.userId) {
baggage = baggage.setEntry("langfuse.user.id", { value: props.userId });
}
if (props.projectId) {
baggage = baggage.setEntry("langfuse.project.id", {
value: props.projectId,
});
}
return opentelemetry.propagation.setBaggage(ctx, baggage);
};
+2 -1
View File
@@ -41,13 +41,14 @@ export * from "./redis/dataRetentionProcessingQueue";
export * from "./redis/coreDataS3ExportQueue";
export * from "./redis/meteringDataPostgresExportQueue";
export * from "./redis/experimentCreateQueue";
export * from "./redis/dlxRetryQueue";
export * from "./redis/dlqRetryQueue";
export * from "./auth/types";
export * from "./queues";
export * from "./orderByToPrisma";
export * from "./filterToPrisma";
export * from "./instrumentation";
export * from "./logger";
export * from "./headerPropagation";
export * from "./queries";
export * from "./repositories";
export * from "./redis/evalExecutionQueue";
@@ -43,6 +43,14 @@ export async function instrumentAsync<T>(
},
activeContext,
async (span) => {
const baggage = opentelemetry.propagation.getBaggage(
opentelemetry.context.active(),
);
if (baggage) {
baggage
.getAllEntries()
.forEach(([k, v]) => span.setAttribute(k, v.value));
}
try {
const result = await callback(span);
span.end();
@@ -77,6 +85,14 @@ export function instrumentSync<T>(
},
activeContext,
(span) => {
const baggage = opentelemetry.propagation.getBaggage(
opentelemetry.context.active(),
);
if (baggage) {
baggage
.getAllEntries()
.forEach(([k, v]) => span.setAttribute(k, v.value));
}
try {
const result = callback(span);
span.end();
@@ -141,30 +157,6 @@ export const traceException = (
});
};
export const addUserToSpan = (
attributes: {
userId?: string;
projectId?: string;
email?: string;
orgId?: string;
plan?: string;
},
span?: opentelemetry.Span,
) => {
const activeSpan = span ?? getCurrentSpan();
if (!activeSpan) {
return;
}
attributes.userId && activeSpan.setAttribute("user.id", attributes.userId);
attributes.email && activeSpan.setAttribute("user.email", attributes.email);
attributes.projectId &&
activeSpan.setAttribute("project.id", attributes.projectId);
attributes.orgId && activeSpan.setAttribute("org.id", attributes.orgId);
attributes.plan && activeSpan.setAttribute("org.plan", attributes.plan);
};
export const getTracer = (name: string) => opentelemetry.trace.getTracer(name);
const cloudWatchClient = new CloudWatchClient();
+4 -2
View File
@@ -319,7 +319,8 @@ export const anthropicModels = [
// WARNING: The first entry in the array is chosen as the default model to add LLM API keys
export const vertexAIModels = [
"gemini-2.5-pro-exp-03-25",
"gemini-2.5-pro-preview-05-06",
"gemini-2.5-flash-preview-05-20",
"gemini-2.0-pro-exp-02-05",
"gemini-2.0-flash",
"gemini-2.0-flash-001",
@@ -332,7 +333,8 @@ export const vertexAIModels = [
// WARNING: The first entry in the array is chosen as the default model to add LLM API keys. Make sure it supports top_p, max_tokens and temperature.
export const googleAIStudioModels = [
"gemini-2.5-pro-exp-03-25",
"gemini-2.5-pro-preview-05-06",
"gemini-2.5-flash-preview-05-20",
"gemini-2.0-flash",
"gemini-2.0-flash-lite-preview",
"gemini-2.0-flash-lite-preview-02-05",
+7
View File
@@ -1,6 +1,7 @@
import { env } from "../env";
import winston from "winston";
import { getCurrentSpan } from "./instrumentation";
import { propagation, context } from "@opentelemetry/api";
const tracingFormat = function () {
return winston.format((info) => {
@@ -13,6 +14,12 @@ const tracingFormat = function () {
info["trace_id"] = traceId;
info["span_id"] = spanId;
}
const baggage = propagation.getBaggage(context.active());
if (baggage) {
const headerObj: Record<string, string> = {};
baggage.getAllEntries().forEach(([k, v]) => (headerObj[k] = v.value));
if (Object.keys(headerObj).length) info = { ...headerObj, ...info };
}
return info;
})();
};
+1
View File
@@ -120,6 +120,7 @@ export const CreateEvalQueueEventSchema = DatasetRunItemUpsertEventSchema.and(
z.object({
timestamp: z.date(),
configId: z.string(),
exactTimestamp: z.date().optional(),
}),
),
);
@@ -26,7 +26,7 @@ export class BatchActionQueue {
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: 10_000,
attempts: 2,
attempts: 10,
backoff: {
type: "exponential",
delay: 5000,
+1 -1
View File
@@ -20,7 +20,7 @@ import { DataRetentionProcessingQueue } from "./dataRetentionProcessingQueue";
import { BatchActionQueue } from "./batchActionQueue";
import { CreateEvalQueue } from "./createEvalQueue";
import { ScoreDeleteQueue } from "./scoreDelete";
import { DeadLetterRetryQueue } from "./dlxRetryQueue";
import { DeadLetterRetryQueue } from "./dlqRetryQueue";
export function getQueue(queueName: QueueName): Queue | null {
switch (queueName) {
@@ -48,12 +48,14 @@ export const checkTraceExists = async ({
timestamp,
filter,
maxTimeStamp,
exactTimestamp,
}: {
projectId: string;
traceId: string;
timestamp: Date;
filter: FilterState;
maxTimeStamp: Date | undefined;
exactTimestamp?: Date;
}): Promise<boolean> => {
const { tracesFilter } = getProjectIdDefaultFilter(projectId, {
tracesPrefix: "t",
@@ -111,6 +113,7 @@ export const checkTraceExists = async ({
AND timestamp >= {timestamp: DateTime64(3)} - ${TRACE_TO_OBSERVATIONS_INTERVAL}
${maxTimeStamp ? `AND timestamp <= {maxTimeStamp: DateTime64(3)}` : ""}
${!maxTimeStamp ? `AND timestamp <= {timestamp: DateTime64(3)} + INTERVAL 2 DAY` : ""}
${exactTimestamp ? `AND timestamp = {exactTimestamp: DateTime64(3)}` : ""}
GROUP BY t.id, t.project_id
`;
@@ -126,6 +129,9 @@ export const checkTraceExists = async ({
...(maxTimeStamp
? { maxTimeStamp: convertDateToClickhouseDateTime(maxTimeStamp) }
: {}),
...(exactTimestamp
? { exactTimestamp: convertDateToClickhouseDateTime(exactTimestamp) }
: {}),
},
tags: {
feature: "tracing",
@@ -460,7 +460,7 @@ class S3StorageService implements StorageService {
return { signedUrl };
} catch (err) {
logger.error(`Failed to upload file to ${fileName}`, err);
throw new Error("Failed to upload to S3 or generate signed URL");
throw new Error(`Failed to upload to S3 or generate signed URL: ${err}`);
}
}
@@ -157,7 +157,7 @@ export type TracesTableMetricsClickhouseReturnType = {
};
export type FetchTracesTableProps = {
select: "count" | "rows" | "metrics";
select: "count" | "rows" | "metrics" | "identifiers";
projectId: string;
filter: FilterState;
searchQuery?: string;
@@ -169,84 +169,42 @@ export type FetchTracesTableProps = {
tags?: Record<string, string>;
};
export const getTracesTableCount = async (props: {
projectId: string;
filter: FilterState;
searchQuery?: string;
searchType: TracingSearchType[];
orderBy?: OrderByState;
limit?: number;
page?: number;
}) => {
const countRows = await getTracesTableGeneric<{ count: string }>({
select: "count",
tags: { kind: "count" },
...props,
});
const converted = countRows.map((row) => ({
count: Number(row.count),
}));
return converted.length > 0 ? converted[0].count : 0;
// Define return type mapping for better type safety
type SelectReturnTypeMap = {
count: { count: string };
metrics: TracesTableMetricsClickhouseReturnType;
rows: TracesTableReturnType;
identifiers: { id: string; projectId: string; timestamp: string };
};
export const getTracesTableMetrics = async (props: {
projectId: string;
filter: FilterState;
searchQuery?: string;
orderBy?: OrderByState;
limit?: number;
page?: number;
clickhouseConfigs?: ClickHouseClientConfigOptions | undefined;
}): Promise<Array<Omit<TracesMetricsUiReturnType, "scores">>> => {
const countRows =
await getTracesTableGeneric<TracesTableMetricsClickhouseReturnType>({
select: "metrics",
tags: { kind: "analytic" },
...props,
});
// Function overloads for type-safe select-specific returns
async function getTracesTableGeneric(
// eslint-disable-next-line no-unused-vars
props: FetchTracesTableProps & { select: "count" },
): Promise<Array<SelectReturnTypeMap["count"]>>;
return countRows.map(convertToUITableMetrics);
};
async function getTracesTableGeneric(
// eslint-disable-next-line no-unused-vars
props: FetchTracesTableProps & { select: "metrics" },
): Promise<Array<SelectReturnTypeMap["metrics"]>>;
export const getTracesTable = async (p: {
projectId: string;
filter: FilterState;
searchQuery?: string;
searchType?: TracingSearchType[];
orderBy?: OrderByState;
limit?: number;
page?: number;
clickhouseConfigs?: ClickHouseClientConfigOptions | undefined;
}) => {
const {
projectId,
filter,
searchQuery,
searchType,
orderBy,
limit,
page,
clickhouseConfigs,
} = p;
const rows = await getTracesTableGeneric<TracesTableReturnType>({
select: "rows",
tags: { kind: "list" },
projectId,
filter,
searchQuery,
searchType,
orderBy,
limit,
page,
clickhouseConfigs,
});
async function getTracesTableGeneric(
// eslint-disable-next-line no-unused-vars
props: FetchTracesTableProps & { select: "rows" },
): Promise<Array<SelectReturnTypeMap["rows"]>>;
return rows.map(convertToUiTableRows);
};
async function getTracesTableGeneric(
// eslint-disable-next-line no-unused-vars
props: FetchTracesTableProps & { select: "identifiers" },
): Promise<Array<SelectReturnTypeMap["identifiers"]>>;
const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
// Implementation with union type for internal use
async function getTracesTableGeneric(
// eslint-disable-next-line no-unused-vars
props: FetchTracesTableProps,
): Promise<Array<SelectReturnTypeMap[keyof SelectReturnTypeMap]>>;
async function getTracesTableGeneric(props: FetchTracesTableProps) {
const {
select,
projectId,
@@ -297,6 +255,12 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
t.session_id as session_id,
t.public as public`;
break;
case "identifiers":
sqlSelect = `
t.id as id,
t.project_id as projectId,
t.timestamp as timestamp`;
break;
default:
throw new Error(`Unknown select type: ${select}`);
}
@@ -471,7 +435,7 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
)
SELECT ${sqlSelect}
-- FINAL is used for non default ordering and count.
FROM traces t ${["metrics", "rows"].includes(select) && defaultOrder ? "" : "FINAL"}
FROM traces t ${["metrics", "rows", "identifiers"].includes(select) && defaultOrder ? "" : "FINAL"}
${select === "metrics" || requiresObservationsJoin ? `LEFT JOIN observations_stats os on os.project_id = t.project_id and os.trace_id = t.id` : ""}
${select === "metrics" || requiresScoresJoin ? `LEFT JOIN scores_avg s on s.project_id = t.project_id and s.trace_id = t.id` : ""}
WHERE t.project_id = {projectId: String}
@@ -480,11 +444,13 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
${chOrderBy}
-- This is used for metrics and row queries. Count has only one result.
-- This is only used for default ordering. Otherwise, we use final.
${["metrics", "rows"].includes(select) && defaultOrder ? "LIMIT 1 BY id, project_id" : ""}
${["metrics", "rows", "identifiers"].includes(select) && defaultOrder ? "LIMIT 1 BY id, project_id" : ""}
${limit !== undefined && page !== undefined ? `LIMIT {limit: Int32} OFFSET {offset: Int32}` : ""}
`;
const res = await queryClickhouse<T>({
const res = await queryClickhouse<
SelectReturnTypeMap[keyof SelectReturnTypeMap]
>({
query: query,
params: {
limit: limit,
@@ -506,4 +472,120 @@ const getTracesTableGeneric = async <T>(props: FetchTracesTableProps) => {
});
return res;
}
export const getTracesTableCount = async (props: {
projectId: string;
filter: FilterState;
searchQuery?: string;
searchType: TracingSearchType[];
orderBy?: OrderByState;
limit?: number;
page?: number;
}) => {
const countRows = await getTracesTableGeneric({
select: "count",
tags: { kind: "count" },
...props,
});
const converted = countRows.map((row) => ({
count: Number(row.count),
}));
return converted.length > 0 ? converted[0].count : 0;
};
export const getTracesTableMetrics = async (props: {
projectId: string;
filter: FilterState;
searchQuery?: string;
orderBy?: OrderByState;
limit?: number;
page?: number;
clickhouseConfigs?: ClickHouseClientConfigOptions | undefined;
}): Promise<Array<Omit<TracesMetricsUiReturnType, "scores">>> => {
const countRows = await getTracesTableGeneric({
select: "metrics",
tags: { kind: "analytic" },
...props,
});
return countRows.map(convertToUITableMetrics);
};
export const getTracesTable = async (p: {
projectId: string;
filter: FilterState;
searchQuery?: string;
searchType?: TracingSearchType[];
orderBy?: OrderByState;
limit?: number;
page?: number;
clickhouseConfigs?: ClickHouseClientConfigOptions | undefined;
}) => {
const {
projectId,
filter,
searchQuery,
searchType,
orderBy,
limit,
page,
clickhouseConfigs,
} = p;
const rows = await getTracesTableGeneric({
select: "rows",
tags: { kind: "list" },
projectId,
filter,
searchQuery,
searchType,
orderBy,
limit,
page,
clickhouseConfigs,
});
return rows.map(convertToUiTableRows);
};
export const getTraceIdentifiers = async (props: {
projectId: string;
filter: FilterState;
searchQuery?: string;
searchType?: TracingSearchType[];
orderBy?: OrderByState;
limit?: number;
page?: number;
clickhouseConfigs?: ClickHouseClientConfigOptions | undefined;
}) => {
const {
projectId,
filter,
searchQuery,
searchType,
orderBy,
limit,
page,
clickhouseConfigs,
} = props;
const identifiers = await getTracesTableGeneric({
select: "identifiers",
tags: { kind: "list" },
projectId,
filter,
searchQuery,
searchType,
orderBy,
limit,
page,
clickhouseConfigs,
});
return identifiers.map((row) => ({
id: row.id,
projectId: row.projectId,
timestamp: parseClickhouseUTCDateTimeFormat(row.timestamp),
}));
};
@@ -75,6 +75,7 @@ export const tableNames = [
"prompts",
"users",
"job_configurations",
"job_executions",
"dataset_items",
] as const;
+2 -1
View File
@@ -35,4 +35,5 @@ export type TableName =
| "dashboard"
| "widgets"
| "users"
| "eval_configs";
| "eval_configs"
| "job_executions";
+150 -88
View File
@@ -7,6 +7,8 @@ settings:
overrides:
nanoid: ^3.3.8
katex: ^0.16.21
tar-fs: ^2.1.2
rollup@^4.0.0: ^4.22.4
patchedDependencies:
next-auth@4.24.11:
@@ -58,7 +60,7 @@ importers:
version: 14.2.26(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-auth:
specifier: ^4.24.11
version: 4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
version: 4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@14.2.26(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
zod:
specifier: ^3.24.4
version: 3.24.4
@@ -150,8 +152,8 @@ importers:
specifier: ^12.26.0
version: 12.26.0
'@clickhouse/client':
specifier: ^1.4.0
version: 1.4.0
specifier: ^1.11.1
version: 1.11.1
'@google-cloud/storage':
specifier: ^7.15.2
version: 7.15.2
@@ -232,7 +234,7 @@ importers:
version: 4.0.2
next-auth:
specifier: ^4.24.11
version: 4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
version: 4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@14.2.26(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
nodemailer:
specifier: ^6.9.15
version: 6.9.15
@@ -635,7 +637,7 @@ importers:
version: 14.2.26(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-auth:
specifier: ^4.24.11
version: 4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
version: 4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@14.2.26(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-query-params:
specifier: ^5.0.1
version: 5.0.1(next@14.2.26(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)(use-query-params@2.2.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))
@@ -865,9 +867,6 @@ importers:
'@appsignal/opentelemetry-instrumentation-bullmq':
specifier: ^0.7.3
version: 0.7.3(bullmq@5.34.10)
'@clickhouse/client':
specifier: ^1.4.0
version: 1.4.0
'@langfuse/shared':
specifier: workspace:*
version: link:../packages/shared
@@ -1736,11 +1735,11 @@ packages:
'@chevrotain/utils@11.0.3':
resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==}
'@clickhouse/client-common@1.4.0':
resolution: {integrity: sha512-kglG8YyWnR1K24RckUf5ZdNTN0U0s+a1j/bpCO4ZjzjO87ICgWlXFVD22pZqSACW7/2IIi1IkzbwtxKI2s/MOw==}
'@clickhouse/client-common@1.11.1':
resolution: {integrity: sha512-bme0le2yhDSAh13d2fxhSW5ZrNoVqZ3LTyac8jK6hNH0qkksXnjYkLS6KQalPU6NMpffxHmpI4+/Gi2MnX0NCA==}
'@clickhouse/client@1.4.0':
resolution: {integrity: sha512-O4mbFPM/wQtFck01ghYI2mnNHv9jSFEiQBsTCH4t6MKeGHNAPkJGaFGv+KycLTv6zjnQNjiUGdXDMVRema5SyA==}
'@clickhouse/client@1.11.1':
resolution: {integrity: sha512-u9h++h72SmWystijNqfNvMkfA+5+Y1LNfmLL/odCL3VgI3oyAPP9ubSw/Yrt2zRZkLKehMMD1kuOej0QHbSoBA==}
engines: {node: '>=16'}
'@codemirror/autocomplete@6.17.0':
@@ -4352,7 +4351,7 @@ packages:
resolution: {integrity: sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==}
engines: {node: '>=16.0.0 || 14 >= 14.17'}
peerDependencies:
rollup: ^2.68.0||^3.0.0||^4.0.0
rollup: ^4.22.4
peerDependenciesMeta:
rollup:
optional: true
@@ -4361,73 +4360,108 @@ packages:
resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
rollup: ^4.22.4
peerDependenciesMeta:
rollup:
optional: true
'@rollup/rollup-android-arm-eabi@4.13.0':
resolution: {integrity: sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==}
'@rollup/rollup-android-arm-eabi@4.41.1':
resolution: {integrity: sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.13.0':
resolution: {integrity: sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==}
'@rollup/rollup-android-arm64@4.41.1':
resolution: {integrity: sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.13.0':
resolution: {integrity: sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==}
'@rollup/rollup-darwin-arm64@4.41.1':
resolution: {integrity: sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.13.0':
resolution: {integrity: sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==}
'@rollup/rollup-darwin-x64@4.41.1':
resolution: {integrity: sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-linux-arm-gnueabihf@4.13.0':
resolution: {integrity: sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==}
'@rollup/rollup-freebsd-arm64@4.41.1':
resolution: {integrity: sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.41.1':
resolution: {integrity: sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.41.1':
resolution: {integrity: sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.13.0':
resolution: {integrity: sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==}
'@rollup/rollup-linux-arm-musleabihf@4.41.1':
resolution: {integrity: sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.41.1':
resolution: {integrity: sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-musl@4.13.0':
resolution: {integrity: sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==}
'@rollup/rollup-linux-arm64-musl@4.41.1':
resolution: {integrity: sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.13.0':
resolution: {integrity: sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==}
'@rollup/rollup-linux-loongarch64-gnu@4.41.1':
resolution: {integrity: sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-powerpc64le-gnu@4.41.1':
resolution: {integrity: sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.41.1':
resolution: {integrity: sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.13.0':
resolution: {integrity: sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==}
'@rollup/rollup-linux-riscv64-musl@4.41.1':
resolution: {integrity: sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.41.1':
resolution: {integrity: sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==}
cpu: [s390x]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.41.1':
resolution: {integrity: sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-musl@4.13.0':
resolution: {integrity: sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==}
'@rollup/rollup-linux-x64-musl@4.41.1':
resolution: {integrity: sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==}
cpu: [x64]
os: [linux]
'@rollup/rollup-win32-arm64-msvc@4.13.0':
resolution: {integrity: sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==}
'@rollup/rollup-win32-arm64-msvc@4.41.1':
resolution: {integrity: sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.13.0':
resolution: {integrity: sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==}
'@rollup/rollup-win32-ia32-msvc@4.41.1':
resolution: {integrity: sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.13.0':
resolution: {integrity: sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==}
'@rollup/rollup-win32-x64-msvc@4.41.1':
resolution: {integrity: sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==}
cpu: [x64]
os: [win32]
@@ -10684,8 +10718,8 @@ packages:
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
rollup@4.13.0:
resolution: {integrity: sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==}
rollup@4.41.1:
resolution: {integrity: sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
@@ -11215,8 +11249,8 @@ packages:
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
engines: {node: '>=6'}
tar-fs@2.1.1:
resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
tar-fs@2.1.3:
resolution: {integrity: sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==}
tar-stream@2.2.0:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
@@ -13703,11 +13737,11 @@ snapshots:
'@chevrotain/utils@11.0.3': {}
'@clickhouse/client-common@1.4.0': {}
'@clickhouse/client-common@1.11.1': {}
'@clickhouse/client@1.4.0':
'@clickhouse/client@1.11.1':
dependencies:
'@clickhouse/client-common': 1.4.0
'@clickhouse/client-common': 1.11.1
'@codemirror/autocomplete@6.17.0(@codemirror/language@6.10.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)(@lezer/common@1.2.3)':
dependencies:
@@ -14703,7 +14737,7 @@ snapshots:
'@aws-sdk/client-bedrock-agent-runtime': 3.668.0
'@aws-sdk/client-bedrock-runtime': 3.668.0
'@aws-sdk/client-kendra': 3.668.0
'@aws-sdk/credential-provider-node': 3.675.0(@aws-sdk/client-sso-oidc@3.675.0(@aws-sdk/client-sts@3.675.0))(@aws-sdk/client-sts@3.675.0)
'@aws-sdk/credential-provider-node': 3.675.0(@aws-sdk/client-sso-oidc@3.668.0(@aws-sdk/client-sts@3.668.0))(@aws-sdk/client-sts@3.668.0)
'@langchain/core': 0.3.18(openai@4.82.0(zod@3.24.4))
zod: 3.24.4
zod-to-json-schema: 3.23.5(zod@3.24.4)
@@ -15081,7 +15115,7 @@ snapshots:
'@next-auth/prisma-adapter@1.0.7(@prisma/client@6.3.0(prisma@6.3.0(typescript@5.4.5))(typescript@5.4.5))(next-auth@4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@14.2.26(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))':
dependencies:
'@prisma/client': 6.3.0(prisma@6.3.0(typescript@5.4.5))(typescript@5.4.5)
next-auth: 4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-auth: 4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@14.2.26(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@next/env@14.2.26': {}
@@ -15944,7 +15978,7 @@ snapshots:
https-proxy-agent: 5.0.1
progress: 2.0.3
proxy-from-env: 1.1.0
tar-fs: 2.1.1
tar-fs: 2.1.3
unbzip2-stream: 1.4.3
yargs: 17.7.1
optionalDependencies:
@@ -16764,43 +16798,64 @@ snapshots:
optionalDependencies:
rollup: 3.29.5
'@rollup/rollup-android-arm-eabi@4.13.0':
'@rollup/rollup-android-arm-eabi@4.41.1':
optional: true
'@rollup/rollup-android-arm64@4.13.0':
'@rollup/rollup-android-arm64@4.41.1':
optional: true
'@rollup/rollup-darwin-arm64@4.13.0':
'@rollup/rollup-darwin-arm64@4.41.1':
optional: true
'@rollup/rollup-darwin-x64@4.13.0':
'@rollup/rollup-darwin-x64@4.41.1':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.13.0':
'@rollup/rollup-freebsd-arm64@4.41.1':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.13.0':
'@rollup/rollup-freebsd-x64@4.41.1':
optional: true
'@rollup/rollup-linux-arm64-musl@4.13.0':
'@rollup/rollup-linux-arm-gnueabihf@4.41.1':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.13.0':
'@rollup/rollup-linux-arm-musleabihf@4.41.1':
optional: true
'@rollup/rollup-linux-x64-gnu@4.13.0':
'@rollup/rollup-linux-arm64-gnu@4.41.1':
optional: true
'@rollup/rollup-linux-x64-musl@4.13.0':
'@rollup/rollup-linux-arm64-musl@4.41.1':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.13.0':
'@rollup/rollup-linux-loongarch64-gnu@4.41.1':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.13.0':
'@rollup/rollup-linux-powerpc64le-gnu@4.41.1':
optional: true
'@rollup/rollup-win32-x64-msvc@4.13.0':
'@rollup/rollup-linux-riscv64-gnu@4.41.1':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.41.1':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.41.1':
optional: true
'@rollup/rollup-linux-x64-gnu@4.41.1':
optional: true
'@rollup/rollup-linux-x64-musl@4.41.1':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.41.1':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.41.1':
optional: true
'@rollup/rollup-win32-x64-msvc@4.41.1':
optional: true
'@rushstack/eslint-patch@1.7.2': {}
@@ -18129,7 +18184,7 @@ snapshots:
'@typescript-eslint/eslint-plugin': 7.3.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
'@typescript-eslint/parser': 7.12.0(eslint@8.57.0)(typescript@5.4.5)
eslint-config-prettier: 9.1.0(eslint@8.57.0)
eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0))
eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0)
eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
@@ -20133,7 +20188,7 @@ snapshots:
eslint: 8.57.0
eslint-plugin-turbo: 1.13.4(eslint@8.57.0)
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)):
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)):
dependencies:
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
@@ -20224,7 +20279,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@@ -20251,7 +20306,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.12.0(eslint@8.57.0)(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@@ -23120,7 +23175,7 @@ snapshots:
dependencies:
type-fest: 2.19.0
next-auth@4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
next-auth@4.24.11(patch_hash=cczsf6i6qe2m2htirvgxjtoclu)(next@14.2.26(@babel/core@7.24.3)(@opentelemetry/api@1.9.0)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.15)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.26.0
'@panva/hkdf': 1.1.1
@@ -23999,7 +24054,7 @@ snapshots:
extract-zip: 2.0.1
https-proxy-agent: 5.0.1
proxy-from-env: 1.1.0
tar-fs: 2.1.1
tar-fs: 2.1.3
unbzip2-stream: 1.4.3
ws: 8.13.0
optionalDependencies:
@@ -24496,23 +24551,30 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
rollup@4.13.0:
rollup@4.41.1:
dependencies:
'@types/estree': 1.0.5
'@types/estree': 1.0.7
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.13.0
'@rollup/rollup-android-arm64': 4.13.0
'@rollup/rollup-darwin-arm64': 4.13.0
'@rollup/rollup-darwin-x64': 4.13.0
'@rollup/rollup-linux-arm-gnueabihf': 4.13.0
'@rollup/rollup-linux-arm64-gnu': 4.13.0
'@rollup/rollup-linux-arm64-musl': 4.13.0
'@rollup/rollup-linux-riscv64-gnu': 4.13.0
'@rollup/rollup-linux-x64-gnu': 4.13.0
'@rollup/rollup-linux-x64-musl': 4.13.0
'@rollup/rollup-win32-arm64-msvc': 4.13.0
'@rollup/rollup-win32-ia32-msvc': 4.13.0
'@rollup/rollup-win32-x64-msvc': 4.13.0
'@rollup/rollup-android-arm-eabi': 4.41.1
'@rollup/rollup-android-arm64': 4.41.1
'@rollup/rollup-darwin-arm64': 4.41.1
'@rollup/rollup-darwin-x64': 4.41.1
'@rollup/rollup-freebsd-arm64': 4.41.1
'@rollup/rollup-freebsd-x64': 4.41.1
'@rollup/rollup-linux-arm-gnueabihf': 4.41.1
'@rollup/rollup-linux-arm-musleabihf': 4.41.1
'@rollup/rollup-linux-arm64-gnu': 4.41.1
'@rollup/rollup-linux-arm64-musl': 4.41.1
'@rollup/rollup-linux-loongarch64-gnu': 4.41.1
'@rollup/rollup-linux-powerpc64le-gnu': 4.41.1
'@rollup/rollup-linux-riscv64-gnu': 4.41.1
'@rollup/rollup-linux-riscv64-musl': 4.41.1
'@rollup/rollup-linux-s390x-gnu': 4.41.1
'@rollup/rollup-linux-x64-gnu': 4.41.1
'@rollup/rollup-linux-x64-musl': 4.41.1
'@rollup/rollup-win32-arm64-msvc': 4.41.1
'@rollup/rollup-win32-ia32-msvc': 4.41.1
'@rollup/rollup-win32-x64-msvc': 4.41.1
fsevents: 2.3.3
roughjs@4.6.6:
@@ -25081,7 +25143,7 @@ snapshots:
tapable@2.2.1: {}
tar-fs@2.1.1:
tar-fs@2.1.3:
dependencies:
chownr: 1.1.4
mkdirp-classic: 0.5.3
@@ -25688,7 +25750,7 @@ snapshots:
dependencies:
esbuild: 0.20.2
postcss: 8.5.3
rollup: 4.13.0
rollup: 4.41.1
optionalDependencies:
'@types/node': 20.10.5
fsevents: 2.3.3
@@ -25698,7 +25760,7 @@ snapshots:
dependencies:
esbuild: 0.20.2
postcss: 8.5.3
rollup: 4.13.0
rollup: 4.41.1
optionalDependencies:
'@types/node': 20.11.29
fsevents: 2.3.3
@@ -25708,7 +25770,7 @@ snapshots:
dependencies:
esbuild: 0.20.2
postcss: 8.5.3
rollup: 4.13.0
rollup: 4.41.1
optionalDependencies:
'@types/node': 20.14.8
fsevents: 2.3.3
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "3.63.0",
"version": "3.65.3",
"private": true,
"license": "MIT",
"engines": {
+20 -5
View File
@@ -5268,19 +5268,19 @@ components:
type: number
format: double
nullable: true
description: Price (USD) per input unit
description: Deprecated. See 'prices' instead. Price (USD) per input unit
outputPrice:
type: number
format: double
nullable: true
description: Price (USD) per output unit
description: Deprecated. See 'prices' instead. Price (USD) per output unit
totalPrice:
type: number
format: double
nullable: true
description: >-
Price (USD) per total unit. Cannot be set if input or output price
is set.
Deprecated. See 'prices' instead. Price (USD) per total unit. Cannot
be set if input or output price is set.
tokenizerId:
type: string
nullable: true
@@ -5294,11 +5294,26 @@ components:
JSON. See docs for more details.
isLangfuseManaged:
type: boolean
prices:
type: object
additionalProperties:
$ref: '#/components/schemas/ModelPrice'
description: Price (USD) by usage type
required:
- id
- modelName
- matchPattern
- isLangfuseManaged
- prices
ModelPrice:
title: ModelPrice
type: object
properties:
price:
type: number
format: double
required:
- price
ModelUsageUnit:
title: ModelUsageUnit
type: string
@@ -6535,6 +6550,7 @@ components:
description: Metadata for the project
retentionDays:
type: integer
nullable: true
description: >-
Number of days to retain data. Null or 0 means no retention. Omitted
if no retention is configured.
@@ -6542,7 +6558,6 @@ components:
- id
- name
- metadata
- retentionDays
ProjectDeletionResponse:
title: ProjectDeletionResponse
type: object
@@ -38,6 +38,13 @@ describe("/models API Endpoints", () => {
unit: "TOKENS",
},
});
await prisma.price.createMany({
data: [
{ modelId: "model-1", usageType: "input", price: 0.001 },
{ modelId: "model-1", usageType: "output", price: 0.002 },
{ modelId: "model-1", usageType: "total", price: 0.1 },
],
});
await prisma.model.create({
data: {
id: "model-2",
@@ -51,6 +58,12 @@ describe("/models API Endpoints", () => {
unit: "TOKENS",
},
});
await prisma.price.createMany({
data: [
{ modelId: "model-2", usageType: "input", price: 0.002 },
{ modelId: "model-2", usageType: "output", price: 0.004 },
],
});
});
afterEach(async () => await pruneDatabase());
@@ -67,6 +80,11 @@ describe("/models API Endpoints", () => {
expect(models.body.data[0]).toMatchObject({
isLangfuseManaged: true,
modelName: "gpt-3.5-turbo",
prices: {
input: { price: 0.001 },
output: { price: 0.002 },
total: { price: 0.1 },
},
});
});
@@ -105,6 +123,10 @@ describe("/models API Endpoints", () => {
auth,
);
expect(customModel.body.isLangfuseManaged).toBe(false);
expect(customModel.body.prices).toMatchObject({
input: { price: 0.002 },
output: { price: 0.004 },
});
const models = await makeZodVerifiedAPICall(
GetModelsV1Response,
@@ -132,6 +154,10 @@ describe("/models API Endpoints", () => {
unit: "TOKENS",
tokenizerConfig: { tokensPerMessage: 3, tokensPerName: 1 },
isLangfuseManaged: false,
prices: {
input: { price: 0.002 },
output: { price: 0.004 },
},
});
const prices = await prisma.price.findMany({
@@ -459,7 +459,7 @@ describe("OTel Resource Span Mapping", () => {
expect(traceEvent.body).toMatchObject({
id: "95f3b926c7d009925bcb5dbc27311120",
timestamp: "2025-05-05T13:42:33.936Z",
name: "my-span-with-custom-trace-id",
name: undefined,
environment: "production",
});
});
+1 -1
View File
@@ -1,4 +1,4 @@
import chatCompletionHandler from "@/src/ee/features/playground/server/chatCompletionHandler";
import chatCompletionHandler from "@/src/features/playground/server/chatCompletionHandler";
export const dynamic = "force-dynamic";
export const maxDuration = 120;
+10 -4
View File
@@ -40,6 +40,7 @@ export type ModelParamsContext = {
setModelParamEnabled?: (key: keyof UIModelParams, enabled: boolean) => void;
formDisabled?: boolean;
modelParamsDescription?: string;
customHeader?: React.ReactNode;
};
export const ModelParameters: React.FC<ModelParamsContext> = ({
@@ -50,6 +51,7 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
setModelParamEnabled,
formDisabled = false,
modelParamsDescription,
customHeader,
}) => {
const projectId = useProjectIdFromURL();
const [modelSettingsOpen, setModelSettingsOpen] = useState(false);
@@ -74,9 +76,13 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
if (availableProviders.length === 0) {
return (
<div className="flex flex-col space-y-4 pr-1">
<div className="flex items-center justify-between">
<p className="font-semibold">Model</p>
</div>
{customHeader ? (
customHeader
) : (
<div className="flex items-center justify-between">
<p className="font-semibold">Model</p>
</div>
)}
<p className="text-xs">No LLM API key set in project. </p>
<CreateLLMApiKeyDialog />
</div>
@@ -86,7 +92,7 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
return (
<div className="flex flex-col space-y-2 pb-1 pr-1 pt-2">
<div className="flex items-center justify-between">
<p className="font-semibold">Model</p>
{customHeader ? customHeader : <p className="font-semibold">Model</p>}
<Popover open={modelSettingsOpen} onOpenChange={setModelSettingsOpen}>
<PopoverTrigger asChild>
<Button
+2 -1
View File
@@ -52,8 +52,9 @@ export const VersionLabel = ({ className }: { className?: string }) => {
const selfHostedPlanLabel = !isLangfuseCloud
? plan && isSelfHostedPlan(plan)
? // self-host plan
// TODO: clean up to use planLabels in packages/shared/src/features/entitlements/plans.ts
{
short: plan === "self-hosted:pro" ? "Pro" : "EE",
short: "EE",
long: planLabels[plan],
}
: // no plan, oss
+18 -14
View File
@@ -292,7 +292,7 @@ export function DeleteDashboardButton(props: DeleteButtonProps) {
);
}
export function DeleteEvaluatorButton(props: DeleteButtonProps) {
export function DeleteEvalConfigButton(props: DeleteButtonProps) {
const utils = api.useUtils();
const {
itemId,
@@ -300,21 +300,29 @@ export function DeleteEvaluatorButton(props: DeleteButtonProps) {
scope = "evalJob:CUD",
invalidateFunc = () => void utils.evals.invalidate(),
} = props;
const evaluatorMutation = api.evals.deleteEvalJob.useMutation();
const evaluatorMutation = api.evals.deleteEvalJob.useMutation({
onSuccess: () => {
showSuccessToast({
title: "Running evaluator deleted",
description: "The running evaluator has been deleted successfully",
});
void utils.evals.invalidate();
},
});
const executeDeleteMutation = async (onSuccess: () => void) => {
try {
await evaluatorMutation.mutateAsync({
evalConfigId: itemId,
projectId,
});
onSuccess();
} catch (error) {
return Promise.reject(error);
}
onSuccess();
};
const hasModelBasedEvaluationEntitlement = useHasEntitlement(
"model-based-evaluations",
);
return (
<DeleteButton
{...props}
@@ -322,18 +330,17 @@ export function DeleteEvaluatorButton(props: DeleteButtonProps) {
invalidateFunc={invalidateFunc}
captureDeleteOpen={(capture, isTableAction) =>
capture("eval_config:delete_form_open", {
source: isTableAction ? "table-single-row" : "evaluator",
source: isTableAction ? "table-single-row" : "eval config detail",
})
}
captureDeleteSuccess={(capture, isTableAction) =>
capture("eval_config:delete_evaluator_button_click", {
source: isTableAction ? "table-single-row" : "evaluator",
source: isTableAction ? "table-single-row" : "eval config detail",
})
}
entityToDeleteName="evaluator"
entityToDeleteName="running evaluator"
executeDeleteMutation={executeDeleteMutation}
isDeleteMutationLoading={evaluatorMutation.isLoading}
enabled={hasModelBasedEvaluationEntitlement}
/>
);
}
@@ -370,9 +377,7 @@ export function DeleteEvaluationModelButton(
}
onSuccess();
};
const hasModelBasedEvaluationEntitlement = useHasEntitlement(
"model-based-evaluations",
);
return (
<DeleteButton
{...props}
@@ -394,7 +399,6 @@ export function DeleteEvaluationModelButton(
deleteConfirmation="delete"
executeDeleteMutation={executeDeleteMutation}
isDeleteMutationLoading={isLoading}
enabled={hasModelBasedEvaluationEntitlement}
/>
);
}
+16 -3
View File
@@ -22,6 +22,19 @@ const partitionScores = <T extends APIScoreV2 | LastUserScore>(
return { visibleScores, hiddenScores };
};
const hasMetadata = (score: APIScoreV2 | LastUserScore) => {
if (!score.metadata) return false;
try {
const metadata =
typeof score.metadata === "string"
? JSON.parse(score.metadata)
: score.metadata;
return Object.keys(metadata).length > 0;
} catch {
return false;
}
};
const ScoreGroupBadge = <T extends APIScoreV2 | LastUserScore>({
name,
scores,
@@ -50,17 +63,17 @@ const ScoreGroupBadge = <T extends APIScoreV2 | LastUserScore>({
<HoverCardTrigger className="inline-block">
<MessageCircleMoreIcon className="mb-[0.0625rem] !size-3" />
</HoverCardTrigger>
<HoverCardContent className="overflow-hidden whitespace-normal break-normal">
<HoverCardContent className="max-h-[50dvh] overflow-y-auto whitespace-normal break-normal">
<p className="whitespace-pre-wrap">{s.comment}</p>
</HoverCardContent>
</HoverCard>
)}
{s.metadata && Object.keys(s.metadata).length > 0 && (
{hasMetadata(s) && (
<HoverCard>
<HoverCardTrigger className="inline-block">
<BracesIcon className="mb-[0.0625rem] !size-3" />
</HoverCardTrigger>
<HoverCardContent className="overflow-hidden whitespace-normal break-normal rounded-md border-none p-0">
<HoverCardContent className="max-h-[50dvh] overflow-y-auto whitespace-normal break-normal rounded-md border-none p-0">
<JSONView codeClassName="!rounded-md" json={s.metadata} />
</HoverCardContent>
</HoverCard>
-5
View File
@@ -73,7 +73,6 @@ export const ROUTES: Route[] = [
title: "Dashboards",
pathname: `/project/[projectId]/dashboards`,
icon: LayoutDashboard,
entitlements: ["custom-dashboards"],
productModule: "dashboards",
},
{
@@ -105,19 +104,16 @@ export const ROUTES: Route[] = [
icon: Lightbulb,
pathname: `/project/[projectId]/annotation-queues`,
productModule: "evaluation",
entitlements: ["annotation-queues", "model-based-evaluations"],
projectRbacScopes: ["annotationQueues:read", "evalJob:read"],
items: [
{
title: "Human Annotation",
pathname: `/project/[projectId]/annotation-queues`,
projectRbacScopes: ["annotationQueues:read"],
entitlements: ["annotation-queues"],
},
{
title: "LLM-as-a-Judge",
pathname: `/project/[projectId]/evals`,
entitlements: ["model-based-evaluations"],
projectRbacScopes: ["evalJob:read"],
},
],
@@ -140,7 +136,6 @@ export const ROUTES: Route[] = [
pathname: "/project/[projectId]/playground",
icon: TerminalIcon,
productModule: "playground",
entitlements: ["playground"],
},
{
title: "Datasets",
@@ -4,7 +4,7 @@ import {
type ValueProposition,
} from "@/src/components/ui/splash-screen";
import { ClipboardCheck, Users, BarChart4, GitMerge } from "lucide-react";
import { CreateOrEditAnnotationQueueButton } from "@/src/ee/features/annotation-queues/components/CreateOrEditAnnotationQueueButton";
import { CreateOrEditAnnotationQueueButton } from "@/src/features/annotation-queues/components/CreateOrEditAnnotationQueueButton";
export function AnnotationQueuesOnboarding({
projectId,
@@ -136,7 +136,7 @@ export function DataTableToolbar<TData, TValue>({
<div className={cn("grid h-fit w-full gap-0 px-2", className)}>
<div className="my-2 flex flex-wrap items-center gap-2 @container">
{searchConfig && (
<div className="flex w-full max-w-sm items-stretch">
<div className="flex min-w-0 max-w-64 flex-shrink-0 items-stretch">
<div
className={cn(
"flex h-8 flex-1 items-center border border-input bg-background pl-2",
@@ -180,13 +180,13 @@ export function DataTableToolbar<TData, TValue>({
<Button
variant="outline"
size="default"
className="flex w-auto min-w-[130px] items-center justify-between gap-1 rounded-l-none border-l-0"
className="w-30 flex items-center justify-between gap-1 rounded-l-none border-l-0"
>
<span className="flex items-center gap-1">
<span className="flex items-center gap-1 truncate">
{searchConfig.tableAllowsFullTextSearch &&
(searchConfig.searchType ?? []).includes("content")
? "Metadata + Full Text"
: "Metadata"}
? "Full Text"
: "IDs / Names"}
<DocPopup
description={
<>
@@ -242,13 +242,13 @@ export function DataTableToolbar<TData, TValue>({
}}
>
<DropdownMenuRadioItem value="metadata">
Metadata
IDs / Names
</DropdownMenuRadioItem>
<DropdownMenuRadioItem
value="metadata_fulltext"
disabled={!searchConfig.tableAllowsFullTextSearch}
>
Metadata + Full Text
Full Text
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
@@ -1,4 +1,4 @@
import { type EvalsTemplateRow } from "@/src/ee/features/evals/components/eval-templates-table";
import { type EvalsTemplateRow } from "@/src/features/evals/components/eval-templates-table";
import { type ListEntry } from "@/src/features/navigate-detail-pages/context";
import { useRouter } from "next/router";
@@ -1,9 +1,9 @@
import { usePeekState } from "@/src/components/table/peek/hooks/usePeekState";
import { type EvaluatorDataRow } from "@/src/ee/features/evals/components/evaluator-table";
import { type EvaluatorDataRow } from "@/src/features/evals/components/evaluator-table";
import { Skeleton } from "@/src/components/ui/skeleton";
import TableLink from "@/src/components/table/table-link";
import { CardDescription } from "@/src/components/ui/card";
import { EvaluatorForm } from "@/src/ee/features/evals/components/evaluator-form";
import { EvaluatorForm } from "@/src/features/evals/components/evaluator-form";
import { usePeekEvalConfigData } from "@/src/components/table/peek/hooks/usePeekEvalConfigData";
import {
Tooltip,
@@ -13,7 +13,7 @@ import {
import { LangfuseIcon } from "@/src/components/LangfuseLogo";
import { UserCircle2Icon } from "lucide-react";
import { StatusBadge } from "@/src/components/layouts/status-badge";
import { DeactivateEvalConfig } from "@/src/ee/features/evals/components/deactivate-config";
import { DeactivateEvalConfig } from "@/src/features/evals/components/deactivate-config";
export const PeekViewEvaluatorConfigDetail = ({
projectId,
@@ -1,9 +1,9 @@
import { usePeekState } from "@/src/components/table/peek/hooks/usePeekState";
import { Skeleton } from "@/src/components/ui/skeleton";
import { usePeekEvalTemplateData } from "@/src/components/table/peek/hooks/usePeekEvalTemplateData";
import { EvalTemplateForm } from "@/src/ee/features/evals/components/template-form";
import { MaintainerTooltip } from "@/src/ee/features/evals/components/maintainer-tooltip";
import { getMaintainer } from "@/src/ee/features/evals/utils/typeHelpers";
import { EvalTemplateForm } from "@/src/features/evals/components/template-form";
import { MaintainerTooltip } from "@/src/features/evals/components/maintainer-tooltip";
import { getMaintainer } from "@/src/features/evals/utils/typeHelpers";
export const PeekViewEvaluatorTemplateDetail = ({
projectId,
@@ -427,7 +427,6 @@ export default function TracesTable({
execute: handleAddToAnnotationQueue,
accessCheck: {
scope: "annotationQueues:CUD",
entitlement: "annotation-queues",
},
},
];
@@ -1076,7 +1075,7 @@ export default function TracesTable({
}}
filterColumnDefinition={transformedFilterOptions}
searchConfig={{
metadataSearchFields: ["ID", "Name", "Trace Name", "User ID"],
metadataSearchFields: ["ID", "Trace Name", "User ID"],
updateQuery: setSearchQuery,
currentQuery: searchQuery ?? undefined,
tableAllowsFullTextSearch: true,
+10 -12
View File
@@ -9,14 +9,13 @@ import Link from "next/link";
import { usdFormatter } from "@/src/utils/numbers";
import { withDefault, StringParam, useQueryParam } from "use-query-params";
import ScoresTable from "@/src/components/table/use-cases/scores";
import { JumpToPlaygroundButton } from "@/src/ee/features/playground/page/components/JumpToPlaygroundButton";
import { JumpToPlaygroundButton } from "@/src/features/playground/page/components/JumpToPlaygroundButton";
import { AnnotateDrawer } from "@/src/features/scores/components/AnnotateDrawer";
import useLocalStorage from "@/src/components/useLocalStorage";
import { CommentDrawerButton } from "@/src/features/comments/CommentDrawerButton";
import { cn } from "@/src/utils/tailwind";
import { NewDatasetItemFromExistingObject } from "@/src/features/datasets/components/NewDatasetItemFromExistingObject";
import { CreateNewAnnotationQueueItem } from "@/src/ee/features/annotation-queues/components/CreateNewAnnotationQueueItem";
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
import { CreateNewAnnotationQueueItem } from "@/src/features/annotation-queues/components/CreateNewAnnotationQueueItem";
import { calculateDisplayTotalCost } from "@/src/components/trace/lib/helpers";
import { Fragment, useMemo, useState } from "react";
import { useIsAuthenticatedAndProjectMember } from "@/src/features/auth/hooks";
@@ -65,7 +64,7 @@ export const ObservationPreview = ({
const [emptySelectedConfigIds, setEmptySelectedConfigIds] = useLocalStorage<
string[]
>("emptySelectedConfigIds", []);
const hasEntitlement = useHasEntitlement("annotation-queues");
const isAuthenticatedAndProjectMember =
useIsAuthenticatedAndProjectMember(projectId);
const router = useRouter();
@@ -163,16 +162,15 @@ export const ObservationPreview = ({
scores={scores}
emptySelectedConfigIds={emptySelectedConfigIds}
setEmptySelectedConfigIds={setEmptySelectedConfigIds}
hasGroupedButton={hasEntitlement}
hasGroupedButton={true}
environment={preloadedObservation.environment}
/>
{hasEntitlement && (
<CreateNewAnnotationQueueItem
projectId={projectId}
objectId={preloadedObservation.id}
objectType={AnnotationQueueObjectType.OBSERVATION}
/>
)}
<CreateNewAnnotationQueueItem
projectId={projectId}
objectId={preloadedObservation.id}
objectType={AnnotationQueueObjectType.OBSERVATION}
/>
</div>
{observationWithInputAndOutput.data?.type === "GENERATION" && (
<JumpToPlaygroundButton
+7 -11
View File
@@ -16,8 +16,7 @@ import useLocalStorage from "@/src/components/useLocalStorage";
import { CommentDrawerButton } from "@/src/features/comments/CommentDrawerButton";
import { api } from "@/src/utils/api";
import { NewDatasetItemFromExistingObject } from "@/src/features/datasets/components/NewDatasetItemFromExistingObject";
import { CreateNewAnnotationQueueItem } from "@/src/ee/features/annotation-queues/components/CreateNewAnnotationQueueItem";
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
import { CreateNewAnnotationQueueItem } from "@/src/features/annotation-queues/components/CreateNewAnnotationQueueItem";
import { useMemo, useState } from "react";
import { usdFormatter } from "@/src/utils/numbers";
import { calculateDisplayTotalCost } from "@/src/components/trace/lib/helpers";
@@ -65,7 +64,6 @@ export const TracePreview = ({
const [emptySelectedConfigIds, setEmptySelectedConfigIds] = useLocalStorage<
string[]
>("emptySelectedConfigIds", []);
const hasEntitlement = useHasEntitlement("annotation-queues");
const isAuthenticatedAndProjectMember = useIsAuthenticatedAndProjectMember(
trace.projectId,
);
@@ -139,16 +137,14 @@ export const TracePreview = ({
scores={scores}
emptySelectedConfigIds={emptySelectedConfigIds}
setEmptySelectedConfigIds={setEmptySelectedConfigIds}
hasGroupedButton={hasEntitlement}
hasGroupedButton={true}
environment={trace.environment}
/>
{hasEntitlement && (
<CreateNewAnnotationQueueItem
projectId={trace.projectId}
objectId={trace.id}
objectType={AnnotationQueueObjectType.TRACE}
/>
)}
<CreateNewAnnotationQueueItem
projectId={trace.projectId}
objectId={trace.id}
objectType={AnnotationQueueObjectType.TRACE}
/>
</div>
<CommentDrawerButton
projectId={trace.projectId}
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v3.63.0";
export const VERSION = "v3.65.3";
@@ -68,7 +68,7 @@ export const stripeProducts: StripeProduct[] = [
: "prod_QhK9qKGH25BTcS", // live
mappedPlan: "cloud:team",
checkout: {
title: "Team",
title: "Pro + Teams Add-on",
description: "Organizational and security controls for larger teams.",
price: "$499 / month",
usagePrice: "$8/100k events (100k included)",
@@ -1,60 +0,0 @@
import { InfoIcon, Loader } from "lucide-react";
import { type EvalFormType } from "@/src/ee/features/evals/utils/evaluator-form-utils";
import { api } from "@/src/utils/api";
import { useState } from "react";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/src/components/ui/tooltip";
import { compactNumberFormatter } from "@/src/utils/numbers";
import { useEvalTargetCount } from "@/src/ee/features/evals/hooks/useEvalTargetCount";
type ExecutionCountTooltipProps = {
projectId: string;
item: string;
filter: EvalFormType["filter"];
};
export const ExecutionCountTooltip = ({
projectId,
item,
filter,
}: ExecutionCountTooltipProps) => {
const [isOpen, setIsOpen] = useState(false);
const globalConfig = api.evals.globalJobConfigs.useQuery({
projectId,
});
const { isLoading, totalCount, isTraceTarget } = useEvalTargetCount({
projectId,
item,
filter,
enabled: isOpen, // utilize `isOpen` to only query if user hovers over tooltip to avoid unnecessary queries
});
return (
<Tooltip open={isOpen} onOpenChange={setIsOpen}>
<TooltipTrigger>
<InfoIcon className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent>
<div className="text-sm">
We execute the evaluation on{" "}
{isLoading ? (
<Loader className="inline-block h-4 w-4 animate-spin" />
) : (
compactNumberFormatter(
!globalConfig.data ||
(totalCount && totalCount < globalConfig.data)
? totalCount
: globalConfig.data,
)
)}{" "}
{isTraceTarget ? "traces" : "dataset run items"}.
</div>
</TooltipContent>
</Tooltip>
);
};
@@ -1,31 +0,0 @@
import { ExternalLink } from "lucide-react";
import { CardContent } from "@/src/components/ui/card";
import { Card } from "@/src/components/ui/card";
import Link from "next/link";
export function SetupDefaultEvalModelCard({
projectId,
}: {
projectId: string;
}) {
return (
<Card className="mt-2 border-dark-yellow bg-light-yellow">
<CardContent className="flex flex-col gap-1">
<p className="mt-2 text-sm font-semibold">
This evaluator requires a default evaluation model
</p>
<p className="text-xs text-muted-foreground">
Please set up a default evaluation model for your project.
</p>
<Link
href={`/project/${projectId}/evals/default-model`}
className="mt-2 flex items-center text-sm text-blue-500 hover:underline"
target="_blank"
rel="noopener noreferrer"
>
Configure default model <ExternalLink className="ml-1" size={14} />
</Link>
</CardContent>
</Card>
);
}
@@ -1,185 +0,0 @@
import Page from "@/src/components/layouts/page";
import { useRouter } from "next/router";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
import { SupportOrUpgradePage } from "@/src/ee/features/billing/components/SupportOrUpgradePage";
import { ModelParameters } from "@/src/components/ModelParameters";
import { CardContent } from "@/src/components/ui/card";
import { Card } from "@/src/components/ui/card";
import { useModelParams } from "@/src/ee/features/playground/page/hooks/useModelParams";
import { Button } from "@/src/components/ui/button";
import { api } from "@/src/utils/api";
import { showSuccessToast } from "@/src/features/notifications/showSuccessToast";
import { Skeleton } from "@/src/components/ui/skeleton";
import { useEvaluationModel } from "@/src/ee/features/evals/hooks/useEvaluationModel";
import { DeleteEvaluationModelButton } from "@/src/components/deleteButton";
export default function DefaultEvaluationModelPage() {
const router = useRouter();
const projectId = router.query.projectId as string;
const utils = api.useUtils();
const hasEntitlement = useHasEntitlement("model-based-evaluations");
const hasWriteAccess = useHasProjectAccess({
projectId,
scope: "evalDefaultModel:CUD",
});
const hasReadAccess = useHasProjectAccess({
projectId,
scope: "evalDefaultModel:read",
});
const {
modelParams,
setModelParams,
updateModelParamValue,
setModelParamEnabled,
availableModels,
availableProviders,
} = useModelParams();
const { selectedModel, isDefaultModelLoading } = useEvaluationModel(
projectId,
setModelParams,
);
const { mutate: upsertDefaultModel } =
api.defaultLlmModel.upsertDefaultModel.useMutation({
onSuccess: () => {
showSuccessToast({
title: "Default evaluation model updated",
description: "All running evaluators will use the new model.",
});
utils.defaultLlmModel.fetchDefaultModel.invalidate({ projectId });
},
});
if (isDefaultModelLoading) {
return <Skeleton className="h-[500px] w-full" />;
}
if (!hasReadAccess || !hasEntitlement) {
return <SupportOrUpgradePage />;
}
return (
<>
<Page
withPadding
headerProps={{
title: "Default Evaluation Model",
help: {
description:
"Configure a default evaluation model for your project.",
href: "https://langfuse.com/docs/scores/model-based-evals",
},
breadcrumb: [
{
name: "Evaluator Library",
href: `/project/${projectId}/evals/templates`,
},
],
}}
>
{selectedModel ? (
<div className="flex flex-col gap-6">
<Card className="border-dark-green bg-light-green">
<CardContent className="flex flex-col gap-1">
<p className="mt-2 text-sm font-semibold">
Default evaluation model selected
</p>
<p className="text-xs text-muted-foreground">
This project will use the default evaluation model:
<span className="font-medium">
{" "}
{selectedModel.provider} / {selectedModel.model}
</span>
</p>
</CardContent>
</Card>
</div>
) : (
<div className="flex flex-col gap-6">
<Card className="border-dark-yellow bg-light-yellow">
<CardContent className="flex flex-col gap-1">
<p className="mt-2 text-sm font-semibold">
No default evaluation model selected
</p>
<p className="text-xs text-muted-foreground">
Select a model to use as the default evaluation model for your
project.
</p>
</CardContent>
</Card>
</div>
)}
<Card className="mt-3 flex flex-col gap-6">
<CardContent>
<ModelParameters
{...{
modelParams,
availableModels,
availableProviders,
updateModelParamValue,
setModelParamEnabled,
}}
formDisabled={!hasWriteAccess}
/>
<div className="my-2 text-xs text-muted-foreground">
Select a model which supports function calling.
</div>
</CardContent>
</Card>
{selectedModel ? (
<div className="mt-2 flex justify-end gap-2">
<DeleteEvaluationModelButton
projectId={projectId}
scope="evalDefaultModel:CUD"
/>
<Button
disabled={!hasWriteAccess}
onClick={() => {
upsertDefaultModel({
projectId,
provider: modelParams.provider.value,
adapter: modelParams.adapter.value,
model: modelParams.model.value,
modelParams: {
max_tokens: modelParams.max_tokens.value,
temperature: modelParams.temperature.value,
top_p: modelParams.top_p.value,
},
});
}}
>
Update as default
</Button>
</div>
) : (
<div className="mt-3 flex justify-end">
<Button
disabled={!hasWriteAccess || !modelParams.provider.value}
onClick={() => {
upsertDefaultModel({
projectId,
provider: modelParams.provider.value,
adapter: modelParams.adapter.value,
model: modelParams.model.value,
modelParams: {
max_tokens: modelParams.max_tokens.value,
temperature: modelParams.temperature.value,
top_p: modelParams.top_p.value,
},
});
}}
>
Set as default
</Button>
</div>
)}
</Page>
</>
);
}
@@ -1,9 +1,8 @@
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import useSessionStorage from "@/src/components/useSessionStorage";
import { SupportOrUpgradePage } from "@/src/ee/features/billing/components/SupportOrUpgradePage";
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import { AnnotationQueueItemPage } from "@/src/ee/features/annotation-queues/components/AnnotationQueueItemPage";
import { AnnotationQueueItemPage } from "@/src/features/annotation-queues/components/AnnotationQueueItemPage";
import { api } from "@/src/utils/api";
import { Goal, Network } from "lucide-react";
import Page from "@/src/components/layouts/page";
@@ -21,7 +20,6 @@ export const AnnotationQueuesItem = ({
projectId,
scope: "annotationQueues:read",
});
const hasEntitlement = useHasEntitlement("annotation-queues");
const queue = api.annotationQueues.byId.useQuery(
{
@@ -43,7 +41,7 @@ export const AnnotationQueuesItem = ({
"hideTree",
);
if (!hasAccess || !hasEntitlement) return <SupportOrUpgradePage />;
if (!hasAccess) return <SupportOrUpgradePage />;
return (
<Page
@@ -7,7 +7,7 @@ import useColumnVisibility from "@/src/features/column-visibility/hooks/useColum
import { DataTableToolbar } from "@/src/components/table/data-table-toolbar";
import { useRowHeightLocalStorage } from "@/src/components/table/data-table-row-height-switch";
import useColumnOrder from "@/src/features/column-visibility/hooks/useColumnOrder";
import { CreateOrEditAnnotationQueueButton } from "@/src/ee/features/annotation-queues/components/CreateOrEditAnnotationQueueButton";
import { CreateOrEditAnnotationQueueButton } from "@/src/features/annotation-queues/components/CreateOrEditAnnotationQueueButton";
import { type ScoreDataType } from "@langfuse/shared";
import { getScoreDataTypeIcon } from "@/src/features/scores/components/ScoreDetailColumnHelpers";
import {
@@ -22,7 +22,7 @@ import { cn } from "@/src/utils/tailwind";
import TableLink from "@/src/components/table/table-link";
import Link from "next/link";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import { DeleteAnnotationQueueButton } from "@/src/ee/features/annotation-queues/components/DeleteAnnotationQueueButton";
import { DeleteAnnotationQueueButton } from "@/src/features/annotation-queues/components/DeleteAnnotationQueueButton";
type RowData = {
key: {
@@ -33,10 +33,7 @@ import { getScoreDataTypeIcon } from "@/src/features/scores/components/ScoreDeta
import { MultiSelectKeyValues } from "@/src/features/scores/components/multi-select-key-values";
import { useRouter } from "next/router";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import {
useHasEntitlement,
useEntitlementLimit,
} from "@/src/features/entitlements/hooks";
import { useEntitlementLimit } from "@/src/features/entitlements/hooks";
import { ActionButton } from "@/src/components/ActionButton";
import { DropdownMenuItem } from "@/src/components/ui/dropdown-menu";
import { useUniqueNameValidation } from "@/src/hooks/useUniqueNameValidation";
@@ -57,7 +54,6 @@ export const CreateOrEditAnnotationQueueButton = ({
projectId: projectId,
scope: "annotationQueues:CUD",
});
const hasEntitlement = useHasEntitlement("annotation-queues");
const queueLimit = useEntitlementLimit("annotation-queue-count");
const router = useRouter();
const capture = usePostHogClientCapture();
@@ -186,7 +182,6 @@ export const CreateOrEditAnnotationQueueButton = ({
)
}
hasAccess={hasAccess}
hasEntitlement={hasEntitlement}
limitValue={queueCountData.data}
limit={queueLimit}
size={size}
@@ -1,15 +1,14 @@
import { api } from "@/src/utils/api";
import { useRouter } from "next/router";
import { AnnotationQueueItemsTable } from "@/src/ee/features/annotation-queues/components/AnnotationQueueItemsTable";
import { AnnotationQueueItemsTable } from "@/src/features/annotation-queues/components/AnnotationQueueItemsTable";
import { CardDescription } from "@/src/components/ui/card";
import { Button } from "@/src/components/ui/button";
import { ClipboardPen, Lock } from "lucide-react";
import { Badge } from "@/src/components/ui/badge";
import { getScoreDataTypeIcon } from "@/src/features/scores/components/ScoreDetailColumnHelpers";
import Link from "next/link";
import { CreateOrEditAnnotationQueueButton } from "@/src/ee/features/annotation-queues/components/CreateOrEditAnnotationQueueButton";
import { CreateOrEditAnnotationQueueButton } from "@/src/features/annotation-queues/components/CreateOrEditAnnotationQueueButton";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
import { SupportOrUpgradePage } from "@/src/ee/features/billing/components/SupportOrUpgradePage";
import { Skeleton } from "@/src/components/ui/skeleton";
import Page from "@/src/components/layouts/page";
@@ -39,8 +38,8 @@ export default function QueueItems() {
projectId,
scope: "annotationQueues:CUD",
});
const hasEntitlement = useHasEntitlement("annotation-queues");
if (!hasReadAccess || !hasEntitlement) return <SupportOrUpgradePage />;
if (!hasReadAccess) return <SupportOrUpgradePage />;
return (
<Page
@@ -1,12 +1,11 @@
import { useRouter } from "next/router";
import { AnnotationQueuesTable } from "@/src/ee/features/annotation-queues/components/AnnotationQueuesTable";
import { AnnotationQueuesTable } from "@/src/features/annotation-queues/components/AnnotationQueuesTable";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import { useHasEntitlement } from "@/src/features/entitlements/hooks";
import { SupportOrUpgradePage } from "@/src/ee/features/billing/components/SupportOrUpgradePage";
import Page from "@/src/components/layouts/page";
import { AnnotationQueuesOnboarding } from "@/src/components/onboarding/AnnotationQueuesOnboarding";
import { api } from "@/src/utils/api";
import { CreateOrEditAnnotationQueueButton } from "@/src/ee/features/annotation-queues/components/CreateOrEditAnnotationQueueButton";
import { CreateOrEditAnnotationQueueButton } from "@/src/features/annotation-queues/components/CreateOrEditAnnotationQueueButton";
export default function AnnotationQueues() {
const router = useRouter();
@@ -15,13 +14,12 @@ export default function AnnotationQueues() {
projectId: projectId,
scope: "annotationQueues:read",
});
const hasEntitlement = useHasEntitlement("annotation-queues");
// Check if the user has any annotation queues
const { data: hasAnyQueue, isLoading } = api.annotationQueues.hasAny.useQuery(
{ projectId },
{
enabled: !!projectId && hasEntitlement,
enabled: !!projectId,
trpc: {
context: {
skipBatch: true,
@@ -33,7 +31,7 @@ export default function AnnotationQueues() {
const showOnboarding = !isLoading && !hasAnyQueue;
if (!hasAccess || !hasEntitlement) return <SupportOrUpgradePage />;
if (!hasAccess) return <SupportOrUpgradePage />;
return (
<Page
@@ -1,5 +1,4 @@
import { auditLog } from "@/src/features/audit-logs/auditLog";
import { throwIfNoEntitlement } from "@/src/features/entitlements/server/hasEntitlement";
import { throwIfNoProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import { createBatchActionJob } from "@/src/features/table/server/createBatchActionJob";
import {
@@ -42,12 +41,6 @@ export const queueItemRouter = createTRPCRouter({
)
.query(async ({ input, ctx }) => {
try {
throwIfNoEntitlement({
entitlement: "annotation-queues",
projectId: input.projectId,
sessionUser: ctx.session.user,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -114,12 +107,6 @@ export const queueItemRouter = createTRPCRouter({
)
.query(async ({ input, ctx }) => {
try {
throwIfNoEntitlement({
entitlement: "annotation-queues",
projectId: input.projectId,
sessionUser: ctx.session.user,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -209,12 +196,6 @@ export const queueItemRouter = createTRPCRouter({
)
.query(async ({ input, ctx }) => {
try {
throwIfNoEntitlement({
entitlement: "annotation-queues",
projectId: input.projectId,
sessionUser: ctx.session.user,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -258,12 +239,6 @@ export const queueItemRouter = createTRPCRouter({
)
.mutation(async ({ input, ctx }) => {
try {
throwIfNoEntitlement({
entitlement: "annotation-queues",
projectId: input.projectId,
sessionUser: ctx.session.user,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -354,12 +329,6 @@ export const queueItemRouter = createTRPCRouter({
)
.mutation(async ({ input, ctx }) => {
try {
throwIfNoEntitlement({
entitlement: "annotation-queues",
projectId: input.projectId,
sessionUser: ctx.session.user,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -417,12 +386,6 @@ export const queueItemRouter = createTRPCRouter({
)
.mutation(async ({ input, ctx }) => {
try {
throwIfNoEntitlement({
entitlement: "annotation-queues",
projectId: input.projectId,
sessionUser: ctx.session.user,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -1,5 +1,4 @@
import { auditLog } from "@/src/features/audit-logs/auditLog";
import { throwIfNoEntitlement } from "@/src/features/entitlements/server/hasEntitlement";
import { throwIfNoProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import {
createTRPCRouter,
@@ -26,12 +25,6 @@ export const queueRouter = createTRPCRouter({
}),
)
.query(async ({ input, ctx }) => {
throwIfNoEntitlement({
entitlement: "annotation-queues",
projectId: input.projectId,
sessionUser: ctx.session.user,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -57,12 +50,6 @@ export const queueRouter = createTRPCRouter({
)
.query(async ({ input, ctx }) => {
try {
throwIfNoEntitlement({
entitlement: "annotation-queues",
projectId: input.projectId,
sessionUser: ctx.session.user,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -147,12 +134,6 @@ export const queueRouter = createTRPCRouter({
)
.query(async ({ input, ctx }) => {
try {
throwIfNoEntitlement({
entitlement: "annotation-queues",
projectId: input.projectId,
sessionUser: ctx.session.user,
});
const queueNamesAndIds = await ctx.prisma.annotationQueue.findMany({
where: {
projectId: input.projectId,
@@ -197,12 +178,6 @@ export const queueRouter = createTRPCRouter({
.input(z.object({ queueId: z.string(), projectId: z.string() }))
.query(async ({ input, ctx }) => {
try {
throwIfNoEntitlement({
entitlement: "annotation-queues",
projectId: input.projectId,
sessionUser: ctx.session.user,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -247,12 +222,6 @@ export const queueRouter = createTRPCRouter({
)
.query(async ({ input, ctx }) => {
try {
throwIfNoEntitlement({
entitlement: "annotation-queues",
projectId: input.projectId,
sessionUser: ctx.session.user,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -316,12 +285,6 @@ export const queueRouter = createTRPCRouter({
)
.mutation(async ({ input, ctx }) => {
try {
throwIfNoEntitlement({
entitlement: "annotation-queues",
projectId: input.projectId,
sessionUser: ctx.session.user,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -402,12 +365,6 @@ export const queueRouter = createTRPCRouter({
)
.mutation(async ({ input, ctx }) => {
try {
throwIfNoEntitlement({
entitlement: "annotation-queues",
projectId: input.projectId,
sessionUser: ctx.session.user,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -459,12 +416,6 @@ export const queueRouter = createTRPCRouter({
.input(z.object({ queueId: z.string(), projectId: z.string() }))
.mutation(async ({ input, ctx }) => {
try {
throwIfNoEntitlement({
entitlement: "annotation-queues",
projectId: input.projectId,
sessionUser: ctx.session.user,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -504,12 +455,6 @@ export const queueRouter = createTRPCRouter({
)
.mutation(async ({ input, ctx }) => {
try {
throwIfNoEntitlement({
entitlement: "annotation-queues",
projectId: input.projectId,
sessionUser: ctx.session.user,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -9,19 +9,24 @@ import {
import { encrypt } from "@langfuse/shared/encryption";
import { blobStorageIntegrationFormSchema } from "@/src/features/blobstorage-integration/types";
import { TRPCError } from "@trpc/server";
import { throwIfNoEntitlement } from "@/src/features/entitlements/server/hasEntitlement";
import { logger } from "@langfuse/shared/src/server";
import { type BlobStorageIntegration } from "@langfuse/shared";
import {
logger,
BlobStorageIntegrationProcessingQueue,
QueueJobs,
StorageServiceFactory,
} from "@langfuse/shared/src/server";
import { randomUUID } from "crypto";
import { decrypt } from "@langfuse/shared/encryption";
import {
type BlobStorageIntegration,
BlobStorageIntegrationType,
} from "@langfuse/shared";
import { env } from "@/src/env.mjs";
export const blobStorageIntegrationRouter = createTRPCRouter({
get: protectedProjectProcedure
.input(z.object({ projectId: z.string() }))
.query(async ({ input, ctx }) => {
throwIfNoEntitlement({
entitlement: "integration-blobstorage",
sessionUser: ctx.session.user,
projectId: input.projectId,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -55,11 +60,6 @@ export const blobStorageIntegrationRouter = createTRPCRouter({
.input(blobStorageIntegrationFormSchema.extend({ projectId: z.string() }))
.mutation(async ({ input, ctx }) => {
try {
throwIfNoEntitlement({
entitlement: "integration-blobstorage",
sessionUser: ctx.session.user,
projectId: input.projectId,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -87,6 +87,19 @@ export const blobStorageIntegrationRouter = createTRPCRouter({
fileType,
} = input;
const isSelfHosted = !env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION;
const canUseHostCredentials =
isSelfHosted && type === BlobStorageIntegrationType.S3;
const isUsingHostCredentials =
canUseHostCredentials && (!accessKeyId || !secretAccessKey);
if (!canUseHostCredentials && !accessKeyId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Access Key ID and Secret Access Key are required",
});
}
const data: Partial<BlobStorageIntegration> = {
type,
bucketName,
@@ -112,7 +125,6 @@ export const blobStorageIntegrationRouter = createTRPCRouter({
);
if (existingConfig) {
// Record exists, perform update
if (secretAccessKey) {
data.secretAccessKey = encrypt(secretAccessKey);
}
@@ -125,11 +137,11 @@ export const blobStorageIntegrationRouter = createTRPCRouter({
});
} else {
// Record doesn't exist, perform create
// For create, secretAccessKey is mandatory
if (!secretAccessKey) {
if (!isUsingHostCredentials && !secretAccessKey) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Secret access key is required for new configuration",
message:
"Secret access key is required for new configuration when not using host credentials",
});
}
@@ -138,12 +150,17 @@ export const blobStorageIntegrationRouter = createTRPCRouter({
...(data as BlobStorageIntegration),
projectId: input.projectId,
accessKeyId,
secretAccessKey: encrypt(secretAccessKey),
secretAccessKey: secretAccessKey
? encrypt(secretAccessKey)
: undefined,
},
});
}
});
} catch (e) {
if (e instanceof TRPCError) {
throw e;
}
logger.error(`Failed to update blob storage integration`, e);
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
@@ -155,11 +172,6 @@ export const blobStorageIntegrationRouter = createTRPCRouter({
.input(z.object({ projectId: z.string() }))
.mutation(async ({ input, ctx }) => {
try {
throwIfNoEntitlement({
entitlement: "integration-blobstorage",
sessionUser: ctx.session.user,
projectId: input.projectId,
});
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
@@ -185,4 +197,188 @@ export const blobStorageIntegrationRouter = createTRPCRouter({
});
}
}),
runNow: protectedProjectProcedure
.input(z.object({ projectId: z.string() }))
.mutation(async ({ input, ctx }) => {
try {
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
scope: "integrations:CRUD",
});
// Check if integration exists and is enabled
const integration = await ctx.prisma.blobStorageIntegration.findUnique({
where: {
projectId: input.projectId,
},
});
if (!integration) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Blob storage integration not found for this project",
});
}
if (!integration.enabled) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Blob storage integration is disabled",
});
}
// Get the processing queue
const blobStorageIntegrationProcessingQueue =
BlobStorageIntegrationProcessingQueue.getInstance();
if (!blobStorageIntegrationProcessingQueue) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "BlobStorageIntegrationProcessingQueue not initialized",
});
}
// Create a unique job ID for manual runs to avoid conflicts
const jobId = `${input.projectId}-manual-${new Date().toISOString()}`;
// Enqueue the processing job
await blobStorageIntegrationProcessingQueue.add(
QueueJobs.BlobStorageIntegrationProcessingJob,
{
id: randomUUID(),
name: QueueJobs.BlobStorageIntegrationProcessingJob,
timestamp: new Date(),
payload: {
projectId: input.projectId,
},
},
{
jobId,
},
);
logger.info(
`Manual blob storage integration job queued for project ${input.projectId}`,
);
return { success: true, jobId };
} catch (e) {
logger.error(`Failed to trigger blob storage integration run`, e);
if (e instanceof TRPCError) {
throw e;
}
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to trigger blob storage integration run",
});
}
}),
validate: protectedProjectProcedure
.input(z.object({ projectId: z.string() }))
.mutation(async ({ input, ctx }) => {
try {
throwIfNoProjectAccess({
session: ctx.session,
projectId: input.projectId,
scope: "integrations:CRUD",
});
// Get persisted configuration
const integration = await ctx.prisma.blobStorageIntegration.findUnique({
where: {
projectId: input.projectId,
},
});
if (!integration) {
throw new TRPCError({
code: "NOT_FOUND",
message:
"Blob storage integration not found for this project. Please save your configuration first.",
});
}
// Extract configuration from persisted data
const {
type,
bucketName,
endpoint,
region,
accessKeyId,
secretAccessKey: encryptedSecretAccessKey,
prefix,
forcePathStyle,
} = integration;
const secretAccessKey = encryptedSecretAccessKey
? decrypt(encryptedSecretAccessKey)
: undefined;
// Create storage service with provided configuration
const storageService = StorageServiceFactory.getInstance({
accessKeyId: accessKeyId || undefined,
secretAccessKey,
bucketName,
endpoint: endpoint || undefined,
region: region || undefined,
forcePathStyle: forcePathStyle || false,
useAzureBlob: type === BlobStorageIntegrationType.AZURE_BLOB_STORAGE,
useGoogleCloudStorage: false, // Not supported in blob storage integration
googleCloudCredentials: undefined,
awsSse: undefined,
awsSseKmsKeyId: undefined,
externalEndpoint: undefined,
});
// Create a test file
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const testFileName = `${prefix || ""}langfuse-validation-test-${timestamp}.txt`;
const testContent = `Langfuse blob storage validation test
Project ID: ${input.projectId}
Timestamp: ${new Date().toISOString()}
Configuration: ${type} storage
This file can be safely deleted.`;
// Upload the test file
const result = await storageService.uploadFile({
fileName: testFileName,
fileType: "text/plain",
data: testContent,
expiresInSeconds: 3600, // 1 hour
});
logger.info(
`Blob storage validation successful for project ${input.projectId}`,
);
return {
success: true,
message: "Validation successful! Test file uploaded.",
testFileName,
signedUrl: result.signedUrl,
};
} catch (e) {
logger.error(
`Blob storage validation failed for project ${input.projectId}`,
e,
);
// Extract meaningful error message
let errorMessage = "Unknown error occurred during validation";
if (e instanceof Error) {
errorMessage = e.message;
}
if (e instanceof TRPCError) {
throw e;
}
throw new TRPCError({
code: "BAD_REQUEST",
message: `Validation failed: ${errorMessage}`,
});
}
}),
});
@@ -9,11 +9,8 @@ export const blobStorageIntegrationFormSchema = z.object({
bucketName: z.string().min(1, { message: "Bucket name is required" }),
endpoint: z.string().url().optional().nullable(),
region: z.string().default("auto"),
accessKeyId: z.string().min(1, { message: "Access key ID is required" }),
secretAccessKey: z
.string()
.min(1, { message: "Secret access key is required" })
.nullable(), // Only required on create
accessKeyId: z.string().optional(),
secretAccessKey: z.string().nullable().optional(),
prefix: z
.string()
.refine((value) => !value || value === "" || value.endsWith("/"), {
@@ -344,7 +344,7 @@ export const datasetRouter = createTRPCRouter({
expectedOutput: true,
metadata: true,
},
orderBy: { createdAt: "desc" },
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
take: input.limit,
skip: input.page * input.limit,
});
@@ -866,7 +866,8 @@ export const datasetRouter = createTRPCRouter({
dri.project_id = ${input.projectId}
${filterQuery}
ORDER BY
di.created_at DESC
di.created_at DESC,
di.id DESC
LIMIT ${input.limit}
OFFSET ${input.page * input.limit}
`;
@@ -3,38 +3,25 @@ import { type Plan } from "@langfuse/shared";
// Entitlements: Binary feature access
const entitlements = [
// features
"playground",
"model-based-evaluations",
"rbac-project-roles",
"cloud-billing",
"cloud-multi-tenant-sso",
"integration-posthog",
"integration-blobstorage",
"annotation-queues",
"self-host-ui-customization",
"self-host-allowed-organization-creators",
"prompt-experiments",
"trace-deletion", // Not in use anymore, but necessary to use the TableAction type.
"audit-logs",
"data-retention",
"prompt-protected-labels",
"custom-dashboards",
"admin-api",
] as const;
export type Entitlement = (typeof entitlements)[number];
const cloudAllPlansEntitlements: Entitlement[] = [
"playground",
"model-based-evaluations",
"cloud-billing",
"integration-posthog",
"annotation-queues",
"prompt-experiments",
"trace-deletion",
"custom-dashboards",
];
const selfHostedAllPlansEntitlements: Entitlement[] = ["trace-deletion", "custom-dashboards"];
const selfHostedAllPlansEntitlements: Entitlement[] = ["trace-deletion"];
// Entitlement Limits: Limits on the number of resources that can be created/used
const entitlementLimits = [
@@ -96,7 +83,6 @@ export const entitlementAccess: Record<
"audit-logs",
"data-retention",
"cloud-multi-tenant-sso",
"integration-blobstorage",
"prompt-protected-labels",
"admin-api",
],
@@ -115,7 +101,6 @@ export const entitlementAccess: Record<
"audit-logs",
"data-retention",
"cloud-multi-tenant-sso",
"integration-blobstorage",
"prompt-protected-labels",
"admin-api",
],
@@ -128,25 +113,7 @@ export const entitlementAccess: Record<
},
},
oss: {
entitlements: [...selfHostedAllPlansEntitlements],
entitlementLimits: {
"annotation-queue-count": 0,
"organization-member-count": false,
"data-access-days": false,
"model-based-evaluations-count-evaluators": false,
"prompt-management-count-prompts": false,
},
},
"self-hosted:pro": {
entitlements: [
...selfHostedAllPlansEntitlements,
"annotation-queues",
"model-based-evaluations",
"playground",
"prompt-experiments",
"integration-posthog",
"integration-blobstorage",
],
entitlements: selfHostedAllPlansEntitlements,
entitlementLimits: {
"annotation-queue-count": false,
"organization-member-count": false,
@@ -158,15 +125,9 @@ export const entitlementAccess: Record<
"self-hosted:enterprise": {
entitlements: [
...selfHostedAllPlansEntitlements,
"annotation-queues",
"model-based-evaluations",
"playground",
"prompt-experiments",
"rbac-project-roles",
"self-host-allowed-organization-creators",
"self-host-ui-customization",
"integration-posthog",
"integration-blobstorage",
"audit-logs",
"data-retention",
"prompt-protected-labels",
@@ -60,8 +60,5 @@ export function getSelfHostedInstancePlanServerSide(): Plan | null {
if (licenseKey.startsWith("langfuse_ee_")) {
return "self-hosted:enterprise";
}
if (licenseKey.startsWith("langfuse_pro_")) {
return "self-hosted:pro";
}
return null;
}
@@ -1,4 +1,4 @@
import { EvaluatorStatus } from "@/src/ee/features/evals/types";
import { EvaluatorStatus } from "@/src/features/evals/types";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import { api, type RouterOutputs } from "@/src/utils/api";
@@ -7,6 +7,8 @@ import { type LangfuseColumnDef } from "@/src/components/table/types";
import { IOTableCell } from "@/src/components/ui/CodeJsonViewer";
import useColumnOrder from "@/src/features/column-visibility/hooks/useColumnOrder";
import useColumnVisibility from "@/src/features/column-visibility/hooks/useColumnVisibility";
import { useQueryFilterState } from "@/src/features/filters/hooks/useFilterState";
import { evalExecutionsFilterCols } from "@/src/server/api/definitions/evalExecutionsTable";
import { type RouterOutputs, api } from "@/src/utils/api";
import { type Prisma } from "@langfuse/shared";
import { createColumnHelper } from "@tanstack/react-table";
@@ -38,9 +40,17 @@ export default function EvalLogTable({
pageIndex: withDefault(NumberParam, 0),
pageSize: withDefault(NumberParam, 50),
});
const [filterState, setFilterState] = useQueryFilterState(
[],
"job_executions",
projectId,
);
const logs = api.evals.getLogs.useQuery({
page: paginationState.pageIndex,
limit: paginationState.pageSize,
filter: filterState,
jobConfigurationId,
projectId,
});
@@ -191,6 +201,9 @@ export default function EvalLogTable({
setColumnOrder={setColumnOrder}
rowHeight={rowHeight}
setRowHeight={setRowHeight}
filterState={filterState}
setFilterState={setFilterState}
filterColumnDefinition={evalExecutionsFilterCols}
/>
<DataTable
columns={columns}
@@ -1,5 +1,5 @@
import * as React from "react";
import { EvalTemplateForm } from "@/src/ee/features/evals/components/template-form";
import { EvalTemplateForm } from "@/src/features/evals/components/template-form";
import { api } from "@/src/utils/api";
import { type EvalTemplate } from "@langfuse/shared";
import { useRouter } from "next/router";
@@ -28,19 +28,16 @@ import {
DialogHeader,
DialogTitle,
} from "@/src/components/ui/dialog";
import { EvalTemplateForm } from "@/src/ee/features/evals/components/template-form";
import { EvalTemplateForm } from "@/src/features/evals/components/template-form";
import { showSuccessToast } from "@/src/features/notifications/showSuccessToast";
import { EvalReferencedEvaluators } from "@/src/ee/features/evals/types";
import { EvalReferencedEvaluators } from "@/src/features/evals/types";
import { showErrorToast } from "@/src/features/notifications/showErrorToast";
import { type RouterInput } from "@/src/utils/types";
import { useSingleTemplateValidation } from "@/src/ee/features/evals/hooks/useSingleTemplateValidation";
import { getMaintainer } from "@/src/ee/features/evals/utils/typeHelpers";
import { MaintainerTooltip } from "@/src/ee/features/evals/components/maintainer-tooltip";
import { useSingleTemplateValidation } from "@/src/features/evals/hooks/useSingleTemplateValidation";
import { getMaintainer } from "@/src/features/evals/utils/typeHelpers";
import { MaintainerTooltip } from "@/src/features/evals/components/maintainer-tooltip";
import { ActionButton } from "@/src/components/ActionButton";
import {
useEntitlementLimit,
useHasEntitlement,
} from "@/src/features/entitlements/hooks";
import { useEntitlementLimit } from "@/src/features/entitlements/hooks";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
export type EvalsTemplateRow = {
@@ -86,7 +83,6 @@ export default function EvalsTemplateTable({
});
const hasAccess = useHasProjectAccess({ projectId, scope: "evalJob:CUD" });
const hasEntitlement = useHasEntitlement("model-based-evaluations");
const totalCount = templates.data?.totalCount ?? null;
@@ -120,7 +116,7 @@ export default function EvalsTemplateTable({
projectId,
},
{
enabled: !!projectId && hasEntitlement,
enabled: !!projectId,
trpc: {
context: {
skipBatch: true,
@@ -240,7 +236,6 @@ export default function EvalsTemplateTable({
: undefined
}
hasAccess={hasAccess}
hasEntitlement={hasEntitlement}
limitValue={countsQuery.data?.configActiveCount ?? 0}
limit={evaluatorLimit}
onClick={(e) => {
@@ -260,7 +255,7 @@ export default function EvalsTemplateTable({
variant="outline"
size="icon-xs"
title="Clone"
disabled={!hasAccess || !hasEntitlement}
disabled={!hasAccess}
onClick={(e) => {
e.stopPropagation();
if (id) setCloneTemplateId(id);
@@ -274,7 +269,7 @@ export default function EvalsTemplateTable({
variant="outline"
size="icon-xs"
title="Edit"
disabled={!hasAccess || !hasEntitlement}
disabled={!hasAccess}
onClick={(e) => {
e.stopPropagation();
if (id) setEditTemplateId(id);
@@ -1,7 +1,7 @@
import { ItemBadge } from "@/src/components/ItemBadge";
import { Skeleton } from "@/src/components/ui/skeleton";
import { useExtractVariables } from "@/src/ee/features/evals/hooks/useExtractVariables";
import { type VariableMapping } from "@/src/ee/features/evals/utils/evaluator-form-utils";
import { useExtractVariables } from "@/src/features/evals/hooks/useExtractVariables";
import { type VariableMapping } from "@/src/features/evals/utils/evaluator-form-utils";
import { cn } from "@/src/utils/tailwind";
import { type RouterOutput } from "@/src/utils/types";
import { type EvalTemplate } from "@langfuse/shared";
@@ -1,7 +1,7 @@
import * as React from "react";
import { api } from "@/src/utils/api";
import { useRouter } from "next/router";
import EvalLogTable from "@/src/ee/features/evals/components/eval-log";
import EvalLogTable from "@/src/features/evals/components/eval-log";
import { StatusBadge } from "@/src/components/layouts/status-badge";
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
import Page from "@/src/components/layouts/page";
@@ -9,7 +9,7 @@ import { LevelCountsDisplay } from "@/src/components/level-counts-display";
import {
type JobExecutionState,
generateJobExecutionCounts,
} from "@/src/ee/features/evals/utils/job-execution-utils";
} from "@/src/features/evals/utils/job-execution-utils";
const JobExecutionCounts = ({
jobExecutionsByState,
@@ -1,6 +1,6 @@
import { type EvalTemplate } from "@langfuse/shared";
import { InnerEvaluatorForm } from "@/src/ee/features/evals/components/inner-evaluator-form";
import { type PartialConfig } from "@/src/ee/features/evals/types";
import { InnerEvaluatorForm } from "@/src/features/evals/components/inner-evaluator-form";
import { type PartialConfig } from "@/src/features/evals/types";
export const EvaluatorForm = (props: {
projectId: string;
@@ -16,9 +16,9 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/src/components/ui/tooltip";
import { useSingleTemplateValidation } from "@/src/ee/features/evals/hooks/useSingleTemplateValidation";
import { getMaintainer } from "@/src/ee/features/evals/utils/typeHelpers";
import { MaintainerTooltip } from "@/src/ee/features/evals/components/maintainer-tooltip";
import { useSingleTemplateValidation } from "@/src/features/evals/hooks/useSingleTemplateValidation";
import { getMaintainer } from "@/src/features/evals/utils/typeHelpers";
import { MaintainerTooltip } from "@/src/features/evals/components/maintainer-tooltip";
import Link from "next/link";
interface EvaluatorSelectorProps {
@@ -130,7 +130,7 @@ export function EvaluatorSelector({
{isInvalid && (
<Tooltip>
<TooltipTrigger asChild>
<AlertCircle className="ml-1 h-4 w-4 text-destructive" />
<AlertCircle className="ml-1 h-4 w-4 text-yellow-500" />
</TooltipTrigger>
<TooltipContent>
Requires project-level evaluation model
@@ -202,7 +202,7 @@ export function EvaluatorSelector({
{isInvalid && (
<Tooltip>
<TooltipTrigger asChild>
<AlertCircle className="ml-1 h-4 w-4 text-destructive" />
<AlertCircle className="ml-1 h-4 w-4 text-yellow-500" />
</TooltipTrigger>
<TooltipContent>
Requires project-level evaluation model
@@ -239,24 +239,9 @@ export function EvaluatorSelector({
);
})}
</InputCommandGroup>
{filteredTemplates.langfuse.length > 0 && <InputCommandSeparator />}
</>
)}
<InputCommandGroup forceMount>
<InputCommandItem
onSelect={() => {
window.open(
`/project/${projectId}/evals/default-model`,
"_blank",
);
}}
>
Manage default evaluation model
<ExternalLink className="ml-auto h-4 w-4" />
</InputCommandItem>
</InputCommandGroup>
{onCreateNew && (
<>
<InputCommandSeparator alwaysRender />
@@ -19,7 +19,7 @@ import {
StringParam,
} from "use-query-params";
import { z } from "zod";
import { generateJobExecutionCounts } from "@/src/ee/features/evals/utils/job-execution-utils";
import { generateJobExecutionCounts } from "@/src/features/evals/utils/job-execution-utils";
import { useOrderByState } from "@/src/features/orderBy/hooks/useOrderByState";
import TableIdOrName from "@/src/components/table/table-id";
import { MoreVertical, Loader2, ExternalLinkIcon, Edit } from "lucide-react";
@@ -36,12 +36,12 @@ import {
import { Button } from "@/src/components/ui/button";
import { showSuccessToast } from "@/src/features/notifications/showSuccessToast";
import { Dialog, DialogContent, DialogTitle } from "@/src/components/ui/dialog";
import { EvaluatorForm } from "@/src/ee/features/evals/components/evaluator-form";
import { EvaluatorForm } from "@/src/features/evals/components/evaluator-form";
import { useRouter } from "next/router";
import { DeleteEvaluatorButton } from "@/src/components/deleteButton";
import { DeleteEvalConfigButton } from "@/src/components/deleteButton";
import { evalConfigFilterColumns } from "@/src/server/api/definitions/evalConfigsTable";
import { RAGAS_TEMPLATE_PREFIX } from "@/src/ee/features/evals/types";
import { MaintainerTooltip } from "@/src/ee/features/evals/components/maintainer-tooltip";
import { RAGAS_TEMPLATE_PREFIX } from "@/src/features/evals/types";
import { MaintainerTooltip } from "@/src/features/evals/components/maintainer-tooltip";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
export type EvaluatorDataRow = {
@@ -292,7 +292,7 @@ export default function EvaluatorTable({ projectId }: { projectId: string }) {
Edit
</DropdownMenuItem>
<DropdownMenuItem asChild>
<DeleteEvaluatorButton
<DeleteEvalConfigButton
aria-label="delete"
itemId={id}
projectId={projectId}
@@ -0,0 +1,45 @@
import { type EvalFormType } from "@/src/features/evals/utils/evaluator-form-utils";
import { api } from "@/src/utils/api";
import { compactNumberFormatter } from "@/src/utils/numbers";
import { useEvalTargetCount } from "@/src/features/evals/hooks/useEvalTargetCount";
type ExecutionCountTooltipProps = {
projectId: string;
item: string;
filter: EvalFormType["filter"];
};
export const ExecutionCountTooltip = ({
projectId,
item,
filter,
}: ExecutionCountTooltipProps) => {
const globalConfig = api.evals.globalJobConfigs.useQuery({
projectId,
});
const { isLoading, totalCount, isTraceTarget } = useEvalTargetCount({
projectId,
item,
filter,
enabled: true,
});
return (
<>
<span className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
(
{isLoading ? (
<span className="inline-block font-mono">...</span>
) : (
compactNumberFormatter(
!globalConfig.data || (totalCount && totalCount < globalConfig.data)
? totalCount
: globalConfig.data,
)
)}
{isTraceTarget ? " traces" : " dataset run items"})
</span>
</>
);
};
@@ -47,24 +47,24 @@ import {
isTraceOrDatasetObject,
isTraceTarget,
type LangfuseObject,
} from "@/src/ee/features/evals/utils/evaluator-form-utils";
import { ExecutionCountTooltip } from "@/src/ee/features/evals/components/execution-count-tooltip";
} from "@/src/features/evals/utils/evaluator-form-utils";
import { ExecutionCountTooltip } from "@/src/features/evals/components/execution-count-tooltip";
import {
TimeScopeDescription,
VariableMappingDescription,
} from "@/src/ee/features/evals/components/eval-form-descriptions";
} from "@/src/features/evals/components/eval-form-descriptions";
import { Suspense, lazy } from "react";
import {
getDateFromOption,
type TableDateRange,
} from "@/src/utils/date-range-utils";
import { useEvalConfigMappingData } from "@/src/ee/features/evals/hooks/useEvalConfigMappingData";
import { type PartialConfig } from "@/src/ee/features/evals/types";
import { useEvalConfigMappingData } from "@/src/features/evals/hooks/useEvalConfigMappingData";
import { type PartialConfig } from "@/src/features/evals/types";
import { Switch } from "@/src/components/ui/switch";
import {
EvaluationPromptPreview,
getVariableColor,
} from "@/src/ee/features/evals/components/evaluation-prompt-preview";
} from "@/src/features/evals/components/evaluation-prompt-preview";
import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav";
import { Skeleton } from "@/src/components/ui/skeleton";
@@ -4,7 +4,7 @@ import {
TooltipTrigger,
TooltipContent,
} from "@/src/components/ui/tooltip";
import { RagasLogoIcon } from "@/src/ee/features/evals/components/ragas-logo";
import { RagasLogoIcon } from "@/src/features/evals/components/ragas-logo";
import { UserCircle2Icon } from "lucide-react";
function MaintainerIcon({ maintainer }: { maintainer: string }) {
@@ -0,0 +1,90 @@
import { Button } from "@/src/components/ui/button";
import { useHasProjectAccess } from "@/src/features/rbac/utils/checkProjectAccess";
import { api } from "@/src/utils/api";
import { cn } from "@/src/utils/tailwind";
import { Check, Pencil, TriangleAlert } from "lucide-react";
import Link from "next/link";
export function ManageDefaultEvalModel({
projectId,
setUpMessage,
variant = "default",
showEditButton = true,
className,
}: {
projectId: string;
setUpMessage?: string;
variant?: "default" | "color-coded";
showEditButton?: boolean;
className?: string;
}) {
const hasDefaultModelReadAccess = useHasProjectAccess({
projectId,
scope: "evalDefaultModel:read",
});
const hasDefaultModelWriteAccess = useHasProjectAccess({
projectId,
scope: "evalDefaultModel:CUD",
});
const { data: defaultModel } = api.defaultLlmModel.fetchDefaultModel.useQuery(
{ projectId },
{ enabled: hasDefaultModelReadAccess },
);
return (
<div className="flex items-center">
{!showEditButton &&
(defaultModel ? (
<Check className="mr-2 h-4 w-4 text-dark-green" />
) : (
<TriangleAlert className="mr-2 h-4 w-4 text-dark-yellow" />
))}
{defaultModel ? (
<span
className={cn(
"text-sm font-medium",
variant === "color-coded" && "text-dark-green",
className,
)}
>
{"Current default model: "}
{defaultModel.provider} / {defaultModel.model}
</span>
) : (
<span
className={cn(
"text-sm font-medium",
variant === "color-coded" && "text-dark-yellow",
className,
)}
>
{setUpMessage ?? "No default model set"}
</span>
)}
{showEditButton && (
<Button
variant="ghost"
size="icon-sm"
type="button"
asChild
disabled={!hasDefaultModelWriteAccess}
>
<Link
href={`/project/${projectId}/evals/default-model`}
target="_blank"
>
<Pencil
className={cn(
"h-3 w-3",
variant === "color-coded" &&
!defaultModel &&
"text-dark-yellow",
)}
/>
</Link>
</Button>
)}
</div>
);
}

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