Compare commits

...
15 Commits
Author SHA1 Message Date
Max Deichmann fa26275634 chore: release v2.46.0
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-28 16:04:42 +02:00
Max DeichmannandGitHub 7587c75459 chore: do not log io (#2164) 2024-05-28 11:27:12 +00:00
ClemoandGitHub a956d0115c docs: update 2024-05-27 19:14:33 -07:00
Marc KlingenandGitHub cb82835489 fix(ui): overflow behavior of tag manager (#2156) 2024-05-27 17:13:32 +02:00
Hassieb PakzadandGitHub d1c1e60eca feat: use custom keys for playground (#2139) 2024-05-27 14:29:05 +02:00
Marc Klingen 81b612273a chore: release v2.45.1
CI/CD / lint (push) Waiting to run
CI/CD / test-docker-build (push) Waiting to run
CI/CD / tests-web (20) (push) Waiting to run
CI/CD / tests-worker (20) (push) Waiting to run
CI/CD / e2e-tests (push) Waiting to run
CI/CD / all-ci-passed (push) Blocked by required conditions
CI/CD / push-docker-image (push) Blocked by required conditions
release.yml / release (push) Waiting to run
2024-05-27 13:10:28 +02:00
Marc KlingenandGitHub 2c1abf2f6e feat(auth): add AWS Cognito provider (#2151)
Fixes #2107
2024-05-27 10:21:04 +00:00
Marc KlingenandGitHub db1a662f7f fix(ui): default hide version and release on trace table (#2150) 2024-05-27 10:06:22 +00:00
Richard KrümmelandGitHub 1b0e3de709 fix(ui): table header tooltips (#2148) 2024-05-27 11:20:48 +02:00
Richard KrümmelandGitHub be8aec2f8f feat(theme): add posthog (#2147) 2024-05-27 11:20:11 +02:00
adc01014c0 fix(scores): table source filter (#2144)
Co-authored-by: Marc Klingen <git@marcklingen.com>
2024-05-24 19:29:39 +00:00
Marc KlingenandGitHub 215af995fb perf: add indicies to foreign keys on job executions table (#2143) 2024-05-24 21:12:52 +02:00
marliessophieandGitHub 74ff237145 feat(scores): rename source REVIEW to ANNOTATION (#2141)
* feat(scores): add 'ANNOTATION' to `ScoreSource`

* feat: adjust seeder

* feat: adjust trpc router

* add: backfill scores source `REVIEW` to `ANNOTATION`

* add: drop `REVIEW` from scores source enum

* update yml

* refactor: rename trpc procedures
2024-05-24 19:58:30 +02:00
porter-deployment-app[bot]GitHubporter-deployment-app[bot] <87230664+porter-deployment-app[bot]@users.noreply.github.com>
5c2ae0e678 Enable Porter Application worker (#2138)
Create porter_stack_worker.yml file

Co-authored-by: porter-deployment-app[bot] <87230664+porter-deployment-app[bot]@users.noreply.github.com>
2024-05-24 14:42:46 +00:00
marliessophieandGitHub a6d8ce8c96 feat(score_config): add table via migration (#2126)
* feat: add author user id column to scores table

* feat: add author user id to scores created via trpc router

* feat: add author user id to scores created in seeder

* fixup

* add: index to author user id on scores

* feat: annotation config schema

* add: migration

* add: migrations in order

* fixup after merge

* fix: replicate to worker/types

* fix: address feedback

* rn: to scoreConfig

* push

* refactor: rn `ScoreDataType` to `ScoreConfigDataType`

* fix

* fix: scores server test

* context: order of returned data can vary based on how data is retrieved. Likely re-ordered due to new index on configId in scores table

* fix: re-generate types for correct alphabetical order

* fix: remodel migrations to match new schema

* feat: rm backfill migration for data_type on scores

* make data_type column optional on scores

* push: regenerate types

* push

* feat: make dataType on scores required again

* rename: scores add data type migration

* re-run migrations

* push
2024-05-23 19:26:16 +02:00
56 changed files with 563 additions and 230 deletions
+5
View File
@@ -58,6 +58,11 @@ LANGFUSE_CSP_ENFORCE_HTTPS="true"
# AUTH_AUTH0_CLIENT_SECRET=
# AUTH_AUTH0_ISSUER=
# AUTH_AUTH0_ALLOW_ACCOUNT_LINKING=false
# AUTH_COGNITO_CLIENT_ID=
# AUTH_COGNITO_CLIENT_SECRET=
# AUTH_COGNITO_ISSUER=
# AUTH_COGNITO_ALLOW_ACCOUNT_LINKING=false
# Transactional email, optional
# Defines the email address to use as the from address.
+29
View File
@@ -0,0 +1,29 @@
"on":
push:
branches:
- main
name: Deploy to worker
jobs:
porter-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set Github tag
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Setup porter
uses: porter-dev/setup-porter@v0.1.0
- name: Deploy stack
timeout-minutes: 30
run: exec porter apply
env:
PORTER_CLUSTER: "3959"
PORTER_DEPLOYMENT_TARGET_ID: d2bb23e4-1d77-48f2-a113-383d076959b3
PORTER_HOST: https://dashboard.getporter.dev
PORTER_PR_NUMBER: ${{ github.event.number }}
PORTER_PROJECT: "12565"
PORTER_REPO_NAME: ${{ github.event.repository.name }}
PORTER_STACK_NAME: worker
PORTER_TAG: ${{ steps.vars.outputs.sha_short }}
PORTER_TOKEN: ${{ secrets.PORTER_STACK_12565_3959 }}
+3 -3
View File
@@ -3,9 +3,9 @@ Join us in scaling Langfuse in Berlin, Germany. We are an open source company, w
_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
- Backend Engineer, 70-130k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/mnrdwla-backend-engineer
- Product Engineer, 70-130k EUR, 0.25-0.75% Equity, https://www.ycombinator.com/companies/langfuse/jobs/aAvmoFB-product-engineer
- Developer Advocate, 60-110k EUR, 0.25-0.5% Equity, https://www.ycombinator.com/companies/langfuse/jobs/uHysbKH-developer-advocate-devrel
_More Info_
+7
View File
@@ -2,6 +2,7 @@ import { type Provider } from "next-auth/providers/index";
import GoogleProvider from "next-auth/providers/google";
import GitHubProvider from "next-auth/providers/github";
import OktaProvider from "next-auth/providers/okta";
import CognitoProvider from "next-auth/providers/cognito";
import Auth0Provider from "next-auth/providers/auth0";
import AzureADProvider from "next-auth/providers/azure-ad";
import { isEeAvailable } from "..";
@@ -144,6 +145,12 @@ const dbToNextAuthProvider = (provider: SsoProviderSchema): Provider | null => {
...provider.authConfig,
clientSecret: decrypt(provider.authConfig.clientSecret),
});
else if (provider.authProvider === "cognito")
return CognitoProvider({
id: getAuthProviderIdForSsoConfig(provider), // use the domain as the provider id as we use domain-specific credentials
...provider.authConfig,
clientSecret: decrypt(provider.authConfig.clientSecret),
});
else {
// Type check to ensure we handle all providers
// eslint-disable-next-line no-unused-vars
+14
View File
@@ -64,11 +64,24 @@ export const AzureAdProviderSchema = base.extend({
.nullish(),
});
export const CognitoProviderSchema = base.extend({
authProvider: z.literal("cognito"),
authConfig: z
.object({
clientId: z.string(),
clientSecret: z.string(),
issuer: z.string(),
allowDangerousEmailAccountLinking: z.boolean().optional().default(false),
})
.nullish(),
});
export type GoogleProviderSchema = z.infer<typeof GoogleProviderSchema>;
export type GithubProviderSchema = z.infer<typeof GithubProviderSchema>;
export type Auth0ProviderSchema = z.infer<typeof Auth0ProviderSchema>;
export type OktaProviderSchema = z.infer<typeof OktaProviderSchema>;
export type AzureAdProviderSchema = z.infer<typeof AzureAdProviderSchema>;
export type CognitoProviderSchema = z.infer<typeof CognitoProviderSchema>;
export const SsoProviderSchema = z.discriminatedUnion("authProvider", [
GoogleProviderSchema,
@@ -76,6 +89,7 @@ export const SsoProviderSchema = z.discriminatedUnion("authProvider", [
Auth0ProviderSchema,
OktaProviderSchema,
AzureAdProviderSchema,
CognitoProviderSchema,
]);
export type SsoProviderSchema = z.infer<typeof SsoProviderSchema>;
+1 -1
View File
@@ -209,8 +209,8 @@ types:
- ARCHIVED
ScoreSource:
enum:
- ANNOTATION
- API
- REVIEW
- EVAL
errors:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "langfuse",
"version": "2.45.0",
"version": "2.46.0",
"author": "engineering@langfuse.com",
"license": "MIT",
"private": true,
+23 -1
View File
@@ -25,11 +25,16 @@ export const ObservationLevel = {
} as const;
export type ObservationLevel = (typeof ObservationLevel)[keyof typeof ObservationLevel];
export const ScoreSource = {
ANNOTATION: "ANNOTATION",
API: "API",
REVIEW: "REVIEW",
EVAL: "EVAL"
} as const;
export type ScoreSource = (typeof ScoreSource)[keyof typeof ScoreSource];
export const ScoreDataType = {
CATEGORICAL: "CATEGORICAL",
NUMERIC: "NUMERIC"
} as const;
export type ScoreDataType = (typeof ScoreDataType)[keyof typeof ScoreDataType];
export const PricingUnit = {
PER_1000_TOKENS: "PER_1000_TOKENS",
PER_1000_CHARS: "PER_1000_CHARS"
@@ -351,6 +356,22 @@ export type Score = {
comment: string | null;
trace_id: string;
observation_id: string | null;
config_id: string | null;
string_value: string | null;
data_type: Generated<ScoreDataType>;
};
export type ScoreConfig = {
id: string;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
project_id: string;
name: string;
data_type: ScoreDataType;
is_archived: Generated<boolean>;
min_value: number | null;
max_value: number | null;
categories: unknown | null;
description: string | null;
};
export type Session = {
id: string;
@@ -450,6 +471,7 @@ export type DB = {
project_memberships: ProjectMembership;
projects: Project;
prompts: Prompt;
score_configs: ScoreConfig;
scores: Score;
Session: Session;
sso_configs: SsoConfig;
@@ -0,0 +1,42 @@
-- CreateEnum
CREATE TYPE "ScoreDataType" AS ENUM ('CATEGORICAL', 'NUMERIC');
-- AlterTable
ALTER TABLE "scores" ADD COLUMN "config_id" TEXT,
ADD COLUMN "data_type" "ScoreDataType" NOT NULL DEFAULT 'NUMERIC',
ADD COLUMN "string_value" TEXT;
-- CreateTable
CREATE TABLE "score_configs" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"project_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"data_type" "ScoreDataType" NOT NULL,
"is_archived" BOOLEAN NOT NULL DEFAULT false,
"min_value" DOUBLE PRECISION,
"max_value" DOUBLE PRECISION,
"categories" JSONB,
"description" TEXT,
CONSTRAINT "score_configs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "score_configs_data_type_idx" ON "score_configs"("data_type");
-- CreateIndex
CREATE INDEX "score_configs_is_archived_idx" ON "score_configs"("is_archived");
-- CreateIndex
CREATE INDEX "score_configs_project_id_idx" ON "score_configs"("project_id");
-- CreateIndex
CREATE INDEX "score_configs_categories_idx" ON "score_configs"("categories");
-- CreateIndex
CREATE UNIQUE INDEX "score_configs_id_project_id_key" ON "score_configs"("id", "project_id");
-- AddForeignKey
ALTER TABLE "score_configs" ADD CONSTRAINT "score_configs_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "scores_config_id_idx" ON "scores"("config_id");
@@ -0,0 +1,2 @@
-- AddForeignKey
ALTER TABLE "scores" ADD CONSTRAINT "scores_config_id_fkey" FOREIGN KEY ("config_id") REFERENCES "score_configs"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "ScoreSource" ADD VALUE 'ANNOTATION';
@@ -0,0 +1,4 @@
-- Backfill the scores source for 'REVIEW' to be 'ANNOTATION'
UPDATE "scores"
SET "source" = 'ANNOTATION'::"ScoreSource"
WHERE "source" = 'REVIEW'::"ScoreSource";
@@ -0,0 +1,14 @@
/*
Warnings:
- The values [REVIEW] on the enum `ScoreSource` will be removed. If these variants are still used in the database, this will fail.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "ScoreSource_new" AS ENUM ('ANNOTATION', 'API', 'EVAL');
ALTER TABLE "scores" ALTER COLUMN "source" TYPE "ScoreSource_new" USING ("source"::text::"ScoreSource_new");
ALTER TYPE "ScoreSource" RENAME TO "ScoreSource_old";
ALTER TYPE "ScoreSource_new" RENAME TO "ScoreSource";
DROP TYPE "ScoreSource_old";
COMMIT;
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_job_configuration_id_idx" ON "job_executions"("job_configuration_id");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_job_output_score_id_idx" ON "job_executions"("job_output_score_id");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_job_input_trace_id_idx" ON "job_executions"("job_input_trace_id");
@@ -0,0 +1,2 @@
-- CreateIndex
CREATE INDEX CONCURRENTLY "job_executions_created_at_idx" ON "job_executions"("created_at");
+39 -1
View File
@@ -114,6 +114,7 @@ model Project {
LlmApiKeys LlmApiKeys[]
PosthogIntegration PosthogIntegration[]
Score Score[]
scoreConfig ScoreConfig[]
@@map("projects")
}
@@ -403,13 +404,18 @@ model Score {
comment String?
traceId String @map("trace_id")
observationId String? @map("observation_id")
configId String? @map("config_id")
stringValue String? @map("string_value")
dataType ScoreDataType @default(NUMERIC) @map("data_type")
JobExecution JobExecution[]
scoreConfig ScoreConfig? @relation(fields: [configId], references: [id], onDelete: SetNull)
@@unique([id, projectId]) // used for upserts via prisma
@@index(timestamp)
@@index([value])
@@index([projectId])
@@index([authorUserId])
@@index([configId])
@@index([traceId], type: Hash)
@@index([observationId], type: Hash)
@@index([source])
@@ -417,11 +423,39 @@ model Score {
}
enum ScoreSource {
ANNOTATION
API
REVIEW
EVAL
}
model ScoreConfig {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
projectId String @map("project_id")
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
name String
dataType ScoreDataType @map("data_type")
isArchived Boolean @default(false) @map("is_archived")
minValue Float? @map("min_value")
maxValue Float? @map("max_value")
categories Json? @map("categories")
description String?
score Score[]
@@unique([id, projectId]) // used for upserts via prisma
@@index([dataType])
@@index([isArchived])
@@index([projectId])
@@index([categories])
@@map("score_configs")
}
enum ScoreDataType {
CATEGORICAL
NUMERIC
}
enum PricingUnit {
PER_1000_TOKENS
PER_1000_CHARS
@@ -712,6 +746,10 @@ model JobExecution {
@@index([projectId, status])
@@index([projectId, id])
@@index([projectId])
@@index([jobConfigurationId])
@@index([jobOutputScoreId])
@@index([jobInputTraceId])
@@index([createdAt])
@@map("job_executions")
}
+20 -1
View File
@@ -12,6 +12,7 @@ import { chunk } from "lodash";
import { v4 } from "uuid";
import { ModelUsageUnit } from "../src";
import { getDisplaySecretKey, hashSecretKey } from "../src/server/auth";
import { encrypt } from "../src/encryption";
const LOAD_TRACE_VOLUME = 10_000;
@@ -165,6 +166,24 @@ async function main() {
await uploadObjects(traces, observations, scores, sessions, events);
// If openai key is in environment, add it to the projects LLM API keys
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
if (OPENAI_API_KEY) {
await prisma.llmApiKeys.create({
data: {
projectId: project1.id,
secretKey: encrypt(OPENAI_API_KEY),
displaySecretKey: getDisplaySecretKey(OPENAI_API_KEY),
provider: "openai",
},
});
} else {
console.warn(
"No OPENAI_API_KEY found in environment. Skipping seeding LLM API key."
);
}
// add eval objects
const evalTemplate = await prisma.evalTemplate.upsert({
where: {
@@ -500,7 +519,7 @@ function createObjects(
name: "manual-score",
value: Math.floor(Math.random() * 3) - 1,
timestamp: traceTs,
source: ScoreSource.REVIEW,
source: ScoreSource.ANNOTATION,
projectId,
authorUserId: `user-${i}`,
},
+4 -4
View File
@@ -466,8 +466,8 @@ importers:
specifier: ^7.51.5
version: 7.51.5(react@18.2.0)
react-icons:
specifier: ^5.0.1
version: 5.0.1(react@18.2.0)
specifier: ^5.2.1
version: 5.2.1(react@18.2.0)
react-responsive:
specifier: ^10.0.0
version: 10.0.0(react@18.2.0)
@@ -15018,8 +15018,8 @@ packages:
react: 18.2.0
dev: false
/react-icons@5.0.1(react@18.2.0):
resolution: {integrity: sha512-WqLZJ4bLzlhmsvme6iFdgO8gfZP17rfjYEJ2m9RsZjZ+cc4k1hTzknEz63YS1MeT50kVzoa1Nz36f4BEx+Wigw==}
/react-icons@5.2.1(react@18.2.0):
resolution: {integrity: sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==}
peerDependencies:
react: '*'
dependencies:
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.45.0",
"version": "2.46.0",
"private": true,
"license": "MIT",
"engines": {
@@ -106,7 +106,7 @@
"react-day-picker": "^8.10.1",
"react-dom": "18.2.0",
"react-hook-form": "^7.51.5",
"react-icons": "^5.0.1",
"react-icons": "^5.2.1",
"react-responsive": "^10.0.0",
"react18-json-view": "^0.2.8-canary.6",
"sonner": "^1.4.41",
+1 -1
View File
@@ -1825,8 +1825,8 @@ components:
title: ScoreSource
type: string
enum:
- ANNOTATION
- API
- REVIEW
- EVAL
CreateDatasetItemRequest:
title: CreateDatasetItemRequest
+15 -15
View File
@@ -489,16 +489,16 @@ describe("/api/public/scores API Endpoint", () => {
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
{
id: scoreId_2,
name: scoreName,
value: 50.5,
},
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
]);
});
it("test operator >=", async () => {
@@ -514,16 +514,16 @@ describe("/api/public/scores API Endpoint", () => {
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_2,
name: scoreName,
value: 50.5,
},
{
id: scoreId_3,
name: scoreName,
value: 100.8,
},
{
id: scoreId_2,
name: scoreName,
value: 50.5,
},
]);
});
it("test operator !=", async () => {
@@ -539,16 +539,16 @@ describe("/api/public/scores API Endpoint", () => {
totalPages: 1,
});
expect(getScore.body.data).toMatchObject([
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
{
id: scoreId_3,
name: scoreName,
value: 100.8,
},
{
id: scoreId_1,
name: scoreName,
value: 10.5,
},
]);
});
it("test operator =", async () => {
+6
View File
@@ -0,0 +1,6 @@
import chatCompletionHandler from "@/src/ee/features/playground/server/chatCompletionHandler";
export const dynamic = "force-dynamic";
export const maxDuration = 60;
export const POST = chatCompletionHandler;
@@ -0,0 +1,77 @@
import Link from "next/link";
import { Label } from "@/src/components/ui/label";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { api } from "@/src/utils/api";
import { supportedModels, type UIModelParams } from "@langfuse/shared";
import { ArrowTopRightIcon } from "@radix-ui/react-icons";
export const LLMApiKeyComponent = (p: {
projectId: string;
modelParams: UIModelParams;
}) => {
const hasAccess = useHasAccess({
projectId: p.projectId,
scope: "llmApiKeys:read",
});
if (!hasAccess) {
return (
<div>
<Label className="text-xs font-semibold">API key</Label>
<p className="text-sm text-muted-foreground">
LLM API Key only visible to Owner and Admin roles.
</p>
</div>
);
}
const apiKeys = api.llmApiKey.all.useQuery({
projectId: p.projectId,
});
if (apiKeys.isLoading) {
return (
<div>
<Label className="text-xs font-semibold">API key</Label>
<p className="text-sm text-muted-foreground">Loading...</p>
</div>
);
}
const model = p.modelParams.model.value;
const modelProvider = Object.entries(supportedModels).find((providerData) =>
(providerData[1] as any as string[]).includes(model),
)?.[0];
const apiKey = apiKeys.data?.data.find((k) => k.provider === modelProvider);
return (
<div className="space-y-2 text-xs">
<Label className="text-xs font-semibold">API key</Label>
<div>
{apiKey ? (
<span className="mr-2 rounded-sm bg-input p-1 text-xs">
{apiKey.displaySecretKey}
</span>
) : undefined}
</div>
{/* Custom form message to include a link to the already existing prompt */}
{!apiKey ? (
<div className="flex flex-col font-medium text-destructive">
{`No LLM API key found for provider ${modelProvider}.`}
<Link
href={`/project/${p.projectId}/settings`}
className="flex flex-row"
>
Create a new LLM API key here. <ArrowTopRightIcon />
</Link>
</div>
) : undefined}
<p className="text-muted-foreground">
The LLM API key is used for each execution and will incur costs.
</p>
</div>
);
};
@@ -8,6 +8,7 @@ import {
} from "@/src/components/ui/select";
import { Slider } from "@/src/components/ui/slider";
import { Switch } from "@/src/components/ui/switch";
import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL";
import { cn } from "@/src/utils/tailwind";
import {
ModelProvider,
@@ -15,6 +16,8 @@ import {
type UIModelParams,
} from "@langfuse/shared";
import { LLMApiKeyComponent } from "./LLMApiKeyComponent";
export type ModelParamsContext = {
modelParams: UIModelParams;
availableModels?: UIModelParams[];
@@ -33,10 +36,14 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
setModelParamEnabled,
formDisabled = false,
}) => {
const projectId = useProjectIdFromURL();
if (!projectId) return null;
return (
<div className="flex flex-col space-y-4">
<p className="font-semibold">Model</p>
<div className="space-y-6">
<div className="space-y-4">
<ModelParamsSelect
title="Provider"
modelParamsKey="provider"
@@ -104,6 +111,7 @@ export const ModelParameters: React.FC<ModelParamsContext> = ({
tooltip="An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both."
updateModelParam={updateModelParamValue}
/>
<LLMApiKeyComponent {...{ projectId, modelParams }} />
</div>
</div>
);
+5 -1
View File
@@ -4,6 +4,7 @@ import {
HoverCardTrigger,
} from "@/src/components/ui/hover-card";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { cn } from "@/src/utils/tailwind";
import { HelpCircle, Info } from "lucide-react";
import Link from "next/link";
@@ -40,7 +41,10 @@ export default function DocPopup({
}
}}
>
<HoverCardTrigger className="mx-1 cursor-pointer" asChild>
<HoverCardTrigger
className={cn("mx-1", href ? "cursor-pointer" : "cursor-default")}
asChild
>
{href ? (
<Link
href={href}
@@ -547,6 +547,7 @@ export default function TracesTable({
header: "Version",
enableHiding: true,
enableSorting: true,
defaultHidden: true,
},
{
accessorKey: "release",
@@ -554,6 +555,7 @@ export default function TracesTable({
header: "Release",
enableHiding: true,
enableSorting: true,
defaultHidden: true,
},
{
accessorKey: "tags",
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.45.0";
export const VERSION = "v2.46.0";
@@ -33,8 +33,6 @@ import {
type ModelParams,
} from "@langfuse/shared";
import { PromptDescription } from "@/src/features/prompts/components/prompt-description";
import Link from "next/dist/client/link";
import { ArrowTopRightIcon } from "@radix-ui/react-icons";
import {
Select,
SelectContent,
@@ -43,8 +41,6 @@ import {
SelectValue,
} from "@/src/components/ui/select";
import { TEMPLATES } from "@/src/ee/features/evals/components/templates";
import { Label } from "@/src/components/ui/label";
import { useHasAccess } from "@/src/features/rbac/utils/checkAccess";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import { getFinalModelParams } from "@/src/ee/utils/getFinalModelParams";
@@ -439,10 +435,6 @@ export const InnerEvalTemplateForm = (props: {
availableModels={[...evalLLMModels]}
formDisabled={!props.isEditing}
/>
<LLMApiKeyComponent
projectId={props.projectId}
modelParams={modelParams}
/>
</div>
</div>
@@ -465,78 +457,6 @@ export const InnerEvalTemplateForm = (props: {
);
};
export const LLMApiKeyComponent = (p: {
projectId: string;
modelParams: UIModelParams;
}) => {
const hasAccess = useHasAccess({
projectId: p.projectId,
scope: "llmApiKeys:read",
});
if (!hasAccess) {
return (
<div>
<Label>API key</Label>
<p className="text-sm text-muted-foreground">
LLM API Key only visible to Owner and Admin roles.
</p>
</div>
);
}
const apiKeys = api.llmApiKey.all.useQuery({
projectId: p.projectId,
});
if (apiKeys.isLoading) {
return (
<div>
<Label>API key</Label>
<p className="text-sm text-muted-foreground">Loading...</p>
</div>
);
}
const getModelProvider = (model: string) => {
return evalLLMModels.find((m) => m.model.value === model)?.provider.value;
};
const getApiKeyForModel = (model: string) => {
const modelProvider = getModelProvider(model);
return apiKeys.data?.data.find((k) => k.provider === modelProvider);
};
return (
<div>
<Label>API key</Label>
<div>
{getApiKeyForModel(p.modelParams.model.value) ? (
<span className="mr-2 rounded-sm bg-input p-1 text-xs">
{getApiKeyForModel(p.modelParams.model.value)?.displaySecretKey}
</span>
) : undefined}
</div>
{/* Custom form message to include a link to the already existing prompt */}
{!getApiKeyForModel(p.modelParams.model.value) ? (
<div className="flex flex-col text-sm font-medium text-destructive">
{"No LLM API key found."}
<Link
href={`/project/${p.projectId}/settings`}
className="flex flex-row"
>
Create a new API key here. <ArrowTopRightIcon />
</Link>
</div>
) : undefined}
<p className="text-sm text-muted-foreground">
The API key is used for each evaluation and will incur costs.
</p>
</div>
);
};
function getModelParamsWithEnabledFlag(
evalPreFill?: EvalTemplateFormPreFill,
): UIModelParams {
@@ -14,6 +14,7 @@ import useCommandEnter from "@/src/ee/features/playground/page/hooks/useCommandE
import usePlaygroundCache from "@/src/ee/features/playground/page/hooks/usePlaygroundCache";
import { getFinalModelParams } from "@/src/ee/utils/getFinalModelParams";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
import useProjectIdFromURL from "@/src/hooks/useProjectIdFromURL";
import { extractVariables } from "@/src/utils/string";
import {
ChatMessageRole,
@@ -57,6 +58,7 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
children,
}) => {
const capture = usePostHogClientCapture();
const projectId = useProjectIdFromURL();
const { playgroundCache, setPlaygroundCache } = usePlaygroundCache();
const [promptVariables, setPromptVariables] = useState<PromptVariable[]>([]);
const [output, setOutput] = useState("");
@@ -172,6 +174,7 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
}
const completionStream = getChatCompletionStream(
projectId,
finalMessages,
modelParams,
);
@@ -263,10 +266,17 @@ export const PlaygroundProvider: React.FC<PropsWithChildren> = ({
};
async function* getChatCompletionStream(
projectId: string | undefined,
messages: ChatMessageWithId[],
modelParams: UIModelParams,
) {
if (!projectId) {
console.error("Project ID is not set");
return;
}
const body = JSON.stringify({
projectId,
messages,
modelParams: getFinalModelParams(modelParams),
});
@@ -16,7 +16,7 @@ export default function Playground() {
<div className="basis-[55%] ">
<ModelParameters {...playgroundContext} />
</div>
<div className="basis-[45%] overflow-auto">
<div className="mt-4 basis-[45%] overflow-auto">
<Variables />
</div>
</div>
@@ -0,0 +1,26 @@
import { getServerSession } from "next-auth";
import { getIsCloudEnvironment } from "@/src/ee/utils/getIsCloudEnvironment";
import { getAuthOptions } from "@/src/server/auth";
import { isProjectMemberOrAdmin } from "@/src/server/utils/checkProjectMembershipOrAdmin";
import { ApiError, ForbiddenError, UnauthorizedError } from "@langfuse/shared";
export type AuthorizeRequestResult = {
userId: string;
};
export const authorizeRequestOrThrow = async (
projectId: string,
): Promise<AuthorizeRequestResult> => {
if (!getIsCloudEnvironment())
throw new ApiError("This endpoint is available in Langfuse cloud only.");
const authOptions = await getAuthOptions();
const session = await getServerSession(authOptions);
if (!session?.user) throw new UnauthorizedError("Unauthenticated");
if (!isProjectMemberOrAdmin(session.user, projectId))
throw new ForbiddenError("User is not a member of this project");
return { userId: session.user.id };
};
@@ -1,75 +1,60 @@
import { StreamingTextResponse } from "ai";
import { getToken } from "next-auth/jwt";
import { type NextRequest, NextResponse } from "next/server";
import { NextResponse, type NextRequest } from "next/server";
import { fetchLLMCompletion } from "@langfuse/shared";
import {
BaseError,
ValidationError,
fetchLLMCompletion,
} from "@langfuse/shared";
import { PosthogCallbackHandler } from "./analytics/posthogCallback";
import {
validateChatCompletionBody,
type ValidatedChatCompletionBody,
} from "./validateChatCompletionBody";
import { getCookieName } from "@/src/server/utils/cookies";
import { env } from "@/src/env.mjs";
import { getIsCloudEnvironment } from "@/src/ee/utils/getIsCloudEnvironment";
import { authorizeRequestOrThrow } from "./authorizeRequest";
import { validateChatCompletionBody } from "./validateChatCompletionBody";
import { prisma } from "@langfuse/shared/src/db";
import { decrypt } from "@langfuse/shared/encryption";
export default async function chatCompletionHandler(req: NextRequest) {
if (!getIsCloudEnvironment()) {
return NextResponse.json(
{ message: "This endpoint is available in Langfuse cloud only." },
{ status: 501 },
);
}
const token = await getToken({
req,
cookieName: getCookieName("next-auth.session-token"),
secret: env.NEXTAUTH_SECRET,
});
if (!token || !token.sub)
// sub is the user id
return NextResponse.json({ message: "Unauthenticated" }, { status: 401 });
if (req.method !== "POST")
return NextResponse.json(
{ message: "Method not allowed" },
{ status: 405 },
);
let body: ValidatedChatCompletionBody;
try {
body = validateChatCompletionBody(await req.json());
} catch (err) {
console.error(err);
const body = validateChatCompletionBody(await req.json());
const { userId } = await authorizeRequestOrThrow(body.projectId);
return NextResponse.json(
{
message: "Invalid request body",
error: err,
},
{ status: 400 },
);
}
try {
const { messages, modelParams } = body;
const LLMApiKey = await prisma.llmApiKeys.findFirst({
where: {
projectId: body.projectId,
provider: modelParams.provider,
},
});
if (!LLMApiKey)
throw new ValidationError(
`No ${modelParams.provider} API key found in project. Please add one in the project settings.`,
);
const stream = await fetchLLMCompletion({
messages,
modelParams,
streaming: true,
callbacks: [new PosthogCallbackHandler("playground", body, token.sub)],
apiKey:
modelParams.provider === "openai"
? env.OPENAI_API_KEY
: env.ANTHROPIC_API_KEY,
callbacks: [new PosthogCallbackHandler("playground", body, userId)],
apiKey: decrypt(LLMApiKey.secretKey),
});
return new StreamingTextResponse(stream);
} catch (err) {
console.error(err);
if (err instanceof BaseError) {
return NextResponse.json(
{
error: err.name,
message: err.message,
},
{ status: err.httpCode },
);
}
if (err instanceof Error) {
return NextResponse.json(
{
@@ -32,6 +32,7 @@ const MessageSchema = z.object({
id: z.string().optional(),
});
export const ChatCompletionBodySchema = z.object({
projectId: z.string(),
messages: z.array(MessageSchema),
modelParams: ModelParamsSchema,
});
+8
View File
@@ -67,6 +67,10 @@ export const env = createEnv({
AUTH_AUTH0_CLIENT_SECRET: z.string().optional(),
AUTH_AUTH0_ISSUER: z.string().url().optional(),
AUTH_AUTH0_ALLOW_ACCOUNT_LINKING: z.enum(["true", "false"]).optional(),
AUTH_COGNITO_CLIENT_ID: z.string().optional(),
AUTH_COGNITO_CLIENT_SECRET: z.string().optional(),
AUTH_COGNITO_ISSUER: z.string().url().optional(),
AUTH_COGNITO_ALLOW_ACCOUNT_LINKING: z.enum(["true", "false"]).optional(),
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT: z.string().optional(),
AUTH_DISABLE_USERNAME_PASSWORD: z.enum(["true", "false"]).optional(),
AUTH_DISABLE_SIGNUP: z.enum(["true", "false"]).optional(),
@@ -167,6 +171,10 @@ export const env = createEnv({
AUTH_AUTH0_ISSUER: process.env.AUTH_AUTH0_ISSUER,
AUTH_AUTH0_ALLOW_ACCOUNT_LINKING:
process.env.AUTH_AUTH0_ALLOW_ACCOUNT_LINKING,
AUTH_COGNITO_CLIENT_ID: process.env.AUTH_COGNITO_CLIENT_ID,
AUTH_COGNITO_CLIENT_SECRET: process.env.AUTH_COGNITO_CLIENT_SECRET,
AUTH_COGNITO_ISSUER: process.env.AUTH_COGNITO_ISSUER,
AUTH_COGNITO_ALLOW_ACCOUNT_LINKING: process.env.AUTH_COGNITO_ALLOW_ACCOUNT_LINKING,
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT:
process.env.AUTH_DOMAINS_WITH_SSO_ENFORCEMENT,
AUTH_DISABLE_USERNAME_PASSWORD: process.env.AUTH_DISABLE_USERNAME_PASSWORD,
@@ -11,7 +11,7 @@ const langfuseUrls = {
STAGING: "https://staging.langfuse.com",
};
const authUrl =
const getAuthURL = () =>
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "US" ||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "EU" ||
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION === "STAGING"
@@ -40,7 +40,7 @@ export const sendProjectInvitation = async (
invitedByUserEmail: inviterEmail,
projectName: projectName,
recieverEmail: to,
inviteLink: authUrl,
inviteLink: getAuthURL(),
langfuseCloudRegion: env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION,
}),
);
+1 -6
View File
@@ -1,13 +1,8 @@
import { z } from "zod";
import { ModelProvider } from "@langfuse/shared";
export const ZodModelProvider = z.enum([
ModelProvider.Anthropic,
ModelProvider.OpenAI,
]);
export const CreateLlmApiKey = z.object({
projectId: z.string(),
secretKey: z.string().min(1),
provider: z.literal(ModelProvider.OpenAI),
provider: z.nativeEnum(ModelProvider),
});
@@ -77,13 +77,13 @@ export function ManualScoreButton({
utils.sessions.invalidate(),
]);
};
const mutCreateScore = api.scores.createReviewScore.useMutation({
const mutCreateScore = api.scores.createAnnotationScore.useMutation({
onSuccess,
});
const mutUpdateScore = api.scores.updateReviewScore.useMutation({
const mutUpdateScore = api.scores.updateAnnotationScore.useMutation({
onSuccess,
});
const mutDeleteScore = api.scores.deleteReviewScore.useMutation({
const mutDeleteScore = api.scores.deleteAnnotationScore.useMutation({
onSuccess,
});
@@ -91,6 +91,7 @@ const events = {
"create_new_button_click",
],
onboarding: ["code_example_tab_switch"],
user_settings: ["theme_changed"],
project_settings: [
"project_delete",
"rename_form_submit",
@@ -175,7 +175,7 @@ function DeleteApiKeyButton(props: { projectId: string; apiKeyId: string }) {
const formSchema = z.object({
secretKey: z.string().min(1),
provider: z.literal(ModelProvider.OpenAI),
provider: z.nativeEnum(ModelProvider),
});
export function CreateLlmApiKeyComponent(props: {
@@ -277,7 +277,7 @@ export function CreateLlmApiKeyComponent(props: {
<Select
defaultValue={field.value}
onValueChange={(value) =>
field.onChange(value as ModelProvider[number])
field.onChange(value as ModelProvider)
}
>
<FormControl>
@@ -286,13 +286,7 @@ export function CreateLlmApiKeyComponent(props: {
</SelectTrigger>
</FormControl>
<SelectContent>
{Array.from(
new Set(
evalLLMModels.map(
(models) => models.provider.value,
),
),
).map((provider) => (
{Object.values(ModelProvider).map((provider) => (
<SelectItem value={provider} key={provider}>
{provider}
</SelectItem>
@@ -59,7 +59,7 @@ const TagManager = ({
return (
<Popover onOpenChange={(open) => handlePopoverChange(open)}>
<PopoverTrigger className="select-none" asChild>
<div className="flex gap-x-2 gap-y-1">
<div className="flex flex-wrap gap-x-2 gap-y-1">
<TagList selectedTags={selectedTags} isLoading={isLoading} />
</div>
</PopoverTrigger>
+11 -1
View File
@@ -2,10 +2,11 @@ import * as React from "react";
import { Monitor, Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { cn } from "@/src/utils/tailwind";
import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture";
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const capture = usePostHogClientCapture();
return (
<div className="ml-auto flex items-center space-x-1">
<div title="Light mode">
@@ -17,6 +18,9 @@ export function ThemeToggle() {
onClick={(e) => {
e.preventDefault();
setTheme("light");
capture("user_settings:theme_changed", {
theme: "light",
});
}}
/>
</div>
@@ -29,6 +33,9 @@ export function ThemeToggle() {
onClick={(e) => {
e.preventDefault();
setTheme("dark");
capture("user_settings:theme_changed", {
theme: "dark",
});
}}
/>
</div>
@@ -41,6 +48,9 @@ export function ThemeToggle() {
onClick={(e) => {
e.preventDefault();
setTheme("system");
capture("user_settings:theme_changed", {
theme: "system",
});
}}
/>
</div>
-5
View File
@@ -1,5 +0,0 @@
import chatCompletionHandler from "@/src/ee/features/playground/server/chatCompletionHandler";
export const runtime = "edge";
export default chatCompletionHandler;
+13 -7
View File
@@ -258,15 +258,21 @@ const handleSingleEvent = async (
req: NextApiRequest,
apiScope: ApiAccessScope,
) => {
if ("body" in event && "input" in event.body && "output" in event.body) {
const { body } = event;
let restEvent = body;
if ("input" in body) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { input, output, ...restEvent } = event.body;
console.log(
`handling single event ${event.id} ${JSON.stringify({ event, body: restEvent })}`,
);
} else {
console.log(`handling single event ${event.id} ${JSON.stringify(event)}`);
const { input, ...rest } = body;
restEvent = rest;
}
if ("output" in restEvent) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { output, ...rest } = restEvent;
restEvent = rest;
}
console.log(
`handling single event ${event.id} ${JSON.stringify({ event, body: restEvent })}`,
);
const cleanedEvent = ingestionEvent.parse(cleanEvent(event));
+19 -2
View File
@@ -14,7 +14,7 @@ import { env } from "@/src/env.mjs";
import { zodResolver } from "@hookform/resolvers/zod";
import { FcGoogle } from "react-icons/fc";
import { FaGithub } from "react-icons/fa";
import { SiOkta, SiAuth0 } from "react-icons/si";
import { SiOkta, SiAuth0, SiAmazoncognito } from "react-icons/si";
import { TbBrandAzure } from "react-icons/tb";
import { signIn } from "next-auth/react";
import Head from "next/head";
@@ -49,6 +49,7 @@ export type PageProps = {
okta: boolean;
azureAd: boolean;
auth0: boolean;
cognito: boolean;
sso: boolean;
};
signUpDisabled: boolean;
@@ -80,6 +81,10 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async () => {
env.AUTH_AUTH0_CLIENT_ID !== undefined &&
env.AUTH_AUTH0_CLIENT_SECRET !== undefined &&
env.AUTH_AUTH0_ISSUER !== undefined,
cognito:
env.AUTH_COGNITO_CLIENT_ID !== undefined &&
env.AUTH_COGNITO_CLIENT_SECRET !== undefined &&
env.AUTH_COGNITO_ISSUER !== undefined,
sso,
},
signUpDisabled: env.AUTH_DISABLE_SIGNUP === "true",
@@ -169,6 +174,18 @@ export function SSOButtons({
Auth0
</Button>
)}
{authProviders.cognito && (
<Button
onClick={() => {
capture("sign_in:button_click", { provider: "cognito" });
void signIn("cognito");
}}
variant="secondary"
>
<SiAmazoncognito className="mr-3" size={18} />
Cognito
</Button>
)}
</div>
</div>
) : null
@@ -421,7 +438,7 @@ export default function SignIn({ authProviders, signUpDisabled }: PageProps) {
No account yet?{" "}
<Link
href="/auth/sign-up"
className="hover:text-hover-primary-accent font-semibold leading-6 text-primary-accent"
className="font-semibold leading-6 text-primary-accent hover:text-hover-primary-accent"
>
Sign up
</Link>
@@ -1,6 +1,7 @@
import {
type OptionsDefinition,
type ColumnDefinition,
ScoreSource,
} from "@langfuse/shared";
export const scoresTableCols: ColumnDefinition[] = [
@@ -31,8 +32,9 @@ export const scoresTableCols: ColumnDefinition[] = [
{
name: "Source",
id: "source",
type: "string",
internal: 's."source"',
type: "stringOptions",
internal: 's."source"::text',
options: Object.values(ScoreSource).map((value) => ({ value })),
},
{
name: "Name",
+8 -8
View File
@@ -121,7 +121,7 @@ export const scoresRouter = createTRPCRouter({
return res;
}),
createReviewScore: protectedProjectProcedure
createAnnotationScore: protectedProjectProcedure
.input(
z.object({
projectId: z.string(),
@@ -158,7 +158,7 @@ export const scoresRouter = createTRPCRouter({
name: input.name,
comment: input.comment,
authorUserId: ctx.session.user.id,
source: "REVIEW",
source: "ANNOTATION",
},
});
await auditLog({
@@ -174,7 +174,7 @@ export const scoresRouter = createTRPCRouter({
});
return score;
}),
updateReviewScore: protectedProjectProcedure
updateAnnotationScore: protectedProjectProcedure
.input(
z.object({
projectId: z.string(),
@@ -193,11 +193,11 @@ export const scoresRouter = createTRPCRouter({
where: {
id: input.id,
projectId: input.projectId,
source: "REVIEW",
source: "ANNOTATION",
},
});
if (!score) {
throw new Error("No review score with this id in this project.");
throw new Error("No annotation score with this id in this project.");
}
await auditLog({
@@ -224,7 +224,7 @@ export const scoresRouter = createTRPCRouter({
},
});
}),
deleteReviewScore: protectedProjectProcedure
deleteAnnotationScore: protectedProjectProcedure
.input(z.object({ projectId: z.string(), id: z.string() }))
.mutation(async ({ input, ctx }) => {
throwIfNoAccess({
@@ -236,12 +236,12 @@ export const scoresRouter = createTRPCRouter({
const score = await ctx.prisma.score.findFirst({
where: {
id: input.id,
source: "REVIEW",
source: "ANNOTATION",
projectId: input.projectId,
},
});
if (!score) {
throw new Error("No review score with this id in this project.");
throw new Error("No annotation score with this id in this project.");
}
await auditLog({
+7 -2
View File
@@ -79,6 +79,7 @@ import superjson from "superjson";
import { ZodError } from "zod";
import { setUpSuperjson } from "@/src/utils/superjson";
import { DB } from "@/src/server/db";
import { isProjectMemberOrAdmin } from "@/src/server/utils/checkProjectMembershipOrAdmin";
setUpSuperjson();
@@ -177,7 +178,7 @@ const enforceUserIsAuthedAndProjectMember = t.middleware(
({ id }) => id === projectId,
);
if (!sessionProject && ctx.session.user.admin !== true)
if (!sessionProject && !isProjectMemberOrAdmin(ctx.session.user, projectId))
throw new TRPCError({
code: "UNAUTHORIZED",
message: "User is not a member of this project",
@@ -242,7 +243,11 @@ const enforceTraceAccess = t.middleware(async ({ ctx, rawInput, next }) => {
({ id }) => id === trace.projectId,
);
if (!trace.public && !sessionProject && ctx.session?.user?.admin !== true)
if (
!trace.public &&
!sessionProject &&
!isProjectMemberOrAdmin(ctx.session?.user, trace.projectId)
)
throw new TRPCError({
code: "UNAUTHORIZED",
message:
+23 -7
View File
@@ -19,9 +19,10 @@ import GoogleProvider from "next-auth/providers/google";
import GitHubProvider from "next-auth/providers/github";
import OktaProvider from "next-auth/providers/okta";
import Auth0Provider from "next-auth/providers/auth0";
import CognitoProvider from "next-auth/providers/cognito";
import AzureADProvider from "next-auth/providers/azure-ad";
import { type Provider } from "next-auth/providers/index";
import { getCookieName, cookieOptions } from "./utils/cookies";
import { getCookieName, getCookieOptions } from "./utils/cookies";
import {
getSsoAuthProviderIdForDomain,
loadSsoProviders,
@@ -182,6 +183,21 @@ if (
}),
);
if (
env.AUTH_COGNITO_CLIENT_ID &&
env.AUTH_COGNITO_CLIENT_SECRET &&
env.AUTH_COGNITO_ISSUER
)
staticProviders.push(
CognitoProvider({
clientId: env.AUTH_COGNITO_CLIENT_ID,
clientSecret: env.AUTH_COGNITO_CLIENT_SECRET,
issuer: env.AUTH_COGNITO_ISSUER,
allowDangerousEmailAccountLinking:
env.AUTH_COGNITO_ALLOW_ACCOUNT_LINKING === "true",
}),
);
// Extend Prisma Adapter
const prismaAdapter = PrismaAdapter(prisma);
const extendedPrismaAdapter: Adapter = {
@@ -314,27 +330,27 @@ export async function getAuthOptions(): Promise<NextAuthOptions> {
cookies: {
sessionToken: {
name: getCookieName("next-auth.session-token"),
options: cookieOptions,
options: getCookieOptions(),
},
csrfToken: {
name: getCookieName("next-auth.csrf-token"),
options: cookieOptions,
options: getCookieOptions(),
},
callbackUrl: {
name: getCookieName("next-auth.callback-url"),
options: cookieOptions,
options: getCookieOptions(),
},
state: {
name: getCookieName("next-auth.state"),
options: cookieOptions,
options: getCookieOptions(),
},
nonce: {
name: getCookieName("next-auth.nonce"),
options: cookieOptions,
options: getCookieOptions(),
},
pkceCodeVerifier: {
name: getCookieName("next-auth.pkce.code_verifier"),
options: cookieOptions,
options: getCookieOptions(),
},
},
events: {
@@ -0,0 +1,15 @@
import { type User } from "next-auth";
export const isProjectMemberOrAdmin = (
user: User | null | undefined,
projectId: string,
): boolean => {
if (!user) return false;
const isAdmin = user.admin === true;
const isProjectMember = user.projects.some(
(project) => project.id === projectId,
);
return isProjectMember || isAdmin;
};
+5 -5
View File
@@ -1,20 +1,20 @@
import { env } from "@/src/env.mjs";
// Use secure cookies on https hostnames, exception for Vercel which sets NEXTAUTH_URL without the protocol
const shouldSecureCookies =
const shouldSecureCookies = () =>
env.NEXTAUTH_URL.startsWith("https://") || process.env.VERCEL === "1";
export const cookieOptions = {
export const getCookieOptions = () => ({
domain: env.NEXTAUTH_COOKIE_DOMAIN ?? undefined,
httpOnly: true,
sameSite: "lax" as const,
path: "/",
secure: shouldSecureCookies,
};
secure: shouldSecureCookies(),
});
export const getCookieName = (name: string) =>
[
shouldSecureCookies ? "__Secure-" : "",
shouldSecureCookies() ? "__Secure-" : "",
name,
env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION
? `.${env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION}`
+23 -1
View File
@@ -28,11 +28,16 @@ export const ObservationLevel = {
export type ObservationLevel =
(typeof ObservationLevel)[keyof typeof ObservationLevel];
export const ScoreSource = {
ANNOTATION: "ANNOTATION",
API: "API",
REVIEW: "REVIEW",
EVAL: "EVAL",
} as const;
export type ScoreSource = (typeof ScoreSource)[keyof typeof ScoreSource];
export const ScoreDataType = {
CATEGORICAL: "CATEGORICAL",
NUMERIC: "NUMERIC",
} as const;
export type ScoreDataType = (typeof ScoreDataType)[keyof typeof ScoreDataType];
export const PricingUnit = {
PER_1000_TOKENS: "PER_1000_TOKENS",
PER_1000_CHARS: "PER_1000_CHARS",
@@ -356,6 +361,22 @@ export type Score = {
comment: string | null;
trace_id: string;
observation_id: string | null;
config_id: string | null;
string_value: string | null;
data_type: Generated<ScoreDataType>;
};
export type ScoreConfig = {
id: string;
created_at: Generated<Timestamp>;
updated_at: Generated<Timestamp>;
project_id: string;
name: string;
data_type: ScoreDataType;
is_archived: Generated<boolean>;
min_value: number | null;
max_value: number | null;
categories: unknown | null;
description: string | null;
};
export type Session = {
id: string;
@@ -455,6 +476,7 @@ export type DB = {
project_memberships: ProjectMembership;
projects: Project;
prompts: Prompt;
score_configs: ScoreConfig;
scores: Score;
Session: Session;
sso_configs: SsoConfig;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "worker",
"version": "2.45.0",
"version": "2.46.0",
"description": "",
"license": "MIT",
"main": "index.js",
+1 -1
View File
@@ -1 +1 @@
export const VERSION = "v2.45.0";
export const VERSION = "v2.46.0";