Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85343bf26c | ||
|
|
e3b4dc1928 | ||
|
|
a4939bacd8 | ||
|
|
4441942cf5 | ||
|
|
6597b02c09 | ||
|
|
0df3ad27dc | ||
|
|
696d458a27 | ||
|
|
fd18ab605d | ||
|
|
d47d14eecd | ||
|
|
24a81e1589 | ||
|
|
ac0c13f3e5 | ||
|
|
a7b90fa32c | ||
|
|
9eaf2a2e0f | ||
|
|
61e3cc33bf | ||
|
|
2be198400d | ||
|
|
830df682f9 | ||
|
|
2a420b2e90 | ||
|
|
53127c34ab | ||
|
|
9c57ff4853 | ||
|
|
a71176eda4 | ||
|
|
f59a85f412 |
@@ -48,12 +48,25 @@ types:
|
||||
date: date
|
||||
countTraces: integer
|
||||
countObservations: integer
|
||||
totalCost: double
|
||||
totalCost:
|
||||
type: double
|
||||
docs: Total model cost in USD
|
||||
usage: list<UsageByModel>
|
||||
UsageByModel:
|
||||
docs: Daily usage of a given model. Usage corresponds to the unit set for the specific model (e.g. tokens).
|
||||
properties:
|
||||
model: optional<string>
|
||||
inputUsage: integer
|
||||
outputUsage: integer
|
||||
totalUsage: integer
|
||||
inputUsage:
|
||||
type: integer
|
||||
docs: Total number of generation input units (e.g. tokens)
|
||||
outputUsage:
|
||||
type: integer
|
||||
docs: Total number of generation output units (e.g. tokens)
|
||||
totalUsage:
|
||||
type: integer
|
||||
docs: Total number of generation total units (e.g. tokens)
|
||||
countTraces: integer
|
||||
countObservations: integer
|
||||
totalCost:
|
||||
type: double
|
||||
docs: Total model cost in USD
|
||||
|
||||
@@ -70,14 +70,24 @@ types:
|
||||
name: string
|
||||
prompt: list<ChatMessage>
|
||||
config: optional<unknown>
|
||||
labels: optional<list<string>>
|
||||
labels:
|
||||
type: optional<list<string>>
|
||||
docs: List of deployment labels of this prompt version.
|
||||
tags:
|
||||
type: optional<list<string>>
|
||||
docs: List of tags to apply to all versions of this prompt.
|
||||
|
||||
CreateTextPromptRequest:
|
||||
properties:
|
||||
name: string
|
||||
prompt: string
|
||||
config: optional<unknown>
|
||||
labels: optional<list<string>>
|
||||
labels:
|
||||
type: optional<list<string>>
|
||||
docs: List of deployment labels of this prompt version.
|
||||
tags:
|
||||
type: optional<list<string>>
|
||||
docs: List of tags to apply to all versions of this prompt.
|
||||
|
||||
Prompt:
|
||||
union:
|
||||
@@ -89,7 +99,12 @@ types:
|
||||
name: string
|
||||
version: integer
|
||||
config: unknown
|
||||
labels: list<string>
|
||||
labels:
|
||||
type: list<string>
|
||||
docs: List of deployment labels of this prompt version.
|
||||
tags:
|
||||
type: list<string>
|
||||
docs: List of tags. Used to filter via UI and API. The same across versions of a prompt.
|
||||
|
||||
ChatMessage:
|
||||
properties:
|
||||
|
||||
@@ -30,6 +30,7 @@ service:
|
||||
docs: Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit.
|
||||
userId: optional<string>
|
||||
name: optional<string>
|
||||
sessionId: optional<string>
|
||||
fromTimestamp:
|
||||
type: optional<datetime>
|
||||
docs: Retrieve only traces newer than this datetime (ISO 8601).
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.47.2",
|
||||
"version": "2.47.7",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "dataset_items_source_trace_id_idx" ON "dataset_items" USING HASH ("source_trace_id");
|
||||
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "scores_project_id_name_idx" ON "scores"("project_id", "name");
|
||||
@@ -419,6 +419,7 @@ model Score {
|
||||
@@index(timestamp)
|
||||
@@index([value])
|
||||
@@index([projectId])
|
||||
@@index([projectId, name])
|
||||
@@index([authorUserId])
|
||||
@@index([configId])
|
||||
@@index([traceId], type: Hash)
|
||||
@@ -532,6 +533,7 @@ model DatasetItem {
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
datasetRunItems DatasetRunItems[]
|
||||
|
||||
@@index([sourceTraceId], type: Hash)
|
||||
@@index([sourceObservationId], type: Hash)
|
||||
@@index([datasetId], type: Hash)
|
||||
@@index([createdAt])
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.47.2",
|
||||
"version": "2.47.7",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -1262,6 +1262,12 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
- name: sessionId
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
- name: fromTimestamp
|
||||
in: query
|
||||
description: Retrieve only traces newer than this datetime (ISO 8601).
|
||||
@@ -2499,6 +2505,7 @@ components:
|
||||
totalCost:
|
||||
type: number
|
||||
format: double
|
||||
description: Total model cost in USD
|
||||
usage:
|
||||
type: array
|
||||
items:
|
||||
@@ -2521,14 +2528,28 @@ components:
|
||||
nullable: true
|
||||
inputUsage:
|
||||
type: integer
|
||||
description: Total number of generation input units (e.g. tokens)
|
||||
outputUsage:
|
||||
type: integer
|
||||
description: Total number of generation output units (e.g. tokens)
|
||||
totalUsage:
|
||||
type: integer
|
||||
description: Total number of generation total units (e.g. tokens)
|
||||
countTraces:
|
||||
type: integer
|
||||
countObservations:
|
||||
type: integer
|
||||
totalCost:
|
||||
type: number
|
||||
format: double
|
||||
description: Total model cost in USD
|
||||
required:
|
||||
- inputUsage
|
||||
- outputUsage
|
||||
- totalUsage
|
||||
- countTraces
|
||||
- countObservations
|
||||
- totalCost
|
||||
Observations:
|
||||
title: Observations
|
||||
type: object
|
||||
@@ -2654,6 +2675,13 @@ components:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
description: List of deployment labels of this prompt version.
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
description: List of tags to apply to all versions of this prompt.
|
||||
required:
|
||||
- name
|
||||
- prompt
|
||||
@@ -2672,6 +2700,13 @@ components:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
description: List of deployment labels of this prompt version.
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
description: List of tags to apply to all versions of this prompt.
|
||||
required:
|
||||
- name
|
||||
- prompt
|
||||
@@ -2713,11 +2748,20 @@ components:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of deployment labels of this prompt version.
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: >-
|
||||
List of tags. Used to filter via UI and API. The same across
|
||||
versions of a prompt.
|
||||
required:
|
||||
- name
|
||||
- version
|
||||
- config
|
||||
- labels
|
||||
- tags
|
||||
ChatMessage:
|
||||
title: ChatMessage
|
||||
type: object
|
||||
|
||||
@@ -699,7 +699,7 @@
|
||||
"auth": null,
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"type\": \"chat\",\n \"name\": \"example\",\n \"prompt\": [\n {\n \"role\": \"example\",\n \"content\": \"example\"\n }\n ],\n \"config\": \"UNKNOWN\",\n \"labels\": [\n \"example\"\n ]\n}",
|
||||
"raw": "{\n \"type\": \"chat\",\n \"name\": \"example\",\n \"prompt\": [\n {\n \"role\": \"example\",\n \"content\": \"example\"\n }\n ],\n \"config\": \"UNKNOWN\",\n \"labels\": [\n \"example\"\n ],\n \"tags\": [\n \"example\"\n ]\n}",
|
||||
"options": {
|
||||
"raw": {
|
||||
"language": "json"
|
||||
@@ -963,7 +963,7 @@
|
||||
"request": {
|
||||
"description": "Get list of traces.",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&fromTimestamp=&orderBy=&tags=",
|
||||
"raw": "{{baseUrl}}/api/public/traces?page=&limit=&userId=&name=&sessionId=&fromTimestamp=&orderBy=&tags=",
|
||||
"host": [
|
||||
"{{baseUrl}}"
|
||||
],
|
||||
@@ -993,6 +993,11 @@
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "sessionId",
|
||||
"value": "",
|
||||
"description": null
|
||||
},
|
||||
{
|
||||
"key": "fromTimestamp",
|
||||
"value": "",
|
||||
|
||||
@@ -18,7 +18,7 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
samplingContext.request.url &&
|
||||
samplingContext.request.url.includes("api/trpc")
|
||||
) {
|
||||
return 0.3;
|
||||
return 0.1;
|
||||
}
|
||||
if (
|
||||
samplingContext.request &&
|
||||
@@ -27,12 +27,12 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN)
|
||||
samplingContext.transactionContext.status !== "ok" &&
|
||||
samplingContext.transactionContext.status !== "unauthenticated"
|
||||
) {
|
||||
return 1;
|
||||
return 0.1;
|
||||
}
|
||||
return 0.1;
|
||||
return 0.01;
|
||||
},
|
||||
|
||||
profilesSampleRate: 0.2, // Profiling sample rate is relative to tracesSampleRate
|
||||
profilesSampleRate: 0.1,
|
||||
integrations: [
|
||||
// Add profiling integration to list of integrations
|
||||
new ProfilingIntegration(),
|
||||
|
||||
@@ -68,12 +68,18 @@ describe("/api/public/metrics/daily API Endpoint", () => {
|
||||
inputUsage: 333,
|
||||
outputUsage: 0,
|
||||
totalUsage: 333,
|
||||
countObservations: 1,
|
||||
countTraces: 1,
|
||||
totalCost: 0,
|
||||
},
|
||||
{
|
||||
model: "modelC",
|
||||
inputUsage: 666,
|
||||
outputUsage: 777,
|
||||
totalUsage: 1443,
|
||||
countObservations: 1,
|
||||
countTraces: 1,
|
||||
totalCost: 1024.22,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -88,6 +94,9 @@ describe("/api/public/metrics/daily API Endpoint", () => {
|
||||
inputUsage: 100,
|
||||
outputUsage: 200,
|
||||
totalUsage: 300,
|
||||
countObservations: 1,
|
||||
countTraces: 1,
|
||||
totalCost: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -680,6 +680,71 @@ describe("/api/public/v2/prompts API Endpoint", () => {
|
||||
expect(fetchedPrompt.body.createdBy).toBe("API");
|
||||
expect(fetchedPrompt.body.config).toEqual({});
|
||||
});
|
||||
|
||||
it("should update tags across versions", async () => {
|
||||
const promptName = "prompt-name" + nanoid();
|
||||
|
||||
const createPromptVersion = async (tags?: string[]) => {
|
||||
await makeAPICall("POST", baseURI, {
|
||||
name: promptName,
|
||||
prompt: "This is a test prompt",
|
||||
type: PromptType.Text,
|
||||
...(tags !== undefined && { tags: tags }),
|
||||
});
|
||||
};
|
||||
|
||||
const fetchPromptVersion = async (version: number) => {
|
||||
const fetchedPrompt = await makeAPICall(
|
||||
"GET",
|
||||
`${baseURI}/${promptName}?version=${version}`,
|
||||
undefined,
|
||||
);
|
||||
expect(fetchedPrompt.status).toBe(200);
|
||||
if (!isPrompt(fetchedPrompt.body)) {
|
||||
throw new Error("Expected body to be a prompt");
|
||||
}
|
||||
return fetchedPrompt.body;
|
||||
};
|
||||
|
||||
// Create version 1 with ["tag"]
|
||||
await createPromptVersion(["tag"]);
|
||||
let fetchedPrompt1 = await fetchPromptVersion(1);
|
||||
expect(fetchedPrompt1.tags).toEqual(["tag"]);
|
||||
expect(fetchedPrompt1.version).toBe(1);
|
||||
|
||||
// Create version 2 with no tags provided (should use tags from version 1)
|
||||
await createPromptVersion();
|
||||
let fetchedPrompt2 = await fetchPromptVersion(2);
|
||||
expect(fetchedPrompt2.tags).toEqual(["tag"]);
|
||||
expect(fetchedPrompt2.version).toBe(2);
|
||||
|
||||
// Create version 3 with ["tag1", "tag2", "tag3"] (should update tags across versions)
|
||||
await createPromptVersion(["tag1", "tag2", "tag3"]);
|
||||
fetchedPrompt1 = await fetchPromptVersion(1);
|
||||
fetchedPrompt2 = await fetchPromptVersion(2);
|
||||
let fetchedPrompt3 = await fetchPromptVersion(3);
|
||||
expect(fetchedPrompt1.tags).toEqual(["tag1", "tag2", "tag3"]);
|
||||
expect(fetchedPrompt1.version).toBe(1);
|
||||
expect(fetchedPrompt2.tags).toEqual(["tag1", "tag2", "tag3"]);
|
||||
expect(fetchedPrompt2.version).toBe(2);
|
||||
expect(fetchedPrompt3.tags).toEqual(["tag1", "tag2", "tag3"]);
|
||||
expect(fetchedPrompt3.version).toBe(3);
|
||||
|
||||
// remove tags
|
||||
await createPromptVersion([]);
|
||||
fetchedPrompt1 = await fetchPromptVersion(1);
|
||||
fetchedPrompt2 = await fetchPromptVersion(2);
|
||||
fetchedPrompt3 = await fetchPromptVersion(3);
|
||||
let fetchedPrompt4 = await fetchPromptVersion(4);
|
||||
expect(fetchedPrompt1.tags).toEqual([]);
|
||||
expect(fetchedPrompt1.version).toBe(1);
|
||||
expect(fetchedPrompt2.tags).toEqual([]);
|
||||
expect(fetchedPrompt2.version).toBe(2);
|
||||
expect(fetchedPrompt3.tags).toEqual([]);
|
||||
expect(fetchedPrompt3.version).toBe(3);
|
||||
expect(fetchedPrompt4.tags).toEqual([]);
|
||||
expect(fetchedPrompt4.version).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when fetching a prompt list", () => {
|
||||
|
||||
@@ -238,4 +238,62 @@ describe("/api/public/traces API Endpoint", () => {
|
||||
expect(trace.body.htmlPath).toContain(`/traces/${traceId}`);
|
||||
expect(trace.body.htmlPath).toContain(`/project/`); // do not know the projectId
|
||||
});
|
||||
|
||||
it("should filter traces by session ID", async () => {
|
||||
const sessionId = "test-session-id";
|
||||
const anotherSessionId = "another-session-id";
|
||||
|
||||
// Create traces with different session IDs
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-1",
|
||||
name: "test-trace-1",
|
||||
sessionId,
|
||||
userId: "user-1",
|
||||
projectId: "project-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
await makeAPICall("POST", "/api/public/traces", {
|
||||
id: "trace-2",
|
||||
name: "test-trace-2",
|
||||
sessionId: anotherSessionId,
|
||||
userId: "user-2",
|
||||
projectId: "project-1",
|
||||
metadata: { key: "value" },
|
||||
release: "1.0.0",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
// Filter by session ID
|
||||
const tracesBySessionId = await makeAPICall<GetTracesAPIResponse>(
|
||||
"GET",
|
||||
`/api/public/traces?sessionId=${sessionId}`,
|
||||
);
|
||||
|
||||
expect(tracesBySessionId.status).toBe(200);
|
||||
expect(tracesBySessionId.body.data).toHaveLength(1);
|
||||
expect(tracesBySessionId.body.data[0].id).toBe("trace-1");
|
||||
|
||||
// Filter by another session ID
|
||||
const tracesByAnotherSessionId = await makeAPICall<GetTracesAPIResponse>(
|
||||
"GET",
|
||||
`/api/public/traces?sessionId=${anotherSessionId}`,
|
||||
);
|
||||
|
||||
expect(tracesByAnotherSessionId.status).toBe(200);
|
||||
expect(tracesByAnotherSessionId.body.data).toHaveLength(1);
|
||||
expect(tracesByAnotherSessionId.body.data[0].id).toBe("trace-2");
|
||||
|
||||
// Filter by non-existent session ID
|
||||
const tracesByNonExistentSessionId =
|
||||
await makeAPICall<GetTracesAPIResponse>(
|
||||
"GET",
|
||||
`/api/public/traces?sessionId=non-existent-session-id`,
|
||||
);
|
||||
|
||||
expect(tracesByNonExistentSessionId.status).toBe(200);
|
||||
expect(tracesByNonExistentSessionId.body.data).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v2.47.2";
|
||||
export const VERSION = "v2.47.7";
|
||||
|
||||
@@ -49,8 +49,7 @@ export const SaveToPromptButton: React.FC = () => {
|
||||
)
|
||||
.data?.prompts.filter((prompt) => prompt.type === PromptType.Chat)
|
||||
.map((prompt) => ({
|
||||
label:
|
||||
prompt.name.slice(0, 20) + (prompt.name.length > 25 ? "..." : ""),
|
||||
label: prompt.name,
|
||||
value: prompt.id,
|
||||
})) ?? [];
|
||||
|
||||
@@ -126,7 +125,9 @@ export const SaveToPromptButton: React.FC = () => {
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{promptName.label}
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{promptName.label}
|
||||
</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandList>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { api } from "@/src/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { JsonEditor } from "@/src/components/json-editor";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import Link from "next/link";
|
||||
|
||||
const formSchema = z.object({
|
||||
modelName: z.string().min(1),
|
||||
@@ -255,11 +256,33 @@ export const NewModelForm = (props: {
|
||||
name="inputPrice"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Input price (USD)</FormLabel>
|
||||
<FormLabel>
|
||||
Input price (USD per{" "}
|
||||
{form.getValues("unit").toLowerCase().replace(/s$/, "")})
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
<FormDescription>Cost per input unit.</FormDescription>
|
||||
{field.value !== null && field.value !== "" ? (
|
||||
<FormDescription>
|
||||
<ul className="font-mono text-xs">
|
||||
<li>
|
||||
{(parseFloat(field.value ?? "0") * 1000).toFixed(4)} USD
|
||||
/ 1k {form.getValues("unit").toLowerCase()}
|
||||
</li>
|
||||
<li>
|
||||
{(parseFloat(field.value ?? "0") * 100_000).toFixed(4)}{" "}
|
||||
USD / 100k {form.getValues("unit").toLowerCase()}
|
||||
</li>
|
||||
<li>
|
||||
{(parseFloat(field.value ?? "0") * 1_000_000).toFixed(
|
||||
4,
|
||||
)}{" "}
|
||||
USD / 1M {form.getValues("unit").toLowerCase()}
|
||||
</li>
|
||||
</ul>
|
||||
</FormDescription>
|
||||
) : null}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -269,11 +292,33 @@ export const NewModelForm = (props: {
|
||||
name="outputPrice"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Output price (USD)</FormLabel>
|
||||
<FormLabel>
|
||||
Output price (USD per{" "}
|
||||
{form.getValues("unit").toLowerCase().replace(/s$/, "")})
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
<FormDescription>Cost per output unit.</FormDescription>
|
||||
{field.value !== null && field.value !== "" ? (
|
||||
<FormDescription>
|
||||
<ul className="font-mono text-xs">
|
||||
<li>
|
||||
{(parseFloat(field.value ?? "0") * 1000).toFixed(4)} USD
|
||||
/ 1k {form.getValues("unit").toLowerCase()}
|
||||
</li>
|
||||
<li>
|
||||
{(parseFloat(field.value ?? "0") * 100_000).toFixed(4)}{" "}
|
||||
USD / 100k {form.getValues("unit").toLowerCase()}
|
||||
</li>
|
||||
<li>
|
||||
{(parseFloat(field.value ?? "0") * 1_000_000).toFixed(
|
||||
4,
|
||||
)}{" "}
|
||||
USD / 1M {form.getValues("unit").toLowerCase()}
|
||||
</li>
|
||||
</ul>
|
||||
</FormDescription>
|
||||
) : null}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -283,12 +328,34 @@ export const NewModelForm = (props: {
|
||||
name="totalPrice"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Total price (USD)</FormLabel>
|
||||
<FormLabel>
|
||||
Total price (USD per{" "}
|
||||
{form.getValues("unit").toLowerCase().replace(/s$/, "")})
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Cost per unit, if no separate input/output prices.
|
||||
{field.value !== null && field.value !== "" ? (
|
||||
<ul className="mt-2 font-mono text-xs">
|
||||
<li>
|
||||
{(parseFloat(field.value ?? "0") * 1000).toFixed(4)} USD
|
||||
/ 1k {form.getValues("unit").toLowerCase()}
|
||||
</li>
|
||||
<li>
|
||||
{(parseFloat(field.value ?? "0") * 100_000).toFixed(4)}{" "}
|
||||
USD / 100k {form.getValues("unit").toLowerCase()}
|
||||
</li>
|
||||
<li>
|
||||
{(parseFloat(field.value ?? "0") * 1_000_000).toFixed(
|
||||
4,
|
||||
)}{" "}
|
||||
USD / 1M {form.getValues("unit").toLowerCase()}
|
||||
</li>
|
||||
</ul>
|
||||
) : (
|
||||
"Enter total price only if no separate input and output prices are provided."
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -328,7 +395,15 @@ export const NewModelForm = (props: {
|
||||
Optionally, Langfuse can tokenize the input and output of a
|
||||
generation if no unit counts are ingested. This is useful for
|
||||
e.g. streamed OpenAI completions. For details on the supported
|
||||
tokenizers, see the docs.
|
||||
tokenizers, see the{" "}
|
||||
<Link
|
||||
href="https://langfuse.com/docs/model-usage-and-cost"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
>
|
||||
docs
|
||||
</Link>
|
||||
.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -346,8 +421,15 @@ export const NewModelForm = (props: {
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
<FormDescription>
|
||||
The config for the tokenizer. Required for openai. See the
|
||||
docs for details.
|
||||
The config for the tokenizer. Required for openai. See the{" "}
|
||||
<Link
|
||||
href="https://langfuse.com/docs/model-usage-and-cost"
|
||||
className="underline"
|
||||
target="_blank"
|
||||
>
|
||||
docs
|
||||
</Link>{" "}
|
||||
for details.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { ValidationError } from "@langfuse/shared";
|
||||
import { jsonSchema } from "@/src/utils/zod";
|
||||
import { type PrismaClient } from "@langfuse/shared/src/db";
|
||||
import { LATEST_PROMPT_LABEL } from "@/src/features/prompts/constants";
|
||||
import { removeLabelsFromPreviousPromptVersions } from "@/src/features/prompts/server/utils/updatePromptLabels";
|
||||
import { updatePromptTagsOnAllVersions } from "@/src/features/prompts/server/utils/updatePromptTags";
|
||||
|
||||
export type CreatePromptParams = CreatePromptTRPCType & {
|
||||
createdBy: string;
|
||||
@@ -21,6 +23,7 @@ export const createPrompt = async ({
|
||||
config,
|
||||
createdBy,
|
||||
prisma,
|
||||
tags,
|
||||
}: CreatePromptParams) => {
|
||||
const latestPrompt = await prisma.prompt.findFirst({
|
||||
where: { projectId, name },
|
||||
@@ -35,14 +38,8 @@ export const createPrompt = async ({
|
||||
|
||||
const finalLabels = [...labels, LATEST_PROMPT_LABEL]; // Newly created prompts are always labeled as 'latest'
|
||||
|
||||
const previousLabeledPrompts = await prisma.prompt.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
name,
|
||||
labels: { hasSome: finalLabels },
|
||||
},
|
||||
orderBy: [{ version: "desc" }],
|
||||
});
|
||||
// If tags are undefined, use the tags from the latest prompt version
|
||||
const finalTags = [...new Set(tags ?? latestPrompt?.tags ?? [])];
|
||||
|
||||
const create = [
|
||||
prisma.prompt.create({
|
||||
@@ -52,7 +49,7 @@ export const createPrompt = async ({
|
||||
createdBy,
|
||||
labels: [...new Set(finalLabels)], // Ensure labels are unique
|
||||
type,
|
||||
tags: latestPrompt?.tags,
|
||||
tags: finalTags,
|
||||
version: latestPrompt?.version ? latestPrompt.version + 1 : 1,
|
||||
project: { connect: { id: projectId } },
|
||||
config: jsonSchema.parse(config),
|
||||
@@ -62,18 +59,28 @@ export const createPrompt = async ({
|
||||
|
||||
if (finalLabels.length > 0)
|
||||
// If we're creating a new labeled prompt, we must remove those labels on previous prompts since labels are unique
|
||||
previousLabeledPrompts.forEach((prevPrompt) => {
|
||||
create.push(
|
||||
prisma.prompt.update({
|
||||
where: { id: prevPrompt.id },
|
||||
data: {
|
||||
labels: prevPrompt.labels.filter(
|
||||
(prevLabel) => !finalLabels.includes(prevLabel),
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
create.push(
|
||||
...(await removeLabelsFromPreviousPromptVersions({
|
||||
prisma,
|
||||
projectId,
|
||||
promptName: name,
|
||||
labelsToRemove: finalLabels,
|
||||
})),
|
||||
);
|
||||
|
||||
const haveTagsChanged =
|
||||
JSON.stringify([...new Set(finalTags)].sort()) !==
|
||||
JSON.stringify([...new Set(latestPrompt?.tags)].sort());
|
||||
if (haveTagsChanged)
|
||||
// If we're creating a new prompt with tags, we must update those tags on previous prompts since tags are consistent across versions
|
||||
create.push(
|
||||
...(await updatePromptTagsOnAllVersions({
|
||||
prisma,
|
||||
projectId,
|
||||
promptName: name,
|
||||
tags: finalTags,
|
||||
})),
|
||||
);
|
||||
|
||||
const [createdPrompt] = await prisma.$transaction(create);
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { type PrismaClient } from "@langfuse/shared/src/db";
|
||||
|
||||
export const removeLabelsFromPreviousPromptVersions = async ({
|
||||
prisma,
|
||||
projectId,
|
||||
promptName,
|
||||
labelsToRemove,
|
||||
}: {
|
||||
prisma: PrismaClient;
|
||||
projectId: string;
|
||||
promptName: string;
|
||||
labelsToRemove: string[];
|
||||
}) => {
|
||||
const previouslyLabeledPrompts = await prisma.prompt.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
name: promptName,
|
||||
labels: { hasSome: labelsToRemove },
|
||||
},
|
||||
orderBy: [{ version: "desc" }],
|
||||
});
|
||||
|
||||
return previouslyLabeledPrompts.map((prevPrompt) =>
|
||||
prisma.prompt.update({
|
||||
where: { id: prevPrompt.id },
|
||||
data: {
|
||||
labels: prevPrompt.labels.filter(
|
||||
(prevLabel) => !labelsToRemove.includes(prevLabel),
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { type PrismaClient } from "@langfuse/shared/src/db";
|
||||
|
||||
export const updatePromptTagsOnAllVersions = async ({
|
||||
prisma,
|
||||
projectId,
|
||||
promptName,
|
||||
tags,
|
||||
}: {
|
||||
prisma: PrismaClient;
|
||||
projectId: string;
|
||||
promptName: string;
|
||||
tags: string[];
|
||||
}) => {
|
||||
const previousVersions = await prisma.prompt.findMany({
|
||||
where: { projectId, name: promptName },
|
||||
});
|
||||
|
||||
if (previousVersions.length === 0) return [];
|
||||
|
||||
return previousVersions.map((prevVersion) =>
|
||||
prisma.prompt.update({
|
||||
where: { id: prevVersion.id },
|
||||
data: {
|
||||
tags: [...new Set(tags)], // Ensure tags are unique
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -27,6 +27,7 @@ export const CreateTextPromptSchema = z.object({
|
||||
type: z.literal(PromptType.Text).optional(),
|
||||
prompt: z.string(),
|
||||
config: jsonSchema.nullable().default({}),
|
||||
tags: z.array(z.string()).nullish(),
|
||||
});
|
||||
|
||||
export const CreateChatPromptSchema = z.object({
|
||||
@@ -35,6 +36,7 @@ export const CreateChatPromptSchema = z.object({
|
||||
type: z.literal(PromptType.Chat),
|
||||
prompt: z.array(ChatMessageSchema),
|
||||
config: jsonSchema.nullable().default({}),
|
||||
tags: z.array(z.string()).nullish(),
|
||||
});
|
||||
|
||||
export const CreatePromptSchema = z.union([
|
||||
|
||||
@@ -53,10 +53,12 @@ if (
|
||||
setProjectInPosthog();
|
||||
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://eu.posthog.com",
|
||||
ui_host: "https://eu.posthog.com",
|
||||
// Enable debug mode in development
|
||||
loaded: (posthog) => {
|
||||
if (process.env.NODE_ENV === "development") posthog.debug();
|
||||
},
|
||||
autocapture: false,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -68,12 +68,15 @@ export default async function handler(
|
||||
DATE_TRUNC('DAY',
|
||||
o.start_time) "date",
|
||||
o.model,
|
||||
SUM(o.prompt_tokens) inputUsage,
|
||||
SUM(o.completion_tokens) outputUsage,
|
||||
SUM(o.total_tokens) totalUsage
|
||||
count(distinct o.id)::integer as "countObservations",
|
||||
count(distinct t.id)::integer as "countTraces",
|
||||
SUM(o.prompt_tokens) "inputUsage",
|
||||
SUM(o.completion_tokens) "outputUsage",
|
||||
SUM(o.total_tokens) "totalUsage",
|
||||
COALESCE(SUM(o.calculated_total_cost), 0)::DOUBLE PRECISION as "totalCost"
|
||||
FROM
|
||||
traces t
|
||||
LEFT JOIN observations o ON o.trace_id = t.id AND o.project_id = t.project_id
|
||||
LEFT JOIN observations_view o ON o.trace_id = t.id AND o.project_id = t.project_id
|
||||
WHERE o.start_time IS NOT NULL
|
||||
AND t.project_id = ${authCheck.scope.projectId}
|
||||
${traceNameCondition}
|
||||
@@ -94,11 +97,17 @@ export default async function handler(
|
||||
json_agg(json_build_object('model',
|
||||
model,
|
||||
'inputUsage',
|
||||
inputUsage,
|
||||
"inputUsage",
|
||||
'outputUsage',
|
||||
outputUsage,
|
||||
"outputUsage",
|
||||
'totalUsage',
|
||||
totalUsage)) daily_usage_json
|
||||
"totalUsage",
|
||||
'totalCost',
|
||||
"totalCost",
|
||||
'countObservations',
|
||||
"countObservations",
|
||||
'countTraces',
|
||||
"countTraces")) daily_usage_json
|
||||
FROM
|
||||
model_usage
|
||||
GROUP BY
|
||||
|
||||
@@ -25,6 +25,7 @@ const GetTracesSchema = z.object({
|
||||
userId: z.string().nullish(),
|
||||
name: z.string().nullish(),
|
||||
tags: z.union([z.array(z.string()), z.string()]).nullish(),
|
||||
sessionId: z.string().nullish(),
|
||||
fromTimestamp: stringDate,
|
||||
orderBy: z
|
||||
.string() // orderBy=timestamp.asc
|
||||
@@ -105,6 +106,9 @@ export default async function handler(
|
||||
", ",
|
||||
)}] <@ t."tags"`
|
||||
: Prisma.empty;
|
||||
const sessionCondition = obj.sessionId
|
||||
? Prisma.sql`AND t."session_id" = ${obj.sessionId}`
|
||||
: Prisma.empty;
|
||||
const fromTimestampCondition = obj.fromTimestamp
|
||||
? Prisma.sql`AND t."timestamp" >= ${obj.fromTimestamp}::timestamp with time zone at time zone 'UTC'`
|
||||
: Prisma.empty;
|
||||
@@ -145,6 +149,7 @@ export default async function handler(
|
||||
${userCondition}
|
||||
${nameCondition}
|
||||
${tagsCondition}
|
||||
${sessionCondition}
|
||||
${orderByCondition}
|
||||
LIMIT ${obj.limit} OFFSET ${skipValue}
|
||||
) AS t
|
||||
@@ -168,6 +173,7 @@ export default async function handler(
|
||||
projectId: authCheck.scope.projectId,
|
||||
name: obj.name ? obj.name : undefined,
|
||||
userId: obj.userId ? obj.userId : undefined,
|
||||
sessionId: obj.sessionId ? obj.sessionId : undefined,
|
||||
timestamp: obj.fromTimestamp
|
||||
? { gte: new Date(obj.fromTimestamp) }
|
||||
: undefined,
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Switch } from "@/src/components/ui/switch";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
|
||||
import { posthogIntegrationFormSchema } from "@/src/features/posthog-integration/types";
|
||||
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
|
||||
import { api } from "@/src/utils/api";
|
||||
import { type RouterOutput } from "@/src/utils/types";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -28,7 +29,13 @@ import { type z } from "zod";
|
||||
export default function PosthogIntegrationSettings() {
|
||||
const router = useRouter();
|
||||
const projectId = router.query.projectId as string;
|
||||
const state = api.posthogIntegration.get.useQuery({ projectId });
|
||||
const hasAccess = useHasAccess({ projectId, scope: "integrations:CRUD" });
|
||||
const state = api.posthogIntegration.get.useQuery(
|
||||
{ projectId },
|
||||
{
|
||||
enabled: hasAccess,
|
||||
},
|
||||
);
|
||||
if (env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === undefined) return null;
|
||||
|
||||
return (
|
||||
@@ -46,7 +53,7 @@ export default function PosthogIntegrationSettings() {
|
||||
</Button>
|
||||
}
|
||||
status={
|
||||
state.isInitialLoading
|
||||
state.isInitialLoading || !hasAccess
|
||||
? undefined
|
||||
: state.data?.enabled
|
||||
? "active"
|
||||
@@ -63,15 +70,20 @@ export default function PosthogIntegrationSettings() {
|
||||
on a daily schedule to PostHog. When first activated, it will sync all
|
||||
historical data from the beginning of your project.
|
||||
</p>
|
||||
<div className="flex flex-col gap-10"></div>
|
||||
|
||||
{!state.isInitialLoading && (
|
||||
{!hasAccess && (
|
||||
<p className="text-sm">
|
||||
You current role does not grant you access to these settings, please
|
||||
reach out to your project admin or owner.
|
||||
</p>
|
||||
)}
|
||||
{hasAccess && (
|
||||
<>
|
||||
<Header level="h3" title="Configuration" />
|
||||
<Card className="p-4">
|
||||
<PostHogIntegrationSettings
|
||||
state={state.data}
|
||||
projectId={projectId}
|
||||
isLoading={state.isLoading}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
@@ -97,9 +109,11 @@ export default function PosthogIntegrationSettings() {
|
||||
const PostHogIntegrationSettings = ({
|
||||
state,
|
||||
projectId,
|
||||
isLoading,
|
||||
}: {
|
||||
state?: RouterOutput["posthogIntegration"]["get"];
|
||||
projectId: string;
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
const capture = usePostHogClientCapture();
|
||||
const posthogForm = useForm<z.infer<typeof posthogIntegrationFormSchema>>({
|
||||
@@ -109,6 +123,7 @@ const PostHogIntegrationSettings = ({
|
||||
posthogProjectApiKey: state?.posthogApiKey ?? "",
|
||||
enabled: state?.enabled ?? false,
|
||||
},
|
||||
disabled: isLoading,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -205,12 +220,14 @@ const PostHogIntegrationSettings = ({
|
||||
<Button
|
||||
loading={mut.isLoading}
|
||||
onClick={posthogForm.handleSubmit(onSubmit)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
loading={mutDelete.isLoading}
|
||||
disabled={isLoading || !!!state}
|
||||
onClick={() => {
|
||||
if (
|
||||
confirm(
|
||||
|
||||
@@ -62,56 +62,54 @@ export const sessionRouter = createTRPCRouter({
|
||||
totalTokens: number;
|
||||
}>
|
||||
>(Prisma.sql`
|
||||
WITH observation_metrics AS (
|
||||
SELECT
|
||||
t.session_id,
|
||||
EXTRACT(EPOCH FROM COALESCE(MAX(o."end_time"), MAX(o."start_time"), MAX(t.timestamp))) - EXTRACT(EPOCH FROM COALESCE(MIN(o."start_time"), MIN(t.timestamp)))::double precision AS "sessionDuration",
|
||||
SUM(COALESCE(o."calculated_input_cost", 0)) AS "inputCost",
|
||||
SUM(COALESCE(o."calculated_output_cost", 0)) AS "outputCost",
|
||||
SUM(COALESCE(o."calculated_total_cost", 0)) AS "totalCost",
|
||||
SUM(o.prompt_tokens) AS "promptTokens",
|
||||
SUM(o.completion_tokens) AS "completionTokens",
|
||||
SUM(o.total_tokens) AS "totalTokens"
|
||||
FROM traces t
|
||||
LEFT JOIN observations_view o ON o.trace_id = t.id
|
||||
WHERE
|
||||
t."project_id" = ${input.projectId}
|
||||
AND o."project_id" = ${input.projectId}
|
||||
AND t.session_id IS NOT NULL
|
||||
GROUP BY 1
|
||||
),
|
||||
trace_metrics AS (
|
||||
SELECT
|
||||
session_id,
|
||||
array_agg(distinct t.user_id) "userIds",
|
||||
count(t.id)::int "countTraces"
|
||||
FROM traces t
|
||||
WHERE
|
||||
t."project_id" = ${input.projectId}
|
||||
AND t.session_id IS NOT NULL
|
||||
GROUP BY 1
|
||||
)
|
||||
|
||||
SELECT
|
||||
s.id,
|
||||
s."created_at" "createdAt",
|
||||
s. "created_at" AS "createdAt",
|
||||
s.bookmarked,
|
||||
s.public,
|
||||
t."userIds",
|
||||
t."countTraces",
|
||||
o."sessionDuration",
|
||||
COALESCE(o."totalCost", 0) AS "totalCost",
|
||||
COALESCE(o."inputCost", 0) AS "inputCost",
|
||||
COALESCE(o."outputCost", 0) AS "outputCost",
|
||||
COALESCE(o."promptTokens", 0) AS "promptTokens",
|
||||
COALESCE(o."completionTokens", 0) AS "completionTokens",
|
||||
COALESCE(o."totalTokens", 0) AS "totalTokens",
|
||||
t. "userIds",
|
||||
t. "countTraces",
|
||||
o. "sessionDuration",
|
||||
o. "totalCost" AS "totalCost",
|
||||
o. "inputCost" AS "inputCost",
|
||||
o. "outputCost" AS "outputCost",
|
||||
o. "promptTokens" AS "promptTokens",
|
||||
o. "completionTokens" AS "completionTokens",
|
||||
o. "totalTokens" AS "totalTokens",
|
||||
(count(*) OVER ())::int AS "totalCount"
|
||||
FROM trace_sessions s
|
||||
LEFT JOIN trace_metrics t ON t.session_id = s.id
|
||||
LEFT JOIN observation_metrics o ON o.session_id = s.id
|
||||
FROM
|
||||
trace_sessions AS s
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
t.session_id,
|
||||
MAX(t. "timestamp") AS "max_timestamp",
|
||||
MIN(t. "timestamp") AS "min_timestamp",
|
||||
array_agg(t.id) AS "traceIds",
|
||||
array_agg(DISTINCT t.user_id) AS "userIds",
|
||||
count(t.id)::int AS "countTraces"
|
||||
FROM
|
||||
traces t
|
||||
WHERE
|
||||
t.project_id = ${input.projectId}
|
||||
AND t.session_id = s.id
|
||||
GROUP BY
|
||||
t.session_id) AS t ON TRUE
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
EXTRACT(EPOCH FROM COALESCE(MAX(o. "end_time"), MAX(o. "start_time"), t. "max_timestamp")) - EXTRACT(EPOCH FROM COALESCE(MIN(o. "start_time"), t. "min_timestamp"))::double precision AS "sessionDuration",
|
||||
SUM(COALESCE(o. "calculated_input_cost", 0)) AS "inputCost",
|
||||
SUM(COALESCE(o. "calculated_output_cost", 0)) AS "outputCost",
|
||||
SUM(COALESCE(o. "calculated_total_cost", 0)) AS "totalCost",
|
||||
SUM(o.prompt_tokens) AS "promptTokens",
|
||||
SUM(o.completion_tokens) AS "completionTokens",
|
||||
SUM(o.total_tokens) AS "totalTokens"
|
||||
FROM
|
||||
observations_view o
|
||||
WHERE
|
||||
o.project_id = ${input.projectId}
|
||||
AND o.trace_id = ANY (t. "traceIds")) AS o ON TRUE
|
||||
WHERE
|
||||
s."project_id" = ${input.projectId}
|
||||
s. "project_id" = ${input.projectId}
|
||||
${filterCondition}
|
||||
${orderByCondition}
|
||||
LIMIT ${input.limit}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"crons": [
|
||||
{
|
||||
"path": "/api/cron/ingestion-metrics",
|
||||
"schedule": "*/15 * * * *"
|
||||
"schedule": "0 */6 * * *"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "worker",
|
||||
"version": "2.47.2",
|
||||
"version": "2.47.7",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
|
||||
+2
-2
@@ -31,9 +31,9 @@ if (isSentryEnabled) {
|
||||
Sentry.metrics.metricsAggregatorIntegration(),
|
||||
],
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: 0.1, // Capture 100% of the transactions
|
||||
tracesSampleRate: 0.01, // Capture 100% of the transactions
|
||||
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||
profilesSampleRate: 0.1,
|
||||
profilesSampleRate: 0.01,
|
||||
sampleRate: 0.1,
|
||||
});
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v2.47.2";
|
||||
export const VERSION = "v2.47.7";
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { Job, Queue, Worker } from "bullmq";
|
||||
import { BaseError, QueueName, TQueueJobTypes } from "@langfuse/shared";
|
||||
import {
|
||||
ApiError,
|
||||
BaseError,
|
||||
QueueName,
|
||||
TQueueJobTypes,
|
||||
} from "@langfuse/shared";
|
||||
import { evaluate, createEvalJobs } from "../eval-service";
|
||||
import { kyselyPrisma } from "@langfuse/shared/src/db";
|
||||
import logger from "../logger";
|
||||
@@ -59,11 +64,6 @@ export const evalJobExecutor = redis
|
||||
await evaluate({ event: job.data.payload });
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
e,
|
||||
`Failed Evaluation_Execution job for id ${job.data.payload.jobExecutionId} ${e}`
|
||||
);
|
||||
|
||||
const displayError =
|
||||
e instanceof BaseError ? e.message : "An internal error occurred";
|
||||
|
||||
@@ -76,7 +76,20 @@ export const evalJobExecutor = redis
|
||||
.where("project_id", "=", job.data.payload.projectId)
|
||||
.execute();
|
||||
|
||||
Sentry.captureException(e);
|
||||
// do not log expected errors (api failures + missing api keys not provided by the user)
|
||||
if (
|
||||
!(e instanceof ApiError) &&
|
||||
!(
|
||||
e instanceof BaseError &&
|
||||
e.message.includes("API key for provider")
|
||||
)
|
||||
) {
|
||||
logger.error(
|
||||
e,
|
||||
`Failed Evaluation_Execution job for id ${job.data.payload.jobExecutionId} ${e}`
|
||||
);
|
||||
Sentry.captureException(e);
|
||||
}
|
||||
|
||||
throw e;
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user