Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c35e5e9c5 | ||
|
|
fe75e4c5f1 | ||
|
|
6435ce1a79 | ||
|
|
d270143f0e | ||
|
|
4041550d5e | ||
|
|
fc5ff52d1e | ||
|
|
6f6bef722b | ||
|
|
496ac68008 | ||
|
|
595f9f1e5d | ||
|
|
70bca50ad0 | ||
|
|
99791458b4 | ||
|
|
60dc78b498 | ||
|
|
43e5887361 | ||
|
|
c009d9602d | ||
|
|
830ebf6fff | ||
|
|
976f743841 | ||
|
|
89c1e46c55 | ||
|
|
832e5d85ff | ||
|
|
9b3a1f3af3 | ||
|
|
c2515f7e6a |
+1
-1
@@ -96,7 +96,7 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
|
||||
|
||||
### START Langfuse Cloud Config
|
||||
# Used for Langfuse Cloud deployments
|
||||
# Not recommended for self-hosted deployments as these are NOT COVERED BY SEMVER
|
||||
# Not recommended for self-hosted deployments as these are NOT COVERED BY SEMANTIC VERSIONING
|
||||
|
||||
# NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="US"
|
||||
# NEXTAUTH_COOKIE_DOMAIN=".langfuse.com"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
_We are Hiring_
|
||||
Join us in scaling Langfuse in Berlin, Germany. We are an open source company, we hire in person, we are only hiring technical talent.
|
||||
|
||||
_Open Roles_
|
||||
|
||||
- Backend Engineer, 70-110k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/mnrdwla-backend-engineer
|
||||
- Product Engineer, 70-110k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
|
||||
- Developer Advocate, 60-100k EUR, 0.25-0.5% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
|
||||
|
||||
_More Info_
|
||||
|
||||
- https://langfuse.com/careers
|
||||
- https://langfuse.com/docs
|
||||
- https://langfuse.com/changelog
|
||||
+7
-1
@@ -4,7 +4,8 @@ services:
|
||||
langfuse-server:
|
||||
image: ghcr.io/langfuse/langfuse:latest
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
@@ -18,6 +19,11 @@ services:
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.43.2",
|
||||
"version": "2.44.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -293,6 +293,7 @@ export type ObservationView = {
|
||||
calculated_output_cost: string | null;
|
||||
calculated_total_cost: string | null;
|
||||
latency: number | null;
|
||||
time_to_first_token: number | null;
|
||||
};
|
||||
export type PosthogIntegration = {
|
||||
project_id: string;
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
CREATE OR REPLACE VIEW "observations_view" AS
|
||||
SELECT
|
||||
o.*,
|
||||
m.id AS "model_id",
|
||||
m.start_date AS "model_start_date",
|
||||
m.input_price,
|
||||
m.output_price,
|
||||
m.total_price,
|
||||
m.tokenizer_config AS "tokenizer_config",
|
||||
CASE
|
||||
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
|
||||
o.prompt_tokens::decimal * m.input_price
|
||||
ELSE
|
||||
o.input_cost
|
||||
END AS "calculated_input_cost",
|
||||
CASE
|
||||
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
|
||||
o.completion_tokens::decimal * m.output_price
|
||||
ELSE
|
||||
o.output_cost
|
||||
END AS "calculated_output_cost",
|
||||
CASE
|
||||
WHEN o.input_cost IS NULL AND o.output_cost IS NULL AND o.total_cost IS NULL THEN
|
||||
CASE
|
||||
WHEN m.total_price IS NOT NULL AND o.total_tokens IS NOT NULL THEN
|
||||
m.total_price * o.total_tokens
|
||||
ELSE
|
||||
o.prompt_tokens::decimal * m.input_price +
|
||||
o.completion_tokens::decimal * m.output_price
|
||||
END
|
||||
ELSE
|
||||
o.total_cost
|
||||
END AS "calculated_total_cost",
|
||||
CASE WHEN o.end_time IS NULL THEN NULL ELSE (EXTRACT(EPOCH FROM o."end_time") - EXTRACT(EPOCH FROM o."start_time"))::double precision END AS "latency",
|
||||
CASE WHEN o.completion_start_time IS NOT NULL AND o.start_time IS NOT NULL THEN EXTRACT(EPOCH FROM (completion_start_time - start_time))::double precision ELSE NULL END as "time_to_first_token"
|
||||
|
||||
FROM
|
||||
observations o
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
models.*
|
||||
FROM
|
||||
models
|
||||
WHERE (models.project_id = o.project_id OR models.project_id IS NULL)
|
||||
AND models.model_name = o.internal_model
|
||||
AND (models.start_date < o.start_time OR models.start_date IS NULL)
|
||||
AND o.unit::TEXT = models.unit
|
||||
ORDER BY
|
||||
models.project_id ASC, -- in postgres, NULLs are sorted last when ordering ASC
|
||||
models.start_date DESC NULLS LAST -- now, NULLs are sorted last when ordering DESC as well
|
||||
LIMIT 1
|
||||
) m ON TRUE
|
||||
|
||||
|
||||
-- requirements:
|
||||
-- 1. The view should return all columns from the observations table
|
||||
-- 2. The view should match with only one model for each observation if:
|
||||
-- a. The model has the same project_id as the observation, otherwise the model without project_id.
|
||||
-- b. The model has the same model_name as the observation
|
||||
-- c. The model has a start_date that is less than the observation start_time, otherwise the model without start_date
|
||||
-- d. The model has the same unit as the observation
|
||||
@@ -373,6 +373,7 @@ view ObservationView {
|
||||
calculatedOutputCost Decimal? @map("calculated_output_cost")
|
||||
calculatedTotalCost Decimal? @map("calculated_total_cost")
|
||||
latency Float? @map("latency")
|
||||
timeToFirstToken Float? @map("time_to_first_token")
|
||||
|
||||
@@map("observations_view")
|
||||
}
|
||||
|
||||
@@ -528,9 +528,9 @@ function createObjects(
|
||||
const spanTsStart = new Date(
|
||||
traceTs.getTime() + Math.floor(Math.random() * 30)
|
||||
);
|
||||
// random duration of upto 30ms
|
||||
// random duration of upto 5000ms
|
||||
const spanTsEnd = new Date(
|
||||
spanTsStart.getTime() + Math.floor(Math.random() * 30)
|
||||
spanTsStart.getTime() + Math.floor(Math.random() * 5000)
|
||||
);
|
||||
|
||||
const span = {
|
||||
@@ -574,6 +574,13 @@ function createObjects(
|
||||
(spanTsEnd.getTime() - generationTsStart.getTime())
|
||||
)
|
||||
);
|
||||
// somewhere in the middle
|
||||
const generationTsCompletionStart = new Date(
|
||||
generationTsStart.getTime() +
|
||||
Math.floor(
|
||||
(generationTsEnd.getTime() - generationTsStart.getTime()) / 3
|
||||
)
|
||||
);
|
||||
|
||||
const promptTokens = Math.floor(Math.random() * 1000) + 300;
|
||||
const completionTokens = Math.floor(Math.random() * 500) + 100;
|
||||
@@ -602,6 +609,8 @@ function createObjects(
|
||||
id: `generation-${v4()}`,
|
||||
startTime: generationTsStart,
|
||||
endTime: generationTsEnd,
|
||||
completionStartTime:
|
||||
Math.random() > 0.5 ? generationTsCompletionStart : undefined,
|
||||
name: `generation-${i}-${j}-${k}`,
|
||||
projectId: trace.projectId,
|
||||
promptId: promptId,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.43.2",
|
||||
"version": "2.44.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -20,6 +20,15 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
) {
|
||||
return 0.3;
|
||||
}
|
||||
if (
|
||||
samplingContext.request &&
|
||||
samplingContext.request.url &&
|
||||
samplingContext.request.url.includes("api/auth") &&
|
||||
samplingContext.transactionContext.status !== "ok" &&
|
||||
samplingContext.transactionContext.status !== "unauthenticated"
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
return 0.1;
|
||||
},
|
||||
|
||||
|
||||
@@ -727,6 +727,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "2.0.0",
|
||||
tags: ["tag-1", "tag-2"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -743,6 +744,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
id: traceId,
|
||||
name: "trace-name",
|
||||
userId: "user-2",
|
||||
tags: ["tag-3", "tag-4"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -763,6 +765,7 @@ describe("/api/public/ingestion API Endpoint", () => {
|
||||
expect(dbTrace[0]?.externalId).toBeNull();
|
||||
expect(dbTrace[0]?.version).toBe("2.0.0");
|
||||
expect(dbTrace[0]?.projectId).toBe("7a88fb47-b4e2-43b8-a06c-a5ce950dc53a");
|
||||
expect(dbTrace[0]?.tags).toEqual(["tag-1", "tag-2", "tag-3", "tag-4"]);
|
||||
});
|
||||
|
||||
it("should fail for wrong event formats", async () => {
|
||||
|
||||
@@ -253,7 +253,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
>
|
||||
<span className="sr-only">Close sidebar</span>
|
||||
<XMarkIcon
|
||||
className="h-6 w-6 text-white"
|
||||
className="h-5 w-5 text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
@@ -273,7 +273,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
<MainNavigation nav={navigation} />
|
||||
</ul>
|
||||
<div className="mb-2 flex flex-row place-content-between items-center">
|
||||
<div className="text-xs font-semibold leading-6 text-gray-400">
|
||||
<div className="text-xs font-semibold text-gray-400">
|
||||
Project
|
||||
</div>
|
||||
<NewProjectButton size="xs" />
|
||||
@@ -291,7 +291,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
</Transition.Root>
|
||||
|
||||
{/* Static sidebar for desktop */}
|
||||
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-60 lg:flex-col">
|
||||
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-56 lg:flex-col">
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex h-screen grow flex-col border-r border-gray-200 bg-white pt-7">
|
||||
<LangfuseLogo
|
||||
@@ -312,16 +312,16 @@ export default function Layout(props: PropsWithChildren) {
|
||||
description="What do you think about this project? What can be improved?"
|
||||
type="feedback"
|
||||
>
|
||||
<li className="group -mx-2 my-1 flex cursor-pointer gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
|
||||
<li className="group -mx-2 my-1 flex cursor-pointer gap-x-3 rounded-md p-1.5 text-sm font-semibold text-gray-700 hover:bg-gray-50 hover:text-indigo-600">
|
||||
<MessageSquarePlus
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
className="h-5 w-5 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Feedback
|
||||
</li>
|
||||
</FeedbackButtonWrapper>
|
||||
<div className="mb-2 flex flex-row place-content-between items-center">
|
||||
<div className="text-xs font-semibold leading-6 text-gray-400">
|
||||
<div className="text-xs font-semibold text-gray-400">
|
||||
Project
|
||||
</div>
|
||||
<NewProjectButton size="xs" />
|
||||
@@ -334,9 +334,9 @@ export default function Layout(props: PropsWithChildren) {
|
||||
</nav>
|
||||
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button className="flex w-full items-center gap-x-4 overflow-hidden p-1.5 py-3 pl-6 pr-10 text-sm font-semibold leading-6 text-gray-900 hover:bg-gray-50">
|
||||
<Menu.Button className="flex w-full items-center gap-x-2 overflow-hidden p-1.5 py-3 pl-6 pr-8 text-sm font-semibold text-gray-900 hover:bg-gray-50">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<Avatar className="h-8 w-8">
|
||||
<Avatar className="h-7 w-7">
|
||||
<AvatarImage src={session.data?.user?.image ?? undefined} />
|
||||
<AvatarFallback>
|
||||
{session.data?.user?.name
|
||||
@@ -348,7 +348,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
: null}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="flex-shrink truncate text-sm font-semibold leading-6 text-gray-900">
|
||||
<span className="flex-shrink truncate text-sm font-semibold text-gray-900">
|
||||
{session.data?.user?.name}
|
||||
</span>
|
||||
<div className="flex-1" />
|
||||
@@ -367,7 +367,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute -top-full right-0 z-10 mt-2.5 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||
<span className="mb-1 block border-b px-3 pb-2 text-sm leading-6 text-gray-500">
|
||||
<span className="mb-1 block border-b px-3 pb-2 text-sm text-gray-500">
|
||||
{session.data?.user?.email}
|
||||
</span>
|
||||
{userNavigation.map((item) => (
|
||||
@@ -377,7 +377,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
onClick={() => void item.onClick()}
|
||||
className={cn(
|
||||
active ? "bg-gray-50" : "",
|
||||
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
|
||||
"block cursor-pointer px-3 py-1 text-sm text-gray-900",
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
@@ -398,7 +398,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
>
|
||||
<span className="sr-only">Open sidebar</span>
|
||||
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
|
||||
<Bars3Icon className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
<LangfuseLogo
|
||||
version
|
||||
@@ -406,7 +406,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
showEnvLabel={session.data?.user?.email?.endsWith("@langfuse.com")}
|
||||
/>
|
||||
<Menu as="div" className="relative">
|
||||
<Menu.Button className="flex items-center gap-x-4 text-sm font-semibold leading-6 text-gray-900">
|
||||
<Menu.Button className="flex items-center gap-x-4 text-sm font-semibold text-gray-900">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={session.data?.user?.image ?? undefined} />
|
||||
@@ -431,7 +431,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 z-10 mt-2.5 rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 focus:outline-none">
|
||||
<span className="mb-1 block border-b px-3 pb-2 text-sm leading-6 text-gray-500">
|
||||
<span className="mb-1 block border-b px-3 pb-2 text-sm text-gray-500">
|
||||
{session.data?.user?.email}
|
||||
</span>
|
||||
{userNavigation.map((item) => (
|
||||
@@ -441,7 +441,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
onClick={() => void item.onClick()}
|
||||
className={cn(
|
||||
active ? "bg-gray-50" : "",
|
||||
"block cursor-pointer px-3 py-1 text-sm leading-6 text-gray-900",
|
||||
"block cursor-pointer px-3 py-1 text-sm text-gray-900",
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
@@ -453,7 +453,7 @@ export default function Layout(props: PropsWithChildren) {
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
<div className="lg:pl-60">
|
||||
<div className="lg:pl-56">
|
||||
{env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
|
||||
projectId === env.NEXT_PUBLIC_DEMO_PROJECT_ID &&
|
||||
(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING" ||
|
||||
@@ -525,7 +525,7 @@ const MainNavigation: React.FC<{
|
||||
item.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6",
|
||||
"group flex gap-x-3 rounded-md p-1.5 text-sm font-semibold",
|
||||
)}
|
||||
onClick={onNavitemClick}
|
||||
target={item.newTab ? "_blank" : undefined}
|
||||
@@ -536,7 +536,7 @@ const MainNavigation: React.FC<{
|
||||
item.current
|
||||
? "text-indigo-600"
|
||||
: "text-gray-400 group-hover:text-indigo-600",
|
||||
"h-6 w-6 shrink-0",
|
||||
"h-5 w-5 shrink-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -545,7 +545,7 @@ const MainNavigation: React.FC<{
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
"-my-0.5 self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
@@ -565,12 +565,12 @@ const MainNavigation: React.FC<{
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button
|
||||
className="group flex w-full items-center gap-x-3 rounded-md p-2 text-left text-sm font-semibold leading-6 hover:bg-gray-50 hover:text-indigo-600"
|
||||
className="group flex w-full items-center gap-x-3 rounded-md p-1.5 text-left text-sm font-semibold hover:bg-gray-50 hover:text-indigo-600"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{item.icon && (
|
||||
<item.icon
|
||||
className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
className="h-5 w-5 shrink-0 text-gray-400 group-hover:text-indigo-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
@@ -578,7 +578,7 @@ const MainNavigation: React.FC<{
|
||||
{item.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
"-my-0.5 self-center whitespace-nowrap break-keep rounded-sm border px-1 py-0.5 text-xs",
|
||||
item.current
|
||||
? "border-indigo-600 text-indigo-600"
|
||||
: "border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600",
|
||||
@@ -595,7 +595,7 @@ const MainNavigation: React.FC<{
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel as="ul" className="mt-1 px-2">
|
||||
<Disclosure.Panel as="ul" className="mt-1 space-y-1 px-2">
|
||||
{item.children?.map((subItem) => (
|
||||
<li key={subItem.name}>
|
||||
{/* 44px */}
|
||||
@@ -605,7 +605,7 @@ const MainNavigation: React.FC<{
|
||||
subItem.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:bg-gray-50 hover:text-indigo-600",
|
||||
"flex w-full items-center gap-x-3 rounded-md py-2 pl-9 pr-2 text-sm leading-6",
|
||||
"ml-0.5 flex w-full items-center gap-x-3 rounded-md p-1.5 pl-7 pr-2 text-sm",
|
||||
)}
|
||||
target={subItem.newTab ? "_blank" : undefined}
|
||||
>
|
||||
|
||||
@@ -32,7 +32,7 @@ export const ProjectNavigation: React.FC<ProjectNavigationProps> = ({
|
||||
router.push(`/project/${value}`);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="text-gray-700 ring-transparent focus:ring-0 focus:ring-offset-0">
|
||||
<SelectTrigger className="h-8 text-gray-700 ring-transparent focus:ring-0 focus:ring-offset-0">
|
||||
<SelectValue
|
||||
className="text-sm font-semibold text-gray-700"
|
||||
placeholder={currentProjectId}
|
||||
|
||||
@@ -59,6 +59,7 @@ export type GenerationsTableRow = {
|
||||
endTime?: string;
|
||||
completionStartTime?: Date;
|
||||
latency?: number;
|
||||
timeToFirstToken?: number;
|
||||
name?: string;
|
||||
model?: string;
|
||||
// i/o not set explicitly, but fetched from the server from the cell
|
||||
@@ -310,21 +311,12 @@ export default function GenerationsTable({
|
||||
enableHiding: true,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const startTime: Date = row.getValue("startTime");
|
||||
const completionStartTime: Date | undefined =
|
||||
const timeToFirstToken: number | undefined =
|
||||
row.getValue("timeToFirstToken");
|
||||
|
||||
if (!completionStartTime) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const latencyInSeconds =
|
||||
intervalInSeconds(startTime, completionStartTime) || "-";
|
||||
return (
|
||||
<span>
|
||||
{typeof latencyInSeconds === "number"
|
||||
? formatIntervalSeconds(latencyInSeconds)
|
||||
: latencyInSeconds}
|
||||
{timeToFirstToken ? formatIntervalSeconds(timeToFirstToken) : "-"}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
@@ -623,7 +615,7 @@ export default function GenerationsTable({
|
||||
traceName: generation.traceName ?? "",
|
||||
startTime: generation.startTime,
|
||||
endTime: generation.endTime?.toLocaleString() ?? undefined,
|
||||
timeToFirstToken: generation.completionStartTime ?? undefined,
|
||||
timeToFirstToken: generation.timeToFirstToken ?? undefined,
|
||||
latency: generation.latency ?? undefined,
|
||||
totalCost: generation.calculatedTotalCost ?? undefined,
|
||||
inputCost: generation.calculatedInputCost ?? undefined,
|
||||
|
||||
@@ -128,12 +128,11 @@ const ChatMlMessageSchema = z
|
||||
.optional(),
|
||||
name: z.string().optional(),
|
||||
content: z
|
||||
.union([z.record(z.any()), z.record(z.any()).array(), z.string()])
|
||||
.union([z.record(z.any()), z.string(), z.array(z.any())])
|
||||
.nullish(),
|
||||
additional_kwargs: z.record(z.any()).optional(),
|
||||
})
|
||||
.passthrough()
|
||||
|
||||
.refine((value) => value.content !== null || value.role !== undefined)
|
||||
.transform(({ additional_kwargs, ...other }) => ({
|
||||
...other,
|
||||
|
||||
@@ -67,14 +67,10 @@ export const ObservationPreview = (props: {
|
||||
projectId={preloadedObservation.projectId}
|
||||
/>
|
||||
) : undefined}
|
||||
{preloadedObservation.completionStartTime ? (
|
||||
{preloadedObservation.timeToFirstToken ? (
|
||||
<Badge variant="outline">
|
||||
Time to first token:{" "}
|
||||
{formatIntervalSeconds(
|
||||
(preloadedObservation.completionStartTime.getTime() -
|
||||
preloadedObservation.startTime.getTime()) /
|
||||
1000,
|
||||
)}
|
||||
{formatIntervalSeconds(preloadedObservation.timeToFirstToken)}
|
||||
</Badge>
|
||||
) : null}
|
||||
{preloadedObservation.endTime ? (
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v2.43.2";
|
||||
export const VERSION = "v2.44.0";
|
||||
|
||||
@@ -27,7 +27,11 @@ export async function createUserEmailPassword(
|
||||
},
|
||||
});
|
||||
if (user !== null) {
|
||||
throw new Error("User with email already exists. Please sign in.");
|
||||
throw new Error(
|
||||
user.password !== null
|
||||
? "User with email already exists. Please sign in."
|
||||
: "You have already signed up via an identity provider. Please sign in.",
|
||||
);
|
||||
}
|
||||
|
||||
const newUser = await prisma.user.create({
|
||||
|
||||
@@ -41,6 +41,7 @@ export const sendProjectInvitation = async (
|
||||
projectName: projectName,
|
||||
recieverEmail: to,
|
||||
inviteLink: authUrl,
|
||||
langfuseCloudRegion: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ interface ProjectInvitationTemplateProps {
|
||||
projectName: string;
|
||||
recieverEmail: string;
|
||||
inviteLink: string;
|
||||
langfuseCloudRegion?: string;
|
||||
}
|
||||
|
||||
export const ProjectInvitationTemplate = ({
|
||||
@@ -31,6 +32,7 @@ export const ProjectInvitationTemplate = ({
|
||||
projectName,
|
||||
recieverEmail,
|
||||
inviteLink,
|
||||
langfuseCloudRegion,
|
||||
}: ProjectInvitationTemplateProps) => {
|
||||
const previewText = `Join ${invitedByUsername} on Langfuse`;
|
||||
|
||||
@@ -63,7 +65,10 @@ export const ProjectInvitationTemplate = ({
|
||||
{invitedByUserEmail}
|
||||
</Link>
|
||||
) has invited you to the <strong>{projectName}</strong> project on
|
||||
Langfuse.
|
||||
{langfuseCloudRegion
|
||||
? ` Langfuse (${langfuseCloudRegion} data region)`
|
||||
: " Langfuse"}
|
||||
.
|
||||
</Text>
|
||||
<Section className="mb-4 mt-8 text-center">
|
||||
<Button
|
||||
|
||||
@@ -5,5 +5,9 @@ import NextAuth from "next-auth";
|
||||
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
|
||||
// Do whatever you want here, before the request is passed down to `NextAuth`
|
||||
const authOptions = await getAuthOptions();
|
||||
// https://github.com/nextauthjs/next-auth/issues/2408#issuecomment-1382629234
|
||||
// for api routes, we need to call the headers in the api route itself
|
||||
// disable caching for anything auth related
|
||||
res.setHeader("Cache-Control", "no-store, max-age=0");
|
||||
return await NextAuth(req, res, authOptions);
|
||||
}
|
||||
|
||||
@@ -234,19 +234,31 @@ export default function SignIn({ authProviders, signUpDisabled }: PageProps) {
|
||||
redirect: false,
|
||||
turnstileToken,
|
||||
});
|
||||
if (result?.error) {
|
||||
setCredentialsFormError(result.error);
|
||||
|
||||
// Refresh turnstile as the token can only be used once
|
||||
if (env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && turnstileToken) {
|
||||
setTurnstileCData(new Date().getTime().toString());
|
||||
setTurnstileToken(undefined);
|
||||
if (result === undefined) {
|
||||
setCredentialsFormError("An unexpected error occurred.");
|
||||
captureException(new Error("Sign in result is undefined"));
|
||||
} else if (!result.ok) {
|
||||
if (!result.error) {
|
||||
captureException(
|
||||
new Error(
|
||||
`Sign in result error is falsy, result: ${JSON.stringify(result)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
setCredentialsFormError(
|
||||
result?.error ?? "An unexpected error occurred.",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
captureException(error);
|
||||
console.error(error);
|
||||
setCredentialsFormError("An unexpected error occurred.");
|
||||
} finally {
|
||||
// Refresh turnstile as the token can only be used once
|
||||
if (env.NEXT_PUBLIC_TURNSTILE_SITE_KEY && turnstileToken) {
|
||||
setTurnstileCData(new Date().getTime().toString());
|
||||
setTurnstileToken(undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function TemplatesPage() {
|
||||
title="Eval Log"
|
||||
help={{
|
||||
description: "View of all running evals.",
|
||||
href: "https://langfuse.com/docs/evals",
|
||||
href: "https://langfuse.com/docs/scores/model-based-evals",
|
||||
}}
|
||||
/>
|
||||
<EvalLogTable projectId={projectId} />
|
||||
|
||||
@@ -101,6 +101,7 @@ export async function getAllGenerations({
|
||||
o.trace_id as "traceId",
|
||||
t.name as "traceName",
|
||||
o.completion_start_time as "completionStartTime",
|
||||
o.time_to_first_token as "timeToFirstToken",
|
||||
o.prompt_tokens as "promptTokens",
|
||||
o.completion_tokens as "completionTokens",
|
||||
o.total_tokens as "totalTokens",
|
||||
|
||||
+1
-3
@@ -59,9 +59,7 @@ export function transformStreamToCsv(): Transform {
|
||||
row.endTime?.toISOString() ?? "",
|
||||
row.completionStartTime?.toISOString() ?? "",
|
||||
// time to first token
|
||||
row.completionStartTime
|
||||
? intervalInSeconds(row.startTime, row.completionStartTime).toFixed(2)
|
||||
: "",
|
||||
row.timeToFirstToken?.toFixed(2) ?? "",
|
||||
row.scores ? JSON.stringify(row.scores) : "",
|
||||
row.latency ? formatIntervalSeconds(row.latency).slice(0, -1) : "",
|
||||
// latency per token
|
||||
|
||||
@@ -257,6 +257,7 @@ export const traceRouter = createTRPCRouter({
|
||||
totalTokens: true,
|
||||
unit: true,
|
||||
completionStartTime: true,
|
||||
timeToFirstToken: true,
|
||||
promptId: true,
|
||||
modelId: true,
|
||||
inputPrice: true,
|
||||
|
||||
@@ -449,6 +449,10 @@ export class TraceProcessor implements EventProcessor {
|
||||
body.metadata ?? undefined,
|
||||
);
|
||||
|
||||
const mergedTags = existingTrace?.tags
|
||||
? existingTrace.tags.concat(body.tags ?? [])
|
||||
: body.tags;
|
||||
|
||||
if (body.sessionId) {
|
||||
await prisma.traceSession.upsert({
|
||||
where: {
|
||||
@@ -486,7 +490,7 @@ export class TraceProcessor implements EventProcessor {
|
||||
sessionId: body.sessionId ?? undefined,
|
||||
public: body.public ?? undefined,
|
||||
projectId: apiScope.projectId,
|
||||
tags: body.tags ?? undefined,
|
||||
tags: mergedTags ?? undefined,
|
||||
},
|
||||
update: {
|
||||
name: body.name ?? undefined,
|
||||
@@ -501,7 +505,7 @@ export class TraceProcessor implements EventProcessor {
|
||||
version: body.version ?? undefined,
|
||||
sessionId: body.sessionId ?? undefined,
|
||||
public: body.public ?? undefined,
|
||||
tags: body.tags ?? undefined,
|
||||
tags: mergedTags ?? undefined,
|
||||
},
|
||||
});
|
||||
return upsertedTrace;
|
||||
|
||||
+16
-2
@@ -27,6 +27,7 @@ import {
|
||||
loadSsoProviders,
|
||||
} from "@langfuse/ee/sso";
|
||||
import { z } from "zod";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const staticProviders: Provider[] = [
|
||||
CredentialsProvider({
|
||||
@@ -90,7 +91,10 @@ const staticProviders: Provider[] = [
|
||||
});
|
||||
|
||||
if (!dbUser) throw new Error("Invalid credentials");
|
||||
if (dbUser.password === null) throw new Error("Invalid credentials");
|
||||
if (dbUser.password === null)
|
||||
throw new Error(
|
||||
"Please sign in with the identity provider that is linked to your account.",
|
||||
);
|
||||
|
||||
const isValidPassword = await verifyPassword(
|
||||
credentials.password,
|
||||
@@ -212,7 +216,13 @@ const extendedPrismaAdapter: Adapter = {
|
||||
* @see https://next-auth.js.org/configuration/options
|
||||
*/
|
||||
export async function getAuthOptions(): Promise<NextAuthOptions> {
|
||||
const dynamicSsoProviders = await loadSsoProviders();
|
||||
let dynamicSsoProviders: Provider[] = [];
|
||||
try {
|
||||
dynamicSsoProviders = await loadSsoProviders();
|
||||
} catch (e) {
|
||||
console.error("Error loading dynamic SSO providers", e);
|
||||
Sentry.captureException(e);
|
||||
}
|
||||
const providers = [...staticProviders, ...dynamicSsoProviders];
|
||||
|
||||
const data: NextAuthOptions = {
|
||||
@@ -365,5 +375,9 @@ export const getServerAuthSession = async (ctx: {
|
||||
res: GetServerSidePropsContext["res"];
|
||||
}) => {
|
||||
const authOptions = await getAuthOptions();
|
||||
// https://github.com/nextauthjs/next-auth/issues/2408#issuecomment-1382629234
|
||||
// for api routes, we need to call the headers in the api route itself
|
||||
// disable caching for anything auth related
|
||||
ctx.res.setHeader("Cache-Control", "no-store, max-age=0");
|
||||
return getServerSession(ctx.req, ctx.res, authOptions);
|
||||
};
|
||||
|
||||
@@ -298,6 +298,7 @@ export type ObservationView = {
|
||||
calculated_output_cost: string | null;
|
||||
calculated_total_cost: string | null;
|
||||
latency: number | null;
|
||||
time_to_first_token: number | null;
|
||||
};
|
||||
export type PosthogIntegration = {
|
||||
project_id: string;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "worker",
|
||||
"version": "2.43.2",
|
||||
"version": "2.44.0",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v2.43.2";
|
||||
export const VERSION = "v2.44.0";
|
||||
|
||||
Reference in New Issue
Block a user