Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dc0b8a94d | ||
|
|
bc1bd8b3f4 | ||
|
|
8475328e23 | ||
|
|
36d755f994 | ||
|
|
59320c6168 | ||
|
|
8756ea9ed0 | ||
|
|
a16db15b76 | ||
|
|
930ca3b861 | ||
|
|
c0e3343364 | ||
|
|
f5d4ccb85b | ||
|
|
bf1ce92a51 | ||
|
|
3e40ec1195 | ||
|
|
c016ddda00 | ||
|
|
2617309493 | ||
|
|
6b87e1bb5f | ||
|
|
d40ecb4a5c | ||
|
|
e74d26e174 | ||
|
|
9b531c0b8e | ||
|
|
270f03640a | ||
|
|
8840e9c504 | ||
|
|
63766cf8f3 | ||
|
|
e5144dc401 | ||
|
|
297bacb0be | ||
|
|
06fd2c0552 | ||
|
|
adb12697f1 | ||
|
|
28426c9b3c | ||
|
|
a8214e379a | ||
|
|
d131bcfe5e | ||
|
|
cdddb7f5b8 | ||
|
|
2c7f670451 | ||
|
|
f01271ff76 | ||
|
|
b2f034d6bb | ||
|
|
25779cf2d8 | ||
|
|
a8bc224f47 | ||
|
|
ff4b03c0b7 | ||
|
|
0cfffb0726 | ||
|
|
fdeec9ce54 | ||
|
|
2b8701afc4 | ||
|
|
daba490c00 | ||
|
|
35190a308d | ||
|
|
fb071efb72 | ||
|
|
61fa20380b | ||
|
|
3d3bbf7703 | ||
|
|
392c4d720d | ||
|
|
32076fbb8b | ||
|
|
a82c0312f9 | ||
|
|
4775b8125d | ||
|
|
adb45c1ba2 | ||
|
|
fc20a0c768 | ||
|
|
4b91c51fe5 | ||
|
|
5f4ae9d29d | ||
|
|
0bb97d3ca4 | ||
|
|
f15ae605a8 | ||
|
|
14aa6fc571 | ||
|
|
56f47ec8bb | ||
|
|
f834b40209 | ||
|
|
164e7afdc6 | ||
|
|
4fecc8068d | ||
|
|
966662eebc | ||
|
|
e254a9e3e8 | ||
|
|
8d9fea8e2f | ||
|
|
116c63e9fa | ||
|
|
afbcbaf9e9 | ||
|
|
5db7c7ef36 | ||
|
|
40f507c5a3 | ||
|
|
d9e5184a33 | ||
|
|
169d6c8c9d | ||
|
|
33da50dd5d | ||
|
|
fa22296abd | ||
|
|
74a85d6a57 | ||
|
|
f307faa53a | ||
|
|
b9162bf507 | ||
|
|
eb24b5a607 | ||
|
|
e94eeb2694 | ||
|
|
428d22ac00 | ||
|
|
287687bb5d | ||
|
|
a499cd60bd | ||
|
|
aeb1933eaf | ||
|
|
fabebfb6db | ||
|
|
efc0747232 | ||
|
|
0ad4d544d5 | ||
|
|
90ff7a9926 | ||
|
|
2bd7d3ae78 | ||
|
|
eecdfe25e9 | ||
|
|
a159cda5e3 | ||
|
|
e604f39a70 | ||
|
|
8a32f6d90a | ||
|
|
d26a05963a | ||
|
|
8d8d05bd14 | ||
|
|
4637152a68 | ||
|
|
f9ee134e3e | ||
|
|
6860a0beba | ||
|
|
a604f8e61b | ||
|
|
eb7dc25428 | ||
|
|
3934762d40 | ||
|
|
2dcead678a | ||
|
|
5237d4b176 | ||
|
|
100ad2ecff | ||
|
|
617e396ba9 | ||
|
|
d1754b8d52 | ||
|
|
e4277eddcc | ||
|
|
1b4cd50c66 | ||
|
|
e30920406f | ||
|
|
94ed3a4b02 | ||
|
|
fc41347f39 | ||
|
|
80ed179fa6 | ||
|
|
7259baff17 | ||
|
|
68778ee4bc | ||
|
|
d1824a5e36 | ||
|
|
f30d6210dc | ||
|
|
bedc8b1e59 | ||
|
|
c3cc1d0a0d | ||
|
|
d570372e4a | ||
|
|
44825f5cb5 | ||
|
|
c9aa60f3b1 | ||
|
|
500d1dd0fa | ||
|
|
291b8f6420 | ||
|
|
8174e0153e | ||
|
|
88da95404f | ||
|
|
dcee2e9e39 | ||
|
|
cbd07b3d73 | ||
|
|
c5467ef614 | ||
|
|
e8d841ab43 | ||
|
|
4d0b5372e5 | ||
|
|
04dce2d7d9 | ||
|
|
9e47e12b6c | ||
|
|
f5328bc018 | ||
|
|
3cc6da2ff4 | ||
|
|
662e202041 | ||
|
|
6e6606f06e | ||
|
|
99562dbc6d | ||
|
|
60b5706c18 | ||
|
|
fb875bf26e | ||
|
|
695a2c01bc | ||
|
|
414c865fac | ||
|
|
3bf32dd981 | ||
|
|
2e58620f6e | ||
|
|
197125b4de | ||
|
|
cbd66df315 | ||
|
|
7681971dfa | ||
|
|
bb4cb1cb50 | ||
|
|
3695c70b92 | ||
|
|
9cbebbf4a0 | ||
|
|
8abc89b53f | ||
|
|
9667816ae0 | ||
|
|
246e844450 | ||
|
|
ca4fb68b53 | ||
|
|
02a83085e9 | ||
|
|
d9d9bc78a0 | ||
|
|
90cf22da84 | ||
|
|
0147836b41 | ||
|
|
ed11164d96 | ||
|
|
aa2369c4cb | ||
|
|
5ac1a18430 | ||
|
|
7c26edfb1e | ||
|
|
ced2fd3a40 | ||
|
|
7d328b404e | ||
|
|
3f77e7a091 | ||
|
|
4e02088135 | ||
|
|
24e165d595 | ||
|
|
212e88525e | ||
|
|
b520cad77b | ||
|
|
8fd5e5bfc1 | ||
|
|
3adc89e4d7 | ||
|
|
24195bb369 | ||
|
|
69c196c42d | ||
|
|
5d319e1fe3 | ||
|
|
c0120f7984 | ||
|
|
535bc2d2d7 | ||
|
|
a5db5aa940 | ||
|
|
f79a5cc52f | ||
|
|
44a8487410 | ||
|
|
a16f4a0526 | ||
|
|
60183f48fc | ||
|
|
2efaace4a8 | ||
|
|
ca24bebccd | ||
|
|
b7bb9eefe6 | ||
|
|
f2125ddc43 | ||
|
|
dc2789af31 | ||
|
|
39855914b5 | ||
|
|
a2b3ab9b93 | ||
|
|
2cdf13dc0d | ||
|
|
924d653993 | ||
|
|
e99b3a1e0e | ||
|
|
26ae2080d0 | ||
|
|
cd7f1b419f | ||
|
|
e835a9758e | ||
|
|
3d460b71f4 | ||
|
|
e33c6b42c9 | ||
|
|
80e441a211 | ||
|
|
6cbb93019e | ||
|
|
7f448db0ac | ||
|
|
5fae64e377 | ||
|
|
7aa0c4995c | ||
|
|
a8047b629b | ||
|
|
ae6acee9c9 | ||
|
|
1d0567426b | ||
|
|
d2deef409a | ||
|
|
240d4d7def | ||
|
|
99ffc45173 | ||
|
|
c6207c0f62 | ||
|
|
8499a38958 | ||
|
|
6f221302b7 | ||
|
|
f6ae7c5cd4 | ||
|
|
43b271ea4c | ||
|
|
b4fe4529d9 | ||
|
|
725a88f5ae | ||
|
|
1b2a2c01fa | ||
|
|
ae0857494b | ||
|
|
c278bc0ca1 | ||
|
|
5571ae376a | ||
|
|
c338b0409e | ||
|
|
95394b39aa | ||
|
|
1ebc413df4 | ||
|
|
12733ff7da | ||
|
|
41f064cb0f | ||
|
|
0e23a857c6 | ||
|
|
c77890c52a | ||
|
|
fa072d0074 | ||
|
|
0777afcd05 | ||
|
|
dba9fe80d1 | ||
|
|
a2e086d7e6 | ||
|
|
57537ad59e | ||
|
|
07e863f44a | ||
|
|
c50af94e91 | ||
|
|
22dade8808 | ||
|
|
41e6015580 | ||
|
|
c01bd573bb | ||
|
|
f45bc5ffe8 | ||
|
|
3224365277 | ||
|
|
6ec4eb4cf5 | ||
|
|
dacfb3b86e | ||
|
|
8f1b7e3db7 | ||
|
|
3570bbb22f | ||
|
|
5593d83f5b | ||
|
|
c64a9ba535 | ||
|
|
994b96e687 | ||
|
|
23f722bf7b | ||
|
|
d59b6a3b62 | ||
|
|
1857ae9d1e | ||
|
|
9d4d99010c | ||
|
|
1d3db1a6de | ||
|
|
1c629b9c0b | ||
|
|
9c799dfb33 | ||
|
|
009a932945 | ||
|
|
0b2cc19d92 | ||
|
|
64bd529c98 | ||
|
|
9fc7c990aa | ||
|
|
afbeb507db | ||
|
|
86af0efae7 | ||
|
|
be6ce949d6 | ||
|
|
09986a745a | ||
|
|
f1d6c26276 | ||
|
|
1515a49d53 | ||
|
|
0cec22ba56 | ||
|
|
dc23fa1d68 | ||
|
|
7d7ec1ed6c | ||
|
|
d18d2a0de9 | ||
|
|
51fd0edf80 | ||
|
|
a9621b2673 | ||
|
|
99ff3073b6 | ||
|
|
c04b990c49 | ||
|
|
4341215f70 | ||
|
|
e10102eb0b | ||
|
|
3bf5a351ef | ||
|
|
223f4571c9 | ||
|
|
ece6ee5144 | ||
|
|
66b850d365 | ||
|
|
dddc92e683 | ||
|
|
33022d7428 | ||
|
|
21de8052fa | ||
|
|
3d71892d16 | ||
|
|
6bcf0fad2c | ||
|
|
8df81dfdb5 | ||
|
|
080d73c7a5 | ||
|
|
2089a37ff5 | ||
|
|
8a90191156 | ||
|
|
9fe60a2971 | ||
|
|
0dd2a1ce0c |
@@ -0,0 +1,507 @@
|
||||
---
|
||||
name: add-model-price
|
||||
description: Add new LLM model pricing entries to Langfuse's default-model-prices.json. Use when adding model prices, updating model pricing, creating model entries, adding Claude/OpenAI/Anthropic/Google/Gemini/AWS Bedrock/Azure/Vertex AI model pricing, working with matchPattern regex, pricingTiers, or model cost configuration. Covers model price JSON structure, regex patterns for multi-provider matching, tiered pricing with conditions, cache pricing, and validation rules.
|
||||
---
|
||||
|
||||
# Add Model Price
|
||||
|
||||
## Purpose
|
||||
|
||||
Guide for adding new LLM model pricing entries to Langfuse's default model prices configuration. This enables accurate cost tracking across different model providers and deployment platforms.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Automatically activates when:
|
||||
- Adding a new model to the pricing database
|
||||
- Updating model pricing information
|
||||
- Working with `default-model-prices.json`
|
||||
- Creating model matchPattern regex
|
||||
- Configuring pricingTiers or tiered pricing
|
||||
- Adding prices for Claude, OpenAI, Anthropic, Google, Gemini, AWS Bedrock, Azure, or Vertex AI models
|
||||
|
||||
---
|
||||
|
||||
## Quick Start Checklist
|
||||
|
||||
- [ ] **Gather model info**: Fetch official pricing from provider documentation URL
|
||||
- [ ] **Generate UUID**: Run `uuidgen` for the model entry ID (use lowercase)
|
||||
- [ ] **Create matchPattern**: Regex covering all provider formats
|
||||
- [ ] **Define pricingTiers**: At minimum, one default tier with standard prices
|
||||
- [ ] **Add pricing entry**: Insert into `/worker/src/constants/default-model-prices.json`
|
||||
- [ ] **Add to LLM types**: Add model to `/packages/shared/src/server/llm/types.ts` (for playground/LLM-as-judge)
|
||||
- [ ] **Validate JSON**: Run `jq . default-model-prices.json` to verify syntax
|
||||
|
||||
---
|
||||
|
||||
## File Location
|
||||
|
||||
**Target File**: `/worker/src/constants/default-model-prices.json`
|
||||
|
||||
This JSON file contains an array of model pricing definitions used for cost calculation.
|
||||
|
||||
---
|
||||
|
||||
## Data Structure
|
||||
|
||||
### Complete Model Entry Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid-generated-with-uuidgen",
|
||||
"modelName": "model-name-identifier",
|
||||
"matchPattern": "(?i)^regex-pattern$",
|
||||
"createdAt": "ISO-8601-timestamp",
|
||||
"updatedAt": "ISO-8601-timestamp",
|
||||
"tokenizerConfig": null,
|
||||
"tokenizerId": "claude|openai|null",
|
||||
"pricingTiers": [
|
||||
{
|
||||
"id": "model-uuid_tier_default",
|
||||
"name": "Standard",
|
||||
"isDefault": true,
|
||||
"priority": 0,
|
||||
"conditions": [],
|
||||
"prices": {
|
||||
"input": 0.000005,
|
||||
"output": 0.000025
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Required Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | string | Unique UUID (use `uuidgen` command, lowercase) |
|
||||
| `modelName` | string | Primary model identifier |
|
||||
| `matchPattern` | string | Regex for matching model names |
|
||||
| `createdAt` | string | ISO-8601 timestamp |
|
||||
| `updatedAt` | string | ISO-8601 timestamp |
|
||||
| `pricingTiers` | array | At least one pricing tier |
|
||||
|
||||
### Optional Fields
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `tokenizerId` | string | null | "claude", "openai", or null |
|
||||
| `tokenizerConfig` | object | null | Custom tokenizer settings |
|
||||
|
||||
---
|
||||
|
||||
## Pricing Tier Structure
|
||||
|
||||
### Default Tier (Required)
|
||||
|
||||
Every model must have exactly one default tier:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "{model-id}_tier_default",
|
||||
"name": "Standard",
|
||||
"isDefault": true,
|
||||
"priority": 0,
|
||||
"conditions": [],
|
||||
"prices": { }
|
||||
}
|
||||
```
|
||||
|
||||
**Rules for Default Tier:**
|
||||
- `isDefault`: Must be `true`
|
||||
- `priority`: Must be `0`
|
||||
- `conditions`: Must be empty array `[]`
|
||||
|
||||
### Additional Tiers (Optional)
|
||||
|
||||
For usage-based pricing (e.g., large context pricing):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid-for-tier",
|
||||
"name": "Large Context (>200K)",
|
||||
"isDefault": false,
|
||||
"priority": 1,
|
||||
"conditions": [
|
||||
{
|
||||
"usageDetailPattern": "(input|prompt|cached)",
|
||||
"operator": "gt",
|
||||
"value": 200000,
|
||||
"caseSensitive": false
|
||||
}
|
||||
],
|
||||
"prices": { }
|
||||
}
|
||||
```
|
||||
|
||||
**Condition Operators:** `gt`, `gte`, `lt`, `lte`, `eq`, `neq`
|
||||
|
||||
---
|
||||
|
||||
## Gathering Pricing Information
|
||||
|
||||
**IMPORTANT**: Always fetch pricing from official provider documentation. Never use heuristics or assumptions.
|
||||
|
||||
### Official Pricing Sources
|
||||
|
||||
| Provider | URL |
|
||||
|----------|-----|
|
||||
| Anthropic Claude | https://platform.claude.com/docs/en/about-claude/pricing |
|
||||
| OpenAI | https://openai.com/api/pricing/ |
|
||||
| Google Gemini | https://ai.google.dev/pricing |
|
||||
| AWS Bedrock | https://aws.amazon.com/bedrock/pricing/ |
|
||||
| Azure OpenAI | https://azure.microsoft.com/pricing/details/cognitive-services/openai-service/ |
|
||||
|
||||
### Required Information to Gather
|
||||
|
||||
1. **Base input token price** (per MTok)
|
||||
2. **Output token price** (per MTok)
|
||||
3. **Cache write price** (if caching supported)
|
||||
4. **Cache read price** (if caching supported)
|
||||
5. **Long context pricing** (if different tiers exist)
|
||||
6. **Model ID formats** for all platforms (API, Bedrock, Vertex)
|
||||
|
||||
---
|
||||
|
||||
## Price Conversion
|
||||
|
||||
Prices in the JSON are **per token**, not per million tokens.
|
||||
|
||||
| Provider Pricing | JSON Value | Calculation |
|
||||
|-----------------|------------|-------------|
|
||||
| $5 / MTok | `5e-6` | $5 / 1,000,000 |
|
||||
| $25 / MTok | `25e-6` | $25 / 1,000,000 |
|
||||
| $0.50 / MTok | `0.5e-6` | $0.50 / 1,000,000 |
|
||||
| $6.25 / MTok | `6.25e-6` | $6.25 / 1,000,000 |
|
||||
|
||||
**Formula**: `price_per_token = price_per_mtok / 1_000_000` or `price_per_mtok * 1e-6`
|
||||
|
||||
---
|
||||
|
||||
## Common Price Keys by Provider
|
||||
|
||||
### Anthropic Claude Models
|
||||
|
||||
```json
|
||||
{
|
||||
"input": <base_input_price>,
|
||||
"input_tokens": <base_input_price>,
|
||||
"output": <output_price>,
|
||||
"output_tokens": <output_price>,
|
||||
"cache_creation_input_tokens": <cache_write_price>,
|
||||
"input_cache_creation": <cache_write_price>,
|
||||
"cache_read_input_tokens": <cache_read_price>,
|
||||
"input_cache_read": <cache_read_price>
|
||||
}
|
||||
```
|
||||
|
||||
### OpenAI Models
|
||||
|
||||
```json
|
||||
{
|
||||
"input": <input_price>,
|
||||
"input_cached_tokens": <cached_input_price>,
|
||||
"input_cache_read": <cached_input_price>,
|
||||
"output": <output_price>
|
||||
}
|
||||
```
|
||||
|
||||
### Google Gemini Models
|
||||
|
||||
```json
|
||||
{
|
||||
"input": <input_price>,
|
||||
"input_modality_1": <input_price>,
|
||||
"prompt_token_count": <input_price>,
|
||||
"promptTokenCount": <input_price>,
|
||||
"input_cached_tokens": <cached_price>,
|
||||
"cached_content_token_count": <cached_price>,
|
||||
"output": <output_price>,
|
||||
"output_modality_1": <output_price>,
|
||||
"candidates_token_count": <output_price>,
|
||||
"candidatesTokenCount": <output_price>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Match Pattern Examples
|
||||
|
||||
### Anthropic Claude (API + Bedrock + Vertex)
|
||||
|
||||
```regex
|
||||
(?i)^(anthropic\/)?(claude-opus-4-6|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4-6-v1(:0)?|claude-opus-4-6)$
|
||||
```
|
||||
|
||||
**Matches:**
|
||||
- `claude-opus-4-6` (direct API)
|
||||
- `anthropic/claude-opus-4-6` (with prefix)
|
||||
- `anthropic.claude-opus-4-6-v1:0` (AWS Bedrock)
|
||||
- `us.anthropic.claude-opus-4-6-v1:0` (regional Bedrock)
|
||||
- `claude-opus-4-6` (GCP Vertex)
|
||||
|
||||
### With Version Date
|
||||
|
||||
```regex
|
||||
(?i)^(anthropic\/)?(claude-opus-4-5-20251101|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4-5-20251101-v1:0|claude-opus-4-5@20251101)$
|
||||
```
|
||||
|
||||
### OpenAI
|
||||
|
||||
```regex
|
||||
(?i)^(openai\/)?(gpt-4o)$
|
||||
```
|
||||
|
||||
### Google Gemini
|
||||
|
||||
```regex
|
||||
(?i)^(google\/)?(gemini-2.5-pro)$
|
||||
```
|
||||
|
||||
### Pattern Components
|
||||
|
||||
| Component | Purpose | Example |
|
||||
|-----------|---------|---------|
|
||||
| `(?i)` | Case insensitive | Matches GPT-4o and gpt-4o |
|
||||
| `^...$` | Full string match | Prevents partial matches |
|
||||
| `(provider\/)?` | Optional provider prefix | `openai/gpt-4o` |
|
||||
| `(eu\\.\\|us\\.\\|apac\\.)?` | AWS regions | `us.anthropic.model` |
|
||||
| `(:0)?` | Optional version suffix | Bedrock model versions |
|
||||
| `@date` | Vertex AI format | `claude-3-5-sonnet@20240620` |
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step: Adding a New Model
|
||||
|
||||
### Step 1: Fetch Official Pricing
|
||||
|
||||
Use WebFetch to get pricing from official documentation:
|
||||
```
|
||||
WebFetch URL: https://platform.claude.com/docs/en/about-claude/pricing
|
||||
Prompt: Extract pricing for [model name] including input, output, cache write, cache read prices per MTok
|
||||
```
|
||||
|
||||
### Step 2: Generate UUID
|
||||
|
||||
```bash
|
||||
uuidgen
|
||||
# Output: 13458BC0-1C20-44C2-8753-172F54B67647
|
||||
# Convert to lowercase: 13458bc0-1c20-44c2-8753-172f54b67647
|
||||
```
|
||||
|
||||
### Step 3: Create the Entry
|
||||
|
||||
Example for a model with $5 input, $25 output, $6.25 cache write, $0.50 cache read:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "13458bc0-1c20-44c2-8753-172f54b67647",
|
||||
"modelName": "claude-opus-4-6",
|
||||
"matchPattern": "(?i)^(anthropic\/)?(claude-opus-4-6|(eu\\.|us\\.|apac\\.)?anthropic\\.claude-opus-4-6-v1(:0)?|claude-opus-4-6)$",
|
||||
"createdAt": "2026-02-09T00:00:00.000Z",
|
||||
"updatedAt": "2026-02-09T00:00:00.000Z",
|
||||
"tokenizerConfig": null,
|
||||
"tokenizerId": "claude",
|
||||
"pricingTiers": [
|
||||
{
|
||||
"id": "13458bc0-1c20-44c2-8753-172f54b67647_tier_default",
|
||||
"name": "Standard",
|
||||
"isDefault": true,
|
||||
"priority": 0,
|
||||
"conditions": [],
|
||||
"prices": {
|
||||
"input": 5e-6,
|
||||
"input_tokens": 5e-6,
|
||||
"output": 25e-6,
|
||||
"output_tokens": 25e-6,
|
||||
"cache_creation_input_tokens": 6.25e-6,
|
||||
"input_cache_creation": 6.25e-6,
|
||||
"cache_read_input_tokens": 0.5e-6,
|
||||
"input_cache_read": 0.5e-6
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Insert Entry
|
||||
|
||||
Add the entry to the JSON array in `/worker/src/constants/default-model-prices.json`.
|
||||
|
||||
**Placement**: Insert near related models (e.g., other Claude models together).
|
||||
|
||||
### Step 5: Add to LLM Types (for Playground & LLM-as-Judge)
|
||||
|
||||
To make the model available in the Langfuse playground and for LLM-as-a-judge evaluations, add it to the appropriate model array in `/packages/shared/src/server/llm/types.ts`.
|
||||
|
||||
**File**: `/packages/shared/src/server/llm/types.ts`
|
||||
|
||||
**Model Arrays by Provider:**
|
||||
- `anthropicModels` - Anthropic Claude models
|
||||
- `openAIModels` - OpenAI GPT models
|
||||
- `vertexAIModels` - Google Vertex AI models
|
||||
- `googleAIStudioModels` - Google AI Studio models
|
||||
|
||||
**IMPORTANT**: Do NOT add new models as the first entry in the array. The first entry is used as the default model for test LLM API calls, and newer models may not be available to all users yet.
|
||||
|
||||
**Example for Anthropic:**
|
||||
```typescript
|
||||
export const anthropicModels = [
|
||||
"claude-sonnet-4-5-20250929", // Keep existing first entry
|
||||
"claude-haiku-4-5-20251001",
|
||||
"claude-opus-4-6", // Add new model here (not first!)
|
||||
"claude-opus-4-5-20251101",
|
||||
// ... rest of models
|
||||
] as const;
|
||||
```
|
||||
|
||||
### Step 6: Validate
|
||||
|
||||
```bash
|
||||
# Check JSON syntax
|
||||
jq . /path/to/default-model-prices.json > /dev/null && echo "Valid JSON"
|
||||
|
||||
# Verify entry exists
|
||||
jq '.[] | select(.modelName == "claude-opus-4-6")' /path/to/default-model-prices.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multi-Tier Pricing Example
|
||||
|
||||
For models with long context pricing (e.g., different rates above 200K tokens):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid-here",
|
||||
"modelName": "model-name",
|
||||
"matchPattern": "...",
|
||||
"pricingTiers": [
|
||||
{
|
||||
"id": "uuid-here_tier_default",
|
||||
"name": "Standard",
|
||||
"isDefault": true,
|
||||
"priority": 0,
|
||||
"conditions": [],
|
||||
"prices": {
|
||||
"input": 5e-6,
|
||||
"output": 25e-6
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "uuid-for-large-context-tier",
|
||||
"name": "Large Context (>200K)",
|
||||
"isDefault": false,
|
||||
"priority": 1,
|
||||
"conditions": [
|
||||
{
|
||||
"usageDetailPattern": "(input|prompt|cached)",
|
||||
"operator": "gt",
|
||||
"value": 200000,
|
||||
"caseSensitive": false
|
||||
}
|
||||
],
|
||||
"prices": {
|
||||
"input": 10e-6,
|
||||
"output": 37.5e-6
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation Rules
|
||||
|
||||
The system validates pricing tiers with these rules:
|
||||
|
||||
1. **Exactly one default tier** with `isDefault: true`
|
||||
2. **Default tier** must have `priority: 0` and empty `conditions: []`
|
||||
3. **Non-default tiers** must have `priority > 0` and at least one condition
|
||||
4. **All priorities** must be unique within a model
|
||||
5. **All tier names** must be unique within a model
|
||||
6. **Each tier** must have at least one price
|
||||
7. **All tiers** must have identical usage type keys
|
||||
8. **Regex patterns** must be valid and safe (no catastrophic backtracking)
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
**Using heuristics instead of official pricing:**
|
||||
```json
|
||||
// Wrong - assuming cache is 1.25x input
|
||||
"cache_creation_input_tokens": input_price * 1.25
|
||||
|
||||
// Correct - use exact value from official docs
|
||||
"cache_creation_input_tokens": 6.25e-6
|
||||
```
|
||||
|
||||
**Incorrect Price Format:**
|
||||
```json
|
||||
// Wrong - using MTok price directly
|
||||
"input": 5
|
||||
|
||||
// Correct - price per token
|
||||
"input": 5e-6
|
||||
```
|
||||
|
||||
**Missing Tier ID Suffix:**
|
||||
```json
|
||||
// Wrong
|
||||
"id": "some-uuid"
|
||||
|
||||
// Correct for default tier
|
||||
"id": "model-uuid_tier_default"
|
||||
```
|
||||
|
||||
**Invalid Regex Escaping:**
|
||||
```json
|
||||
// Wrong - unescaped dots
|
||||
"matchPattern": "anthropic.claude"
|
||||
|
||||
// Correct - escaped dots
|
||||
"matchPattern": "anthropic\\.claude"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Model Matching
|
||||
|
||||
After adding a model, test that the regex matches expected inputs:
|
||||
|
||||
```javascript
|
||||
const pattern = new RegExp(matchPattern);
|
||||
console.log(pattern.test("claude-opus-4-6")); // true
|
||||
console.log(pattern.test("anthropic/claude-opus-4-6")); // true
|
||||
console.log(pattern.test("anthropic.claude-opus-4-6-v1:0")); // true
|
||||
console.log(pattern.test("us.anthropic.claude-opus-4-6-v1:0")); // true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reference: Existing Model Entries
|
||||
|
||||
Look at these existing entries as templates:
|
||||
|
||||
| Model Type | Example Entry | Notes |
|
||||
|------------|---------------|-------|
|
||||
| Anthropic Claude | `claude-opus-4-5-20251101` | Full multi-provider pattern |
|
||||
| OpenAI GPT | `gpt-4o` | Simple pattern |
|
||||
| Google Gemini | `gemini-2.5-pro` | Multi-tier with large context |
|
||||
|
||||
---
|
||||
|
||||
## Related Files
|
||||
|
||||
- **Pricing JSON**: `/worker/src/constants/default-model-prices.json`
|
||||
- **LLM Types**: `/packages/shared/src/server/llm/types.ts` (model arrays for playground/LLM-as-judge)
|
||||
- **Validation**: `/packages/shared/src/server/pricing-tiers/validation.ts`
|
||||
- **Matcher**: `/packages/shared/src/server/pricing-tiers/matcher.ts`
|
||||
- **Tests**: `/web/src/__tests__/async/model-pricing-tiers.servertest.ts`
|
||||
|
||||
---
|
||||
|
||||
**Skill Status**: COMPLETE
|
||||
**Line Count**: ~340 lines
|
||||
@@ -50,6 +50,7 @@ Automatically activates when working on:
|
||||
- [ ] **Authentication**: Authorization via basic auth
|
||||
- [ ] **Validation**: Zod schemas for query/body/response
|
||||
- [ ] **Versioning**: Versioning in API path and Zod schemas for query/body/response
|
||||
- [ ] **Fern API Docs**: Update `fern/apis/server/definition/` to match TypeScript types
|
||||
- [ ] **Tests**: Add end-to-end test in `__tests__/async/`
|
||||
|
||||
### New Queue Processor Checklist (Worker)
|
||||
@@ -423,6 +424,32 @@ const traces = await queryClickhouse({
|
||||
});
|
||||
```
|
||||
|
||||
### 9. Keep Fern API Definitions in Sync with TypeScript Types
|
||||
|
||||
When modifying public API types in `web/src/features/public-api/types/`, the corresponding Fern API definitions in `fern/apis/server/definition/` must be updated to match.
|
||||
|
||||
**Zod to Fern Type Mapping:**
|
||||
|
||||
| Zod Type | Fern Type | Example |
|
||||
| -------- | --------- | ------- |
|
||||
| `.nullish()` | `optional<nullable<T>>` | `z.string().nullish()` → `optional<nullable<string>>` |
|
||||
| `.nullable()` | `nullable<T>` | `z.string().nullable()` → `nullable<string>` |
|
||||
| `.optional()` | `optional<T>` | `z.string().optional()` → `optional<string>` |
|
||||
| Always present | `T` | `z.string()` → `string` |
|
||||
|
||||
**Source References:**
|
||||
|
||||
Add a comment at the top of each Fern type referencing the TypeScript source file:
|
||||
|
||||
```yaml
|
||||
# Source: web/src/features/public-api/types/traces.ts - APITrace
|
||||
Trace:
|
||||
properties:
|
||||
id: string
|
||||
name:
|
||||
type: nullable<string>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Imports
|
||||
|
||||
@@ -645,7 +645,42 @@ async doIt()
|
||||
async execute()
|
||||
```
|
||||
|
||||
### 3. Return Types
|
||||
### 3. Use Params Objects for Multiple Arguments
|
||||
|
||||
When a function receives multiple arguments, use a single params object instead of positional arguments:
|
||||
|
||||
```typescript
|
||||
// ❌ BAD - Positional arguments are unclear and can be swapped
|
||||
async function createTrace(
|
||||
projectId: string,
|
||||
userId: string,
|
||||
sessionId: string,
|
||||
name: string,
|
||||
) {}
|
||||
|
||||
// Call site - which string is which?
|
||||
await createTrace(projectId, userId, sessionId, name);
|
||||
|
||||
// ✅ GOOD - Params object makes intent clear
|
||||
async function createTrace(params: {
|
||||
projectId: string;
|
||||
userId: string;
|
||||
sessionId: string;
|
||||
name: string;
|
||||
}) {}
|
||||
|
||||
// Call site - clear and prevents argument swapping bugs
|
||||
await createTrace({ projectId, userId, sessionId, name });
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- More readable at call sites
|
||||
- Prevents bugs when positional arguments of the same type are accidentally swapped
|
||||
- Easier to add optional parameters later
|
||||
- Self-documenting code
|
||||
|
||||
### 4. Return Types
|
||||
|
||||
Always use explicit return types:
|
||||
|
||||
@@ -659,7 +694,7 @@ async deleteUser(id: string): Promise<void> {}
|
||||
async createUser(data) {} // No types!
|
||||
```
|
||||
|
||||
### 4. Error Handling
|
||||
### 5. Error Handling
|
||||
|
||||
Services should throw meaningful errors:
|
||||
|
||||
@@ -679,7 +714,7 @@ if (!user) {
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Avoid God Services
|
||||
### 6. Avoid God Services
|
||||
|
||||
Don't create services that do everything:
|
||||
|
||||
|
||||
@@ -25,6 +25,53 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"add-model-price": {
|
||||
"type": "domain",
|
||||
"enforcement": "suggest",
|
||||
"priority": "high",
|
||||
"description": "Guide for adding LLM model pricing entries to default-model-prices.json",
|
||||
"promptTriggers": {
|
||||
"keywords": [
|
||||
"model price",
|
||||
"model pricing",
|
||||
"model prices",
|
||||
"add model",
|
||||
"new model",
|
||||
"model cost",
|
||||
"default-model-prices",
|
||||
"matchPattern",
|
||||
"pricingTiers",
|
||||
"pricing tier",
|
||||
"token pricing",
|
||||
"Claude pricing",
|
||||
"OpenAI pricing",
|
||||
"Anthropic pricing",
|
||||
"Gemini pricing",
|
||||
"Bedrock pricing",
|
||||
"Vertex pricing"
|
||||
],
|
||||
"intentPatterns": [
|
||||
"(add|create|insert|update).*?(model|pricing|price)",
|
||||
"(new|add).*?(claude|openai|anthropic|gemini|gpt).*?(model|price)",
|
||||
"model.*?(price|pricing|cost)",
|
||||
"pricing.*?tier"
|
||||
]
|
||||
},
|
||||
"fileTriggers": {
|
||||
"pathPatterns": [
|
||||
"worker/src/constants/default-model-prices.json",
|
||||
"packages/shared/src/server/llm/types.ts",
|
||||
"**/model*price*.json"
|
||||
],
|
||||
"contentPatterns": [
|
||||
"matchPattern",
|
||||
"pricingTiers",
|
||||
"tokenizerId",
|
||||
"anthropicModels",
|
||||
"openAIModels"
|
||||
]
|
||||
}
|
||||
},
|
||||
"backend-dev-guidelines": {
|
||||
"type": "domain",
|
||||
"enforcement": "suggest",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:20-bookworm
|
||||
|
||||
# Install golang-migrate for database migrations
|
||||
RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz && \
|
||||
RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz && \
|
||||
chmod +x migrate && \
|
||||
mv migrate /usr/local/bin/migrate
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ NEXTAUTH_SECRET="secret"
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV"
|
||||
|
||||
# Langfuse experimental features
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
|
||||
|
||||
# Salt for API key hashing
|
||||
SALT="salt"
|
||||
|
||||
@@ -25,7 +25,7 @@ NEXTAUTH_SECRET="secret"
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV"
|
||||
|
||||
# Langfuse experimental features
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
|
||||
|
||||
# Salt for API key hashing
|
||||
SALT="salt"
|
||||
|
||||
+5
-1
@@ -46,6 +46,8 @@ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
|
||||
# Clickhouse
|
||||
CLICKHOUSE_MIGRATION_URL="clickhouse://localhost:9000"
|
||||
CLICKHOUSE_URL="http://localhost:8123"
|
||||
# CLICKHOUSE_READ_ONLY_URL="http://localhost:8123" # Optional: read replica for legacy tables
|
||||
# CLICKHOUSE_EVENTS_READ_ONLY_URL="http://localhost:8123" # Optional: read replica for events table queries
|
||||
CLICKHOUSE_USER="clickhouse"
|
||||
CLICKHOUSE_PASSWORD="clickhouse"
|
||||
CLICKHOUSE_CLUSTER_ENABLED="false"
|
||||
@@ -62,7 +64,7 @@ NEXTAUTH_SECRET="secret"
|
||||
NEXT_PUBLIC_LANGFUSE_CLOUD_REGION="DEV"
|
||||
|
||||
# Langfuse experimental features
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="true"
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES="false"
|
||||
|
||||
# Salt for API key hashing
|
||||
SALT="salt"
|
||||
@@ -147,3 +149,5 @@ LANGFUSE_AWS_BEDROCK_MODEL="eu.anthropic.claude-3-haiku-20240307-v1:0"
|
||||
# Events table migration
|
||||
LANGFUSE_ENABLE_EVENTS_TABLE_OBSERVATIONS=true
|
||||
LANGFUSE_ENABLE_EVENTS_TABLE_FLAGS=true
|
||||
LANGFUSE_ENABLE_EVENTS_TABLE_V2_APIS=true
|
||||
LANGFUSE_EXPERIMENT_INSERT_INTO_EVENTS_TABLE=true
|
||||
|
||||
+5
-3
@@ -41,8 +41,10 @@ OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
|
||||
OTEL_SERVICE_NAME="langfuse"
|
||||
|
||||
# Default role for users who sign up, optional, can be org or org+project
|
||||
# Supports comma-separated IDs for multiple orgs (e.g., "org1,org2,org3")
|
||||
# LANGFUSE_DEFAULT_ORG_ID=
|
||||
# LANGFUSE_DEFAULT_ORG_ROLE=
|
||||
# Supports comma-separated IDs for multiple projects (e.g., "proj1,proj2,proj3")
|
||||
# LANGFUSE_DEFAULT_PROJECT_ID=
|
||||
# LANGFUSE_DEFAULT_PROJECT_ROLE=
|
||||
|
||||
@@ -51,7 +53,7 @@ OTEL_SERVICE_NAME="langfuse"
|
||||
# LANGFUSE_LOG_FORMAT=text
|
||||
|
||||
# Enable experimental features, optional
|
||||
# LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=true
|
||||
# LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=false
|
||||
|
||||
# Auth, optional configuration
|
||||
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
|
||||
@@ -142,7 +144,7 @@ OTEL_SERVICE_NAME="langfuse"
|
||||
# Transactional email, optional
|
||||
# Defines the email address to use as the from address.
|
||||
# EMAIL_FROM_ADDRESS=
|
||||
# Defines the connection url for smtp server.
|
||||
# Defines the connection url for smtp server.
|
||||
# SMTP_CONNECTION_URL=
|
||||
|
||||
# S3 Batch Exports
|
||||
@@ -215,7 +217,7 @@ LANGFUSE_ENABLE_BLOB_STORAGE_FILE_LOG=true
|
||||
# Ingestion configuration
|
||||
# LANGFUSE_INGESTION_QUEUE_DELAY_MS=
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_BATCH_SIZE=
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_WRITE_INTERVAL_MS=
|
||||
# LANGFUSE_INGESTION_CLICKHOUSE_MAX_ATTEMPTS=
|
||||
|
||||
### START Enterprise Edition Configuration
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
**/node_modules
|
||||
build
|
||||
coverage
|
||||
@@ -80,8 +80,9 @@ jobs:
|
||||
if (context.ref === "refs/heads/main") {
|
||||
return `["staging"]`
|
||||
}
|
||||
// TODO revert to: `["prod-eu", "prod-us", "prod-hipaa"]`
|
||||
if (context.ref === "refs/heads/production") {
|
||||
return `["prod-eu", "prod-us", "prod-hipaa"]`
|
||||
return `["prod-eu", "prod-us"]`
|
||||
}
|
||||
}
|
||||
return "[]"
|
||||
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- uses: pnpm/action-setup@v3
|
||||
@@ -269,7 +269,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- uses: pnpm/action-setup@v3
|
||||
@@ -396,7 +396,7 @@ jobs:
|
||||
pnpm install
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- name: Load default env
|
||||
@@ -422,6 +422,18 @@ jobs:
|
||||
pnpm run db:migrate
|
||||
pnpm run db:seed
|
||||
pnpm run --filter=shared ch:up
|
||||
- name: Download and extract ClickHouse client for dev-tables setup
|
||||
if: matrix.deploy-mode == ''
|
||||
run: |
|
||||
CH_VERSION="24.9.3.128"
|
||||
ARCH="amd64"
|
||||
wget "https://packages.clickhouse.com/deb/pool/main/c/clickhouse/clickhouse-common-static_${CH_VERSION}_${ARCH}.deb" -O clickhouse-client.deb
|
||||
dpkg-deb -x clickhouse-client.deb ch_client_dir
|
||||
sudo cp ch_client_dir/usr/bin/clickhouse /usr/local/bin/
|
||||
- name: Setup Dev Tables
|
||||
if: matrix.deploy-mode == ''
|
||||
run: |
|
||||
pnpm --filter=shared ch:dev-tables
|
||||
- name: Build
|
||||
run: pnpm --filter=worker... run build
|
||||
- name: run tests
|
||||
@@ -456,7 +468,7 @@ jobs:
|
||||
pnpm install
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- name: Load default env
|
||||
@@ -548,7 +560,7 @@ jobs:
|
||||
cp .env.dev.example web/.env
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- name: Run + migrate
|
||||
@@ -604,7 +616,7 @@ jobs:
|
||||
pnpm install
|
||||
- name: Install golang-migrate for Clickhouse migrations
|
||||
run: |
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.0/migrate.linux-amd64.tar.gz | tar xvz
|
||||
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.19.1/migrate.linux-amd64.tar.gz | tar xvz
|
||||
sudo mv migrate /usr/bin/migrate
|
||||
which migrate
|
||||
- name: Load default env
|
||||
@@ -653,7 +665,7 @@ jobs:
|
||||
tests-web-sync,
|
||||
tests-worker,
|
||||
test-worker-llm-connections,
|
||||
e2e-tests,
|
||||
# e2e-tests, # Temporarily disabled - flaky timeouts
|
||||
test-docker-build,
|
||||
e2e-server-tests,
|
||||
tests-web-async,
|
||||
@@ -693,7 +705,7 @@ jobs:
|
||||
push-docker-image:
|
||||
needs: all-ci-passed
|
||||
# if something inside all-ci-passed was skipped, but everything that ran passed, we still want to deploy
|
||||
if: always() && needs.all-ci-passed.outputs.success == 'true' && github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
|
||||
if: always() && needs.all-ci-passed.outputs.success == 'true' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
environment: "protected branches"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Snyk Container - Web
|
||||
on:
|
||||
push:
|
||||
branches: ["production", "main"]
|
||||
|
||||
jobs:
|
||||
snyk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run Snyk to check Docker image for vulnerabilities
|
||||
# Snyk can be used to break the build when it detects vulnerabilities.
|
||||
# In this case we want to upload the issues to GitHub Code Scanning
|
||||
continue-on-error: true
|
||||
uses: snyk/actions/docker@master
|
||||
env:
|
||||
# In order to use the Snyk Action you will need to have a Snyk API token.
|
||||
# See https://docs.snyk.io/integrations/ci-cd-integrations/github-actions-integration#getting-your-snyk-token
|
||||
# or you can sign up for free at https://snyk.io/login
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
image: langfuse/langfuse
|
||||
args: --sarif-file-output=snyk.sarif
|
||||
- name: Upload result to GitHub Code Scanning
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: snyk.sarif
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Snyk Container - Worker
|
||||
on:
|
||||
push:
|
||||
branches: ["production", "main"]
|
||||
|
||||
jobs:
|
||||
snyk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Run Snyk to check Docker image for vulnerabilities
|
||||
# Snyk can be used to break the build when it detects vulnerabilities.
|
||||
# In this case we want to upload the issues to GitHub Code Scanning
|
||||
continue-on-error: true
|
||||
uses: snyk/actions/docker@master
|
||||
env:
|
||||
# In order to use the Snyk Action you will need to have a Snyk API token.
|
||||
# See https://docs.snyk.io/integrations/ci-cd-integrations/github-actions-integration#getting-your-snyk-token
|
||||
# or you can sign up for free at https://snyk.io/login
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
image: langfuse/langfuse-worker
|
||||
args: --sarif-file-output=snyk.sarif
|
||||
- name: Upload result to GitHub Code Scanning
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: snyk.sarif
|
||||
@@ -1,28 +0,0 @@
|
||||
name: Snyk Container
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
jobs:
|
||||
snyk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Scan web image with Snyk
|
||||
uses: snyk/actions/docker@master
|
||||
continue-on-error: true # let upload step always run
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
image: langfuse/langfuse # pulled from Docker Hub
|
||||
args: --severity-threshold=high # (any extra CLI flags, but NO --file)
|
||||
|
||||
- name: Scan worker image with Snyk
|
||||
uses: snyk/actions/docker@master
|
||||
continue-on-error: true # let upload step always run
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
with:
|
||||
image: langfuse/langfuse-worker # pulled from Docker Hub
|
||||
args: --severity-threshold=high # (any extra CLI flags, but NO --file)
|
||||
@@ -17,6 +17,7 @@ migrate
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/.next-check/
|
||||
/out/
|
||||
next-env.d.ts
|
||||
|
||||
@@ -35,6 +36,7 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
.eslintcache
|
||||
|
||||
# local env files
|
||||
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
|
||||
@@ -64,6 +66,7 @@ node_modules
|
||||
**/node_modules
|
||||
**/dist
|
||||
**/.next/*
|
||||
**/.next-check/*
|
||||
**/.turbo/*
|
||||
.yarn
|
||||
.turbo
|
||||
|
||||
Vendored
+7
-2
@@ -28,6 +28,11 @@
|
||||
"editor.defaultFormatter": "Prisma.prisma"
|
||||
},
|
||||
"eslint.lintTask.enable": true,
|
||||
"eslint.workingDirectories": ["./web", "./worker"],
|
||||
"eslint.useFlatConfig": false
|
||||
"eslint.workingDirectories": [
|
||||
{ "pattern": "./web" },
|
||||
{ "pattern": "./worker" },
|
||||
{ "pattern": "./packages/shared" },
|
||||
{ "pattern": "./ee" }
|
||||
],
|
||||
"eslint.useFlatConfig": true
|
||||
}
|
||||
|
||||
@@ -72,9 +72,11 @@ pnpm run infra:dev:up # Start Docker services (PostgreSQL, ClickHouse, Redi
|
||||
pnpm run infra:dev:down # Stop Docker services
|
||||
```
|
||||
|
||||
### Building
|
||||
### Building & Type Checking
|
||||
```sh
|
||||
pnpm --filter=PACKAGE_NAME run build # Runs the build command, will show real typescript errors etc.
|
||||
pnpm tc # Fast typecheck across all packages (alias for pnpm typecheck)
|
||||
pnpm build:check # Full Next.js build to alternate dir (can run parallel with dev server)
|
||||
```
|
||||
|
||||
### Testing in Web Package
|
||||
@@ -82,11 +84,11 @@ The web package uses JEST for unit tests.
|
||||
Depending on the file location (sync, async)
|
||||
`web` related tests must go into the `web/src/__tests__/` folder.
|
||||
```sh
|
||||
pnpm test-sync --testPathPattern="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
|
||||
pnpm test-sync --testPathPatterns="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
|
||||
# For tests in the async folder:
|
||||
pnpm test -- --testPathPattern="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
|
||||
pnpm test -- --testPathPatterns="$FILE_LOCATION_PATTERN" --testNamePattern="$TEST_NAME_PATTERN"
|
||||
# For client tests:
|
||||
pnpm test-client --testPathPattern="buildStepData" --testNamePattern="buildStepData"
|
||||
pnpm test-client --testPathPatterns="buildStepData" --testNamePattern="buildStepData"
|
||||
```
|
||||
|
||||
### Testing in the Worker Package
|
||||
@@ -113,7 +115,7 @@ pnpm run nuke # Remove all node_modules, build files, wipe database
|
||||
- **Styling**: Tailwind CSS with CSS variables for theming
|
||||
- **Components**: shadcn/ui (Radix UI primitives)
|
||||
- **State Management**: TanStack Query (React Query) + tRPC
|
||||
- **Charts**: Tremor, Recharts
|
||||
- **Charts**: Recharts
|
||||
|
||||
### Worker Application (`/worker/`)
|
||||
- **Framework**: Express.js
|
||||
@@ -192,6 +194,21 @@ To get a project, use the `get_project` capability with the full project name as
|
||||
|
||||
## TypeScript Best Practices
|
||||
- In TypeScript, if possible, don't use the `any` type
|
||||
- **Use a single params object for functions with multiple arguments** - This makes code more readable at call sites and prevents bugs when arguments of the same type are accidentally swapped:
|
||||
|
||||
```typescript
|
||||
// ❌ Bad - positional arguments are unclear and can be swapped without type errors
|
||||
function sendMessage(userId: string, sessionId: string, projectId: string) {
|
||||
// ...
|
||||
}
|
||||
sendMessage(someString, someOtherString, anotherString); // Which is which?
|
||||
|
||||
// ✅ Good - params object makes intent clear and prevents argument swapping
|
||||
function sendMessage(params: { userId: string; sessionId: string; projectId: string }) {
|
||||
// ...
|
||||
}
|
||||
sendMessage({ userId: someString, sessionId: someOtherString, projectId: anotherString });
|
||||
```
|
||||
|
||||
## General Coding Guidelines
|
||||
- For easier code reviews, prefer not to move functions etc around within a file unless necessary or instructed to do so
|
||||
|
||||
+6
-7
@@ -94,8 +94,6 @@ The diagram below may not show all relationships if the foreign key is not defin
|
||||
|
||||
Full database schema: [packages/shared/prisma/schema.prisma](packages/shared/prisma/schema.prisma)
|
||||
|
||||
<img src="./packages/shared/prisma/database.svg">
|
||||
|
||||
## Repository Structure
|
||||
|
||||
We built a monorepo using [pnpm](https://pnpm.io/motivation) and [turbo](https://turbo.build/repo/docs) to manage the dependencies and build process. The monorepo contains the following packages:
|
||||
@@ -122,8 +120,8 @@ Requirements
|
||||
**Steps**
|
||||
|
||||
1. Install development dependencies:
|
||||
- [golang-migrate](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate#migrate-cli) as CLI
|
||||
- [clickhouse binary](https://clickhouse.com/docs/install) on macOS with brew: `brew install --cask clickhouse`
|
||||
- [golang-migrate](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate#migrate-cli) as CLI
|
||||
- [clickhouse binary](https://clickhouse.com/docs/install) on macOS with brew: `brew install --cask clickhouse`
|
||||
|
||||
2. Fork the repository and clone it locally
|
||||
|
||||
@@ -182,6 +180,8 @@ pnpm run db:seed:examples
|
||||
pnpm install
|
||||
pnpm run dev
|
||||
pnpm --filter=web run dev # execute command only in one package
|
||||
pnpm tc # fast typecheck all packages
|
||||
pnpm build:check # Full Next.js build to alternate dir (can run parallel with dev server)
|
||||
```
|
||||
|
||||
In the root `package.json`, you can find scripts which are executed with turbo e.g. `turbo run dev`. These scripts are executed with the help of Turbo. Turbo executes the commands in all packages taking care of the correct order of execution. Task definitions can be found in the `turbo.config.js` file.
|
||||
@@ -254,9 +254,9 @@ To run a specific test, for example the test: `"should handle special characters
|
||||
|
||||
```sh
|
||||
cd web # or with --filter=web
|
||||
pnpm test-sync --testPathPattern="prompts\.v2\.servertest" --testNamePattern="should handle special characters in prompt names"
|
||||
pnpm test-sync --testPathPatterns="prompts\.v2\.servertest" --testNamePattern="should handle special characters in prompt names"
|
||||
# for async folder tests:
|
||||
pnpm test -- --testPathPattern="observations-api" --testNamePattern="should fetch all observations"
|
||||
pnpm test -- --testPathPatterns="observations-api" --testNamePattern="should fetch all observations"
|
||||
```
|
||||
|
||||
To run all tests:
|
||||
@@ -424,4 +424,3 @@ npx fern-api generate --api organizations # for the organizations API
|
||||
Langfuse is MIT licensed, except for `ee/` folder. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
When contributing to the Langfuse codebase, you need to agree to the [Contributor License Agreement](https://cla-assistant.io/langfuse/langfuse). You only need to do this once and the CLA bot will remind you if you haven't signed it yet.
|
||||
|
||||
|
||||
+1
-1
@@ -295,6 +295,7 @@ _[Langfuse 中的公共示例追踪](https://cloud.langfuse.com/project/cloramnk
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37368 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> [danny-avila](https://github.com/danny-avila) / [LibreChat](https://github.com/danny-avila/LibreChat) | 33142 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 32486 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
|
||||
@@ -332,7 +333,6 @@ _[Langfuse 中的公共示例追踪](https://cloud.langfuse.com/project/cloramnk
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 128 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 126 |
|
||||
|
||||
## 🔒 安全与隐私
|
||||
|
||||
+1
-1
@@ -301,6 +301,7 @@ Langfuseを利用している主要なオープンソースPythonプロジェク
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37368 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> [danny-avila](https://github.com/danny-avila) / [LibreChat](https://github.com/danny-avila/LibreChat) | 33142 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 32486 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
|
||||
@@ -338,7 +339,6 @@ Langfuseを利用している主要なオープンソースPythonプロジェク
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 128 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 126 |
|
||||
|
||||
## 🔒 セキュリティとプライバシー
|
||||
|
||||
+1
-1
@@ -283,6 +283,7 @@ _[Langfuse의 공개 예제 trace](https://cloud.langfuse.com/project/cloramnkj0
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/131470832?s=40&v=4" width="20" height="20" alt=""> [lobehub](https://github.com/lobehub) / [lobe-chat](https://github.com/lobehub/lobe-chat) | 49003 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/85702467?s=40&v=4" width="20" height="20" alt=""> [langflow-ai](https://github.com/langflow-ai) / [langflow](https://github.com/langflow-ai/langflow) | 39093 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/130722866?s=40&v=4" width="20" height="20" alt=""> [run-llama](https://github.com/run-llama) / [llama_index](https://github.com/run-llama/llama_index) | 37368 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> [danny-avila](https://github.com/danny-avila) / [LibreChat](https://github.com/danny-avila/LibreChat) | 33142 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 32486 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/128289781?s=40&v=4" width="20" height="20" alt=""> [FlowiseAI](https://github.com/FlowiseAI) / [Flowise](https://github.com/FlowiseAI/Flowise) | 32448 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 26931 |
|
||||
@@ -320,7 +321,6 @@ _[Langfuse의 공개 예제 trace](https://cloud.langfuse.com/project/cloramnkj0
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/171735272?s=40&v=4" width="20" height="20" alt=""> [8090-inc](https://github.com/8090-inc) / [xrx-sample-apps](https://github.com/8090-inc/xrx-sample-apps) | 138 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/104478511?s=40&v=4" width="20" height="20" alt=""> [babelcloud](https://github.com/babelcloud) / [LLM-RGB](https://github.com/babelcloud/LLM-RGB) | 135 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 129 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 128 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/51827949?s=40&v=4" width="20" height="20" alt=""> [deepset-ai](https://github.com/deepset-ai) / [haystack-core-integrations](https://github.com/deepset-ai/haystack-core-integrations) | 126 |
|
||||
|
||||
## 🔒 보안 & 개인정보 보호
|
||||
|
||||
@@ -71,13 +71,17 @@
|
||||
<a href="./README.kr.md"><img alt="README in Korean" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/ClickHouse/ClickHouse"><strong>Proudly made with ClickHouse open source database</strong></a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
Langfuse is an **open source LLM engineering** platform. It helps teams collaboratively
|
||||
**develop, monitor, evaluate,** and **debug** AI applications. Langfuse can be **self-hosted in minutes** and is **battle-tested**.
|
||||
|
||||
[](https://langfuse.com/watch-demo)
|
||||
|
||||
|
||||
|
||||
## ✨ Core Features
|
||||
|
||||
<img width="4856" height="1944" alt="Langfuse Overview" src="https://github.com/user-attachments/assets/5dac68ef-d546-49fb-b06f-cfafc19282e3" />
|
||||
@@ -144,7 +148,7 @@ See [self-hosting documentation](https://langfuse.com/self-hosting) to learn mor
|
||||
| [Haystack](https://langfuse.com/docs/integrations/haystack) | Python | Automated instrumentation via Haystack content tracing system. |
|
||||
| [LiteLLM](https://langfuse.com/docs/integrations/litellm) | Python, JS/TS (proxy only) | Use any LLM as a drop in replacement for GPT. Use Azure, OpenAI, Cohere, Anthropic, Ollama, VLLM, Sagemaker, HuggingFace, Replicate (100+ LLMs). |
|
||||
| [Vercel AI SDK](https://langfuse.com/docs/integrations/vercel-ai-sdk) | JS/TS | TypeScript toolkit designed to help developers build AI-powered applications with React, Next.js, Vue, Svelte, Node.js. |
|
||||
| [Mastra](https://langfuse.com/docs/integrations/mastra) | JS/TS | Open source framework for building AI agents and multi-agent systems. |
|
||||
| [Mastra](https://langfuse.com/docs/integrations/mastra) | JS/TS | Open source framework for building AI agents and multi-agent systems. |
|
||||
| [API](https://langfuse.com/docs/api) | | Directly call the public API. OpenAPI spec available. |
|
||||
|
||||
### Packages integrated with Langfuse:
|
||||
@@ -259,6 +263,10 @@ Your contributions are welcome!
|
||||
|
||||
This repository is MIT licensed, except for the `ee` folders. See [LICENSE](LICENSE) and [docs](https://langfuse.com/docs/open-source) for more details.
|
||||
|
||||
## Dependencies
|
||||
|
||||
We deploy this code base in Docker containers based on the Linux Alpine Image ([source](https://github.com/nodejs/docker-node)). You may find the Dockerfiles in [web/Dockerfile](web/Dockerfile) and [worker/Dockerfile](worker/Dockerfile).
|
||||
|
||||
## ⭐️ Star History
|
||||
|
||||
<a href="https://star-history.com/#langfuse/langfuse&Date">
|
||||
@@ -287,6 +295,7 @@ Top open-source Python projects that use Langfuse, ranked by stars ([Source](htt
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/6154722?s=40&v=4" width="20" height="20" alt=""> [microsoft](https://github.com/microsoft) / [ai-agents-for-beginners](https://github.com/microsoft/ai-agents-for-beginners) | 38012 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/139558948?s=40&v=4" width="20" height="20" alt=""> [chatchat-space](https://github.com/chatchat-space) / [Langchain-Chatchat](https://github.com/chatchat-space/Langchain-Chatchat) | 36071 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/31035808?s=40&v=4" width="20" height="20" alt=""> [mindsdb](https://github.com/mindsdb) / [mindsdb](https://github.com/mindsdb/mindsdb) | 35669 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> [danny-avila](https://github.com/danny-avila) / [LibreChat](https://github.com/danny-avila/LibreChat) | 33142 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/121462774?s=40&v=4" width="20" height="20" alt=""> [BerriAI](https://github.com/BerriAI) / [litellm](https://github.com/BerriAI/litellm) | 28726 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/157326433?s=40&v=4" width="20" height="20" alt=""> [onlook-dev](https://github.com/onlook-dev) / [onlook](https://github.com/onlook-dev/onlook) | 22447 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/487568?s=40&v=4" width="20" height="20" alt=""> [NixOS](https://github.com/NixOS) / [nixpkgs](https://github.com/NixOS/nixpkgs) | 21748 |
|
||||
@@ -342,7 +351,6 @@ Top open-source Python projects that use Langfuse, ranked by stars ([Source](htt
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/16997807?s=40&v=4" width="20" height="20" alt=""> [FranciscoMoretti](https://github.com/FranciscoMoretti) / [sparka](https://github.com/FranciscoMoretti/sparka) | 380 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/66303003?s=40&v=4" width="20" height="20" alt=""> [RobotecAI](https://github.com/RobotecAI) / [rai](https://github.com/RobotecAI/rai) | 373 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/137044109?s=40&v=4" width="20" height="20" alt=""> [ElectricCodeGuy](https://github.com/ElectricCodeGuy) / [SupabaseAuthWithSSR](https://github.com/ElectricCodeGuy/SupabaseAuthWithSSR) | 370 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/169401942?s=40&v=4" width="20" height="20" alt=""> [LibreChat-AI](https://github.com/LibreChat-AI) / [librechat.ai](https://github.com/LibreChat-AI/librechat.ai) | 339 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/15125613?s=40&v=4" width="20" height="20" alt=""> [souzatharsis](https://github.com/souzatharsis) / [tamingLLMs](https://github.com/souzatharsis/tamingLLMs) | 323 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/8931462?s=40&v=4" width="20" height="20" alt=""> [aws-samples](https://github.com/aws-samples) / [aws-ai-ml-workshop-kr](https://github.com/aws-samples/aws-ai-ml-workshop-kr) | 295 |
|
||||
| <img class="avatar mr-2" src="https://avatars.githubusercontent.com/u/25676773?s=40&v=4" width="20" height="20" alt=""> [weizxfree](https://github.com/weizxfree) / [KnowFlow](https://github.com/weizxfree/KnowFlow) | 285 |
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
- Migrations in `packages/shared/clickhouse/migrations/clustered` should match their counterparts in `packages/shared/clickhouse/migrations/unclustered` aside from the restrictions listed above.
|
||||
- When adding new indexes on ClickHouse, ensure that there is a corresponding `MATERIALIZE INDEX` statement in the same migration. The materialization can use `SETTINGS mutations_sync = 2` if they operate on smaller tables, but may timeout otherwise.
|
||||
- All ClickHouse queries on project-scoped tables (traces, observations, scores, events, sessions, etc.) must include `WHERE project_id = {projectId: String}` filter to ensure proper tenant isolation and that queries only access data from the intended project.
|
||||
- For operations on the `events` table, you must never use the `FINAL` keyword as it kills performance. `events` is built so that `FINAL` is never required.
|
||||
|
||||
### Postgres
|
||||
|
||||
@@ -45,3 +46,10 @@
|
||||
## Seeder
|
||||
|
||||
- make sure that for new features with data model changes, the database seeder is adjusted.
|
||||
|
||||
## API Documentation
|
||||
|
||||
- Whenever a file in `web/src/features/public-api/types` changes, the `fern/apis` definition probably needs to be adjusted, too.
|
||||
- `nullish` types should map to `optional<nullable<T>>` in fern.
|
||||
- `nullable` types should map to `nullable<T>` in fern.
|
||||
- `optional` types should map to `optional<T>` in fern.
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ services:
|
||||
SALT: ${SALT:-mysalt} # CHANGEME
|
||||
ENCRYPTION_KEY: ${ENCRYPTION_KEY:-0000000000000000000000000000000000000000000000000000000000000000} # CHANGEME: generate via `openssl rand -hex 32`
|
||||
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true}
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-true}
|
||||
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES:-false}
|
||||
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL:-clickhouse://clickhouse:9000}
|
||||
CLICKHOUSE_URL: ${CLICKHOUSE_URL:-http://clickhouse:8123}
|
||||
CLICKHOUSE_USER: ${CLICKHOUSE_USER:-clickhouse}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ["@repo/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import baseConfig from "@repo/eslint-config";
|
||||
|
||||
export default [...baseConfig];
|
||||
+11
-8
@@ -19,27 +19,30 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:check": "tsc",
|
||||
"typecheck": "dotenv -e ../../.env -- tsgo --noEmit --skipLibCheck --incremental --tsBuildInfoFile .tsbuildinfo",
|
||||
"dev": "tsc --watch",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
|
||||
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
|
||||
"lint": "eslint . --cache --cache-location dist/.eslintcache --max-warnings 0",
|
||||
"lint:fix": "eslint . --cache --cache-location dist/.eslintcache --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langfuse/shared": "workspace:*",
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"next": "15.5.9",
|
||||
"next-auth": "^4.24.12",
|
||||
"next": "15.5.10",
|
||||
"next-auth": "^4.24.13",
|
||||
"zod": "^3.25.62"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/node": "^24.3.0",
|
||||
"@typescript-eslint/parser": "^8.48.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"@typescript-eslint/parser": "^8.50.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260122.3",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-watch": "^6.2.0",
|
||||
|
||||
@@ -18,7 +18,9 @@ types:
|
||||
sessionId: optional<string>
|
||||
observationId: optional<string>
|
||||
datasetRunId: optional<string>
|
||||
name: string
|
||||
name:
|
||||
type: string
|
||||
docs: The name of the score. Always overrides "output" for correction scores.
|
||||
value:
|
||||
type: CreateScoreValue
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false)
|
||||
@@ -81,10 +83,11 @@ types:
|
||||
- NUMERIC
|
||||
- CATEGORICAL
|
||||
- BOOLEAN
|
||||
- CORRECTION
|
||||
|
||||
CreateScoreValue:
|
||||
discriminated: false
|
||||
union:
|
||||
- double
|
||||
- string
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores. For corrections pass value as string and set data type to CORRECTION
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
types:
|
||||
# Objects
|
||||
# Source: web/src/features/public-api/types/traces.ts - APITrace, APIExtendedTrace, GetTraceV1Response
|
||||
Trace:
|
||||
properties:
|
||||
id:
|
||||
@@ -9,7 +10,7 @@ types:
|
||||
type: datetime
|
||||
docs: The timestamp when the trace was created
|
||||
name:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The name of the trace
|
||||
input:
|
||||
type: optional<unknown>
|
||||
@@ -18,28 +19,28 @@ types:
|
||||
type: optional<unknown>
|
||||
docs: The output data of the trace. Can be any JSON.
|
||||
sessionId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The session identifier associated with the trace
|
||||
release:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The release version of the application when the trace was created
|
||||
version:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The version of the trace
|
||||
userId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The user identifier associated with the trace
|
||||
metadata:
|
||||
type: optional<unknown>
|
||||
docs: The metadata associated with the trace. Can be any JSON.
|
||||
tags:
|
||||
type: optional<list<string>>
|
||||
docs: The tags associated with the trace. Can be an array of strings or null.
|
||||
type: list<string>
|
||||
docs: The tags associated with the trace.
|
||||
public:
|
||||
type: optional<boolean>
|
||||
type: boolean
|
||||
docs: Public traces are accessible via url without login
|
||||
environment:
|
||||
type: optional<string>
|
||||
type: string
|
||||
docs: The environment from which this trace originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
|
||||
TraceWithDetails: # GET /traces
|
||||
extends: Trace
|
||||
@@ -48,16 +49,16 @@ types:
|
||||
type: string
|
||||
docs: Path of trace in Langfuse UI
|
||||
latency:
|
||||
type: double
|
||||
type: optional<nullable<double>>
|
||||
docs: Latency of trace in seconds
|
||||
totalCost:
|
||||
type: double
|
||||
type: optional<nullable<double>>
|
||||
docs: Cost of trace in USD
|
||||
observations:
|
||||
type: list<string>
|
||||
type: optional<nullable<list<string>>>
|
||||
docs: List of observation ids
|
||||
scores:
|
||||
type: list<string>
|
||||
type: optional<nullable<list<string>>>
|
||||
docs: List of score ids
|
||||
TraceWithFullDetails: # GET traces/[traceID]
|
||||
extends: Trace
|
||||
@@ -66,10 +67,10 @@ types:
|
||||
type: string
|
||||
docs: Path of trace in Langfuse UI
|
||||
latency:
|
||||
type: double
|
||||
type: optional<nullable<double>>
|
||||
docs: Latency of trace in seconds
|
||||
totalCost:
|
||||
type: double
|
||||
type: optional<nullable<double>>
|
||||
docs: Cost of trace in USD
|
||||
observations:
|
||||
type: list<ObservationsView>
|
||||
@@ -77,119 +78,121 @@ types:
|
||||
scores:
|
||||
type: list<ScoreV1>
|
||||
docs: List of scores
|
||||
# Source: web/src/features/public-api/types/sessions.ts - APISession
|
||||
Session:
|
||||
properties:
|
||||
id: string
|
||||
createdAt: datetime
|
||||
projectId: string
|
||||
environment:
|
||||
type: optional<string>
|
||||
type: string
|
||||
docs: The environment from which this session originated.
|
||||
SessionWithTraces:
|
||||
extends: Session
|
||||
properties:
|
||||
traces: list<Trace>
|
||||
# Source: web/src/features/public-api/types/observations.ts - APIObservation
|
||||
Observation:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
docs: The unique identifier of the observation
|
||||
traceId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The trace ID associated with the observation
|
||||
type:
|
||||
type: string
|
||||
docs: The type of the observation
|
||||
name:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The name of the observation
|
||||
startTime:
|
||||
type: datetime
|
||||
docs: The start time of the observation
|
||||
endTime:
|
||||
type: optional<datetime>
|
||||
type: nullable<datetime>
|
||||
docs: The end time of the observation.
|
||||
completionStartTime:
|
||||
type: optional<datetime>
|
||||
type: nullable<datetime>
|
||||
docs: The completion start time of the observation
|
||||
model:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The model used for the observation
|
||||
modelParameters:
|
||||
type: optional<map<string, MapValue>>
|
||||
type: unknown
|
||||
docs: The parameters of the model used for the observation
|
||||
input:
|
||||
type: optional<unknown>
|
||||
type: unknown
|
||||
docs: The input data of the observation
|
||||
version:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The version of the observation
|
||||
metadata:
|
||||
type: optional<unknown>
|
||||
type: unknown
|
||||
docs: Additional metadata of the observation
|
||||
output:
|
||||
type: optional<unknown>
|
||||
type: unknown
|
||||
docs: The output data of the observation
|
||||
usage:
|
||||
type: optional<Usage>
|
||||
type: Usage
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) The usage data of the observation
|
||||
level:
|
||||
type: ObservationLevel
|
||||
docs: The level of the observation
|
||||
statusMessage:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The status message of the observation
|
||||
parentObservationId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The parent observation ID
|
||||
promptId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The prompt ID associated with the observation
|
||||
usageDetails:
|
||||
type: optional<map<string, integer>>
|
||||
type: map<string, integer>
|
||||
docs: The usage details of the observation. Key is the name of the usage metric, value is the number of units consumed. The total key is the sum of all (non-total) usage metrics or the total value ingested.
|
||||
costDetails:
|
||||
type: optional<map<string, double>>
|
||||
type: map<string, double>
|
||||
docs: The cost details of the observation. Key is the name of the cost metric, value is the cost in USD. The total key is the sum of all (non-total) cost metrics or the total value ingested.
|
||||
environment:
|
||||
type: optional<string>
|
||||
type: string
|
||||
docs: The environment from which this observation originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
|
||||
|
||||
ObservationsView:
|
||||
extends: Observation
|
||||
properties:
|
||||
promptName:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The name of the prompt associated with the observation
|
||||
promptVersion:
|
||||
type: optional<integer>
|
||||
type: nullable<integer>
|
||||
docs: The version of the prompt associated with the observation
|
||||
modelId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The unique identifier of the model
|
||||
inputPrice:
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
docs: The price of the input in USD
|
||||
outputPrice:
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
docs: The price of the output in USD.
|
||||
totalPrice:
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
docs: The total price in USD.
|
||||
calculatedInputCost:
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated cost of the input in USD
|
||||
calculatedOutputCost:
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated cost of the output in USD
|
||||
calculatedTotalCost:
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
docs: (Deprecated. Use usageDetails and costDetails instead.) The calculated total cost in USD
|
||||
latency:
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
docs: The latency in seconds.
|
||||
timeToFirstToken:
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
docs: The time to the first token in seconds
|
||||
|
||||
Usage:
|
||||
@@ -197,14 +200,16 @@ types:
|
||||
properties:
|
||||
input:
|
||||
docs: Number of input units (e.g. tokens)
|
||||
type: optional<integer>
|
||||
type: integer
|
||||
output:
|
||||
docs: Number of output units (e.g. tokens)
|
||||
type: optional<integer>
|
||||
type: integer
|
||||
total:
|
||||
docs: Defaults to input+output if not set
|
||||
type: optional<integer>
|
||||
unit: optional<ModelUsageUnit>
|
||||
type: integer
|
||||
unit:
|
||||
docs: Unit of measurement
|
||||
type: nullable<string>
|
||||
inputCost:
|
||||
docs: USD input cost
|
||||
type: optional<double>
|
||||
@@ -214,6 +219,7 @@ types:
|
||||
totalCost:
|
||||
docs: USD total cost, defaults to input+output
|
||||
type: optional<double>
|
||||
# Source: web/src/features/public-api/types/score-configs.ts - APIScoreConfig
|
||||
ScoreConfig:
|
||||
docs: Configuration for a score
|
||||
properties:
|
||||
@@ -222,45 +228,56 @@ types:
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
projectId: string
|
||||
dataType: ScoreDataType
|
||||
dataType: ScoreConfigDataType
|
||||
isArchived:
|
||||
type: boolean
|
||||
docs: Whether the score config is archived. Defaults to false
|
||||
minValue:
|
||||
type: optional<double>
|
||||
type: optional<nullable<double>>
|
||||
docs: Sets minimum value for numerical scores. If not set, the minimum value defaults to -∞
|
||||
maxValue:
|
||||
type: optional<double>
|
||||
type: optional<nullable<double>>
|
||||
docs: Sets maximum value for numerical scores. If not set, the maximum value defaults to +∞
|
||||
categories:
|
||||
type: optional<list<ConfigCategory>>
|
||||
docs: Configures custom categories for categorical scores
|
||||
description: optional<string>
|
||||
description:
|
||||
type: optional<nullable<string>>
|
||||
docs: Description of the score config
|
||||
ConfigCategory:
|
||||
properties:
|
||||
value: double
|
||||
label: string
|
||||
# Source: packages/shared/src/features/scores/interfaces/api/v1/schemas.ts - APIScoreSchemaV1
|
||||
BaseScoreV1:
|
||||
properties:
|
||||
id: string
|
||||
traceId: string
|
||||
name: string
|
||||
source: ScoreSource
|
||||
observationId: optional<string>
|
||||
observationId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The observation ID associated with the score
|
||||
timestamp: datetime
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
authorUserId: optional<string>
|
||||
comment: optional<string>
|
||||
metadata: optional<unknown>
|
||||
authorUserId:
|
||||
type: nullable<string>
|
||||
docs: The user ID of the author
|
||||
comment:
|
||||
type: nullable<string>
|
||||
docs: Comment on the score
|
||||
metadata:
|
||||
type: unknown
|
||||
docs: Metadata associated with the score
|
||||
configId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range
|
||||
queueId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue.
|
||||
environment:
|
||||
type: optional<string>
|
||||
type: string
|
||||
docs: The environment from which this score originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
|
||||
NumericScoreV1:
|
||||
extends: BaseScoreV1
|
||||
@@ -299,29 +316,44 @@ types:
|
||||
type: BooleanScoreV1
|
||||
docs: "Score with BOOLEAN data type"
|
||||
|
||||
# Source: packages/shared/src/features/scores/interfaces/api/v2/schemas.ts - APIScoreSchemaV2
|
||||
BaseScore:
|
||||
properties:
|
||||
id: string
|
||||
traceId: optional<string>
|
||||
sessionId: optional<string>
|
||||
observationId: optional<string>
|
||||
datasetRunId: optional<string>
|
||||
traceId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The trace ID associated with the score
|
||||
sessionId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The session ID associated with the score
|
||||
observationId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The observation ID associated with the score
|
||||
datasetRunId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The dataset run ID associated with the score
|
||||
name: string
|
||||
source: ScoreSource
|
||||
timestamp: datetime
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
authorUserId: optional<string>
|
||||
comment: optional<string>
|
||||
metadata: optional<unknown>
|
||||
authorUserId:
|
||||
type: nullable<string>
|
||||
docs: The user ID of the author
|
||||
comment:
|
||||
type: nullable<string>
|
||||
docs: Comment on the score
|
||||
metadata:
|
||||
type: unknown
|
||||
docs: Metadata associated with the score
|
||||
configId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range
|
||||
queueId:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue.
|
||||
environment:
|
||||
type: optional<string>
|
||||
type: string
|
||||
docs: The environment from which this score originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'.
|
||||
NumericScore:
|
||||
extends: BaseScore
|
||||
@@ -347,6 +379,15 @@ types:
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the score value. If no config is linked, can be any string. Otherwise, must map to a config category
|
||||
CorrectionScore:
|
||||
extends: BaseScore
|
||||
properties:
|
||||
value:
|
||||
type: double
|
||||
docs: The numeric value of the score. Always 0 for correction scores.
|
||||
stringValue:
|
||||
type: string
|
||||
docs: The string representation of the correction content
|
||||
Score:
|
||||
discriminant: "dataType"
|
||||
union:
|
||||
@@ -359,6 +400,9 @@ types:
|
||||
BOOLEAN:
|
||||
type: BooleanScore
|
||||
docs: "Score with BOOLEAN data type"
|
||||
CORRECTION:
|
||||
type: CorrectionScore
|
||||
docs: "Score with CORRECTION data type"
|
||||
|
||||
CreateScoreValue:
|
||||
discriminated: false
|
||||
@@ -367,6 +411,7 @@ types:
|
||||
- string
|
||||
docs: The value of the score. Must be passed as string for categorical scores, and numeric for boolean and numeric scores
|
||||
|
||||
# Source: web/src/features/public-api/types/comments.ts - APIComment
|
||||
Comment:
|
||||
properties:
|
||||
id: string
|
||||
@@ -376,36 +421,55 @@ types:
|
||||
objectType: CommentObjectType
|
||||
objectId: string
|
||||
content: string
|
||||
authorUserId: optional<string>
|
||||
authorUserId:
|
||||
type: optional<nullable<string>>
|
||||
docs: The user ID of the comment author
|
||||
|
||||
# Source: web/src/features/public-api/types/datasets.ts - APIDataset
|
||||
Dataset:
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
description: optional<string>
|
||||
metadata: optional<unknown>
|
||||
description:
|
||||
type: nullable<string>
|
||||
docs: Description of the dataset
|
||||
metadata:
|
||||
type: unknown
|
||||
docs: Metadata associated with the dataset
|
||||
inputSchema:
|
||||
type: optional<unknown>
|
||||
type: nullable<unknown>
|
||||
docs: JSON Schema for validating dataset item inputs
|
||||
expectedOutputSchema:
|
||||
type: optional<unknown>
|
||||
type: nullable<unknown>
|
||||
docs: JSON Schema for validating dataset item expected outputs
|
||||
projectId: string
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
# Source: web/src/features/public-api/types/datasets.ts - APIDatasetItem
|
||||
DatasetItem:
|
||||
properties:
|
||||
id: string
|
||||
status: DatasetStatus
|
||||
input: optional<unknown>
|
||||
expectedOutput: optional<unknown>
|
||||
metadata: optional<unknown>
|
||||
sourceTraceId: optional<string>
|
||||
sourceObservationId: optional<string>
|
||||
input:
|
||||
type: unknown
|
||||
docs: Input data for the dataset item
|
||||
expectedOutput:
|
||||
type: unknown
|
||||
docs: Expected output for the dataset item
|
||||
metadata:
|
||||
type: unknown
|
||||
docs: Metadata associated with the dataset item
|
||||
sourceTraceId:
|
||||
type: nullable<string>
|
||||
docs: The trace ID that sourced this dataset item
|
||||
sourceObservationId:
|
||||
type: nullable<string>
|
||||
docs: The observation ID that sourced this dataset item
|
||||
datasetId: string
|
||||
datasetName: string
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
# Source: web/src/features/public-api/types/datasets.ts - APIDatasetRunItem
|
||||
DatasetRunItem:
|
||||
properties:
|
||||
id: string
|
||||
@@ -413,9 +477,12 @@ types:
|
||||
datasetRunName: string
|
||||
datasetItemId: string
|
||||
traceId: string
|
||||
observationId: optional<string>
|
||||
observationId:
|
||||
type: nullable<string>
|
||||
docs: The observation ID associated with this run item
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
# Source: web/src/features/public-api/types/datasets.ts - APIDatasetRun
|
||||
DatasetRun:
|
||||
properties:
|
||||
id:
|
||||
@@ -425,10 +492,10 @@ types:
|
||||
type: string
|
||||
docs: Name of the dataset run
|
||||
description:
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
docs: Description of the run
|
||||
metadata:
|
||||
type: optional<unknown>
|
||||
type: unknown
|
||||
docs: Metadata of the dataset run
|
||||
datasetId:
|
||||
type: string
|
||||
@@ -446,6 +513,7 @@ types:
|
||||
extends: DatasetRun
|
||||
properties:
|
||||
datasetRunItems: list<DatasetRunItem>
|
||||
# Source: web/src/features/public-api/types/models.ts - APIModelDefinition
|
||||
Model:
|
||||
docs: |
|
||||
Model definition used for transforming usage into USD cost and/or tokenization.
|
||||
@@ -467,25 +535,25 @@ types:
|
||||
type: string
|
||||
startDate:
|
||||
docs: Apply only to generations which are newer than this ISO date.
|
||||
type: optional<datetime>
|
||||
type: nullable<datetime>
|
||||
unit:
|
||||
docs: Unit used by this model.
|
||||
type: optional<ModelUsageUnit>
|
||||
type: optional<nullable<ModelUsageUnit>>
|
||||
inputPrice:
|
||||
docs: Deprecated. See 'prices' instead. Price (USD) per input unit
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
outputPrice:
|
||||
docs: Deprecated. See 'prices' instead. Price (USD) per output unit
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
totalPrice:
|
||||
docs: Deprecated. See 'prices' instead. Price (USD) per total unit. Cannot be set if input or output price is set.
|
||||
type: optional<double>
|
||||
type: nullable<double>
|
||||
tokenizerId:
|
||||
docs: Optional. Tokenizer to be applied to observations which match to this model. See docs for more details.
|
||||
type: optional<string>
|
||||
type: nullable<string>
|
||||
tokenizerConfig:
|
||||
docs: Optional. Configuration for the selected tokenizer. Needs to be JSON. See docs for more details.
|
||||
type: optional<unknown>
|
||||
type: unknown
|
||||
isLangfuseManaged:
|
||||
type: boolean
|
||||
createdAt:
|
||||
@@ -730,11 +798,18 @@ types:
|
||||
- ANNOTATION
|
||||
- API
|
||||
- EVAL
|
||||
ScoreConfigDataType:
|
||||
enum:
|
||||
- NUMERIC
|
||||
- BOOLEAN
|
||||
- CATEGORICAL
|
||||
|
||||
ScoreDataType:
|
||||
enum:
|
||||
- NUMERIC
|
||||
- BOOLEAN
|
||||
- CATEGORICAL
|
||||
- CORRECTION
|
||||
|
||||
errors:
|
||||
Error:
|
||||
|
||||
@@ -21,7 +21,9 @@ service:
|
||||
type: string
|
||||
response: commons.DatasetItem
|
||||
list:
|
||||
docs: Get dataset items
|
||||
docs: |
|
||||
Get dataset items. Optionally specify a version to get the items as they existed at that point in time.
|
||||
Note: If version parameter is provided, datasetName must also be provided.
|
||||
method: GET
|
||||
path: /dataset-items
|
||||
request:
|
||||
@@ -30,6 +32,12 @@ service:
|
||||
datasetName: optional<string>
|
||||
sourceTraceId: optional<string>
|
||||
sourceObservationId: optional<string>
|
||||
version:
|
||||
type: optional<datetime>
|
||||
docs: |
|
||||
ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z").
|
||||
If provided, returns state of dataset at this timestamp.
|
||||
If not provided, returns the latest version. Requires datasetName to be specified.
|
||||
page:
|
||||
type: optional<integer>
|
||||
docs: page number, starts at 1
|
||||
|
||||
@@ -44,6 +44,13 @@ types:
|
||||
traceId:
|
||||
type: optional<string>
|
||||
docs: traceId should always be provided. For compatibility with older SDK versions it can also be inferred from the provided observationId.
|
||||
datasetVersion:
|
||||
type: optional<datetime>
|
||||
docs: |
|
||||
ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z").
|
||||
Specifies the dataset version to use for this experiment run.
|
||||
If provided, the experiment will use dataset items as they existed at or before this timestamp.
|
||||
If not provided, uses the latest version of dataset items.
|
||||
PaginatedDatasetRunItems:
|
||||
properties:
|
||||
data: list<commons.DatasetRunItem>
|
||||
|
||||
@@ -278,7 +278,9 @@ types:
|
||||
sessionId: optional<string>
|
||||
observationId: optional<string>
|
||||
datasetRunId: optional<string>
|
||||
name: string
|
||||
name:
|
||||
type: string
|
||||
docs: The name of the score. Always overrides "output" for correction scores.
|
||||
environment: optional<string>
|
||||
queueId:
|
||||
type: optional<string>
|
||||
|
||||
@@ -9,7 +9,9 @@ service:
|
||||
metrics:
|
||||
docs: |
|
||||
Get metrics from the Langfuse project using a query object.
|
||||
|
||||
|
||||
Consider using the [v2 metrics endpoint](/api-reference#tag/metricsv2/GET/api/public/v2/metrics) for better performance.
|
||||
|
||||
For more details, see the [Metrics API documentation](https://langfuse.com/docs/metrics/features/metrics-api).
|
||||
method: GET
|
||||
path: /metrics
|
||||
|
||||
@@ -20,7 +20,7 @@ service:
|
||||
- `basic` - name, level, statusMessage, version, environment, bookmarked, public, userId, sessionId
|
||||
- `time` - completionStartTime, createdAt, updatedAt
|
||||
- `io` - input, output
|
||||
- `metadata` - metadata
|
||||
- `metadata` - metadata (truncated to 200 chars by default, use `expandMetadata` to get full values)
|
||||
- `model` - providedModelName, internalModelId, modelParameters
|
||||
- `usage` - usageDetails, costDetails, totalCost
|
||||
- `prompt` - promptId, promptName, promptVersion
|
||||
@@ -43,6 +43,13 @@ service:
|
||||
Available groups: core, basic, time, io, metadata, model, usage, prompt, metrics.
|
||||
If not specified, `core` and `basic` field groups are returned.
|
||||
Example: "basic,usage,model"
|
||||
expandMetadata:
|
||||
type: optional<string>
|
||||
docs: |
|
||||
Comma-separated list of metadata keys to return non-truncated.
|
||||
By default, metadata values over 200 characters are truncated.
|
||||
Use this parameter to retrieve full values for specific keys.
|
||||
Example: "key1,key2"
|
||||
limit:
|
||||
type: optional<integer>
|
||||
docs: Number of items to return per page. Maximum 1000, default 50.
|
||||
|
||||
@@ -16,7 +16,10 @@ service:
|
||||
docs: The unique langfuse identifier of an observation, can be an event, span or generation
|
||||
response: commons.ObservationsView
|
||||
getMany:
|
||||
docs: Get a list of observations
|
||||
docs: |
|
||||
Get a list of observations.
|
||||
|
||||
Consider using the [v2 observations endpoint](/api-reference#tag/observationsv2/GET/api/public/v2/observations) for cursor-based pagination and field selection.
|
||||
method: GET
|
||||
path: /observations
|
||||
request:
|
||||
|
||||
@@ -44,8 +44,12 @@ service:
|
||||
type: optional<map<string, unknown>>
|
||||
docs: Optional metadata for the project
|
||||
retention:
|
||||
type: integer
|
||||
docs: Number of days to retain data. Must be 0 or at least 3 days. Requires data-retention entitlement for non-zero values. Optional.
|
||||
type: optional<integer>
|
||||
docs: |
|
||||
Number of days to retain data.
|
||||
Must be 0 or at least 3 days.
|
||||
Requires data-retention entitlement for non-zero values.
|
||||
Optional. Will retain existing retention setting if omitted.
|
||||
response: Project
|
||||
|
||||
delete:
|
||||
@@ -102,10 +106,22 @@ types:
|
||||
properties:
|
||||
data: list<Project>
|
||||
|
||||
Organization:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
docs: The unique identifier of the organization
|
||||
name:
|
||||
type: string
|
||||
docs: The name of the organization
|
||||
|
||||
Project:
|
||||
properties:
|
||||
id: string
|
||||
name: string
|
||||
organization:
|
||||
type: Organization
|
||||
docs: The organization this project belongs to
|
||||
metadata:
|
||||
type: map<string, unknown>
|
||||
docs: Metadata for the project
|
||||
|
||||
@@ -55,7 +55,7 @@ types:
|
||||
CreateScoreConfigRequest:
|
||||
properties:
|
||||
name: string
|
||||
dataType: commons.ScoreDataType
|
||||
dataType: commons.ScoreConfigDataType
|
||||
categories:
|
||||
type: optional<list<commons.ConfigCategory>>
|
||||
docs: Configure custom categories for categorical scores. Pass a list of objects with `label` and `value` properties. Categories are autogenerated for boolean configs and cannot be passed
|
||||
|
||||
@@ -59,6 +59,9 @@ service:
|
||||
traceId:
|
||||
type: optional<string>
|
||||
docs: Retrieve only scores with a specific traceId.
|
||||
observationId:
|
||||
type: optional<string>
|
||||
docs: Comma-separated list of observation IDs to filter scores by.
|
||||
queueId:
|
||||
type: optional<string>
|
||||
docs: Retrieve only scores with a specific annotation queueId.
|
||||
@@ -69,6 +72,17 @@ service:
|
||||
type: optional<string>
|
||||
allow-multiple: true
|
||||
docs: Only scores linked to traces that include all of these tags will be returned.
|
||||
fields:
|
||||
type: optional<string>
|
||||
docs: "Comma-separated list of field groups to include in the response. Available field groups: 'score' (core score fields), 'trace' (trace properties: userId, tags, environment). If not specified, both 'score' and 'trace' are returned by default. Example: 'score' to exclude trace data, 'score,trace' to include both. Note: When filtering by trace properties (using userId or traceTags parameters), the 'trace' field group must be included, otherwise a 400 error will be returned."
|
||||
filter:
|
||||
type: optional<string>
|
||||
docs: >
|
||||
A JSON stringified array of filter objects. Each object requires type, column, operator, and value.
|
||||
Supports filtering by score metadata using the stringObject type.
|
||||
Example: [{"type":"stringObject","column":"metadata","key":"user_id","operator":"=","value":"abc123"}].
|
||||
Supported types: stringObject (metadata key-value filtering), string, number, datetime, stringOptions, arrayOptions.
|
||||
Supported operators for stringObject: =, contains, does not contain, starts with, ends with.
|
||||
response: GetScoresResponse
|
||||
get-by-id:
|
||||
docs: Get a score (supports both trace and session scores)
|
||||
@@ -108,12 +122,18 @@ types:
|
||||
properties:
|
||||
trace: optional<GetScoresResponseTraceData>
|
||||
|
||||
GetScoresResponseDataCorrection:
|
||||
extends: commons.CorrectionScore
|
||||
properties:
|
||||
trace: optional<GetScoresResponseTraceData>
|
||||
|
||||
GetScoresResponseData:
|
||||
discriminant: dataType
|
||||
union:
|
||||
NUMERIC: GetScoresResponseDataNumeric
|
||||
CATEGORICAL: GetScoresResponseDataCategorical
|
||||
BOOLEAN: GetScoresResponseDataBoolean
|
||||
CORRECTION: GetScoresResponseDataCorrection
|
||||
|
||||
GetScoresResponse:
|
||||
properties:
|
||||
|
||||
+8
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "3.141.0",
|
||||
"version": "3.153.0",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
@@ -21,6 +21,9 @@
|
||||
"dx-f": "pnpm i && pnpm run infra:dev:prune && pnpm run infra:dev:up --pull always && pnpm --filter=shared run db:reset:test && pnpm --filter=shared run db:reset -f && SKIP_CONFIRM=1 pnpm --filter=shared run ch:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
|
||||
"dx:skip-infra": "pnpm i && pnpm --filter=shared run db:reset:test && pnpm --filter=shared run db:reset && pnpm --filter=shared run ch:reset && pnpm --filter=shared run db:seed:examples && pnpm run dev",
|
||||
"build": "turbo run build",
|
||||
"build:check": "turbo run build:check",
|
||||
"typecheck": "turbo run typecheck",
|
||||
"tc": "turbo run typecheck",
|
||||
"start": "turbo run start",
|
||||
"dev": "turbo run dev",
|
||||
"dev:worker": "turbo run dev --filter=worker",
|
||||
@@ -40,7 +43,7 @@
|
||||
"husky": "^9.1.7",
|
||||
"prettier": "^3.6.2",
|
||||
"release-it": "^19.0.4",
|
||||
"turbo": "^2.6.3"
|
||||
"turbo": "^2.7.6"
|
||||
},
|
||||
"release-it": {
|
||||
"git": {
|
||||
@@ -93,10 +96,11 @@
|
||||
"tar-fs": "^2.1.2",
|
||||
"rollup@^4.0.0": "^4.22.4",
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"glob": "^10.5.0"
|
||||
"glob": "^10.5.0",
|
||||
"qs": "6.14.1"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"next-auth@4.24.12": "patches/next-auth@4.24.12.patch"
|
||||
"next-auth@4.24.13": "patches/next-auth@4.24.13.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
import turboConfig from "eslint-config-turbo/flat";
|
||||
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
|
||||
import "eslint-plugin-only-warn";
|
||||
|
||||
export default tseslint.config(
|
||||
// Global ignores
|
||||
{
|
||||
name: "langfuse/ignores",
|
||||
ignores: [
|
||||
"**/node_modules/",
|
||||
"**/dist/",
|
||||
"**/build/",
|
||||
"**/coverage/",
|
||||
"**/.next/",
|
||||
"**/.*",
|
||||
"eslint.config.mjs",
|
||||
],
|
||||
},
|
||||
|
||||
// Base JS rules (same as eslint v8 library.js)
|
||||
js.configs.recommended,
|
||||
|
||||
// Turbo monorepo rules
|
||||
...turboConfig,
|
||||
|
||||
// Prettier (last for rule precedence)
|
||||
eslintPluginPrettierRecommended,
|
||||
|
||||
// Global settings
|
||||
{
|
||||
name: "langfuse/base/globals",
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.es2021,
|
||||
React: "readonly",
|
||||
JSX: "readonly",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-redeclare": "off",
|
||||
"import/order": "off",
|
||||
},
|
||||
},
|
||||
|
||||
// TypeScript-specific - parser only + custom rules
|
||||
// Note: Old library.js had no TS rules, only eslint:recommended
|
||||
// Adding parser + plugin to support custom rules, but not extending recommended
|
||||
{
|
||||
name: "langfuse/base/typescript",
|
||||
files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
|
||||
plugins: {
|
||||
"@typescript-eslint": tseslint.plugin,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "off", // TypeScript handles this
|
||||
"no-dupe-class-members": "off", // TypeScript handles this (and supports overloads)
|
||||
"no-unused-vars": "off", // Use @typescript-eslint/no-unused-vars instead
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
name: "redis",
|
||||
message: "Import redis explicitly from '@langfuse/shared/src/server'",
|
||||
},
|
||||
],
|
||||
// Custom rule from eslint v8 shared/.eslintrc.js
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
destructuredArrayIgnorePattern: "^_",
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
// @ts-check
|
||||
export { default } from "./base.js";
|
||||
@@ -1,62 +0,0 @@
|
||||
const { resolve } = require("node:path");
|
||||
|
||||
const project = resolve(process.cwd(), "tsconfig.json");
|
||||
|
||||
// Handle eslint-config-turbo's default export
|
||||
const turboConfig = require("eslint-config-turbo");
|
||||
const turboConfigToUse = turboConfig.default || turboConfig;
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
// extends: ["eslint:recommended", "prettier"require().default],
|
||||
extends: ["eslint:recommended", "prettier"],
|
||||
plugins: ["only-warn", "turbo"],
|
||||
globals: {
|
||||
React: true,
|
||||
JSX: true,
|
||||
},
|
||||
env: {
|
||||
node: true,
|
||||
es6: true,
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
project,
|
||||
},
|
||||
},
|
||||
},
|
||||
ignorePatterns: [
|
||||
// Ignore dotfiles
|
||||
".*.js",
|
||||
"node_modules/",
|
||||
"dist/",
|
||||
],
|
||||
rules: {
|
||||
"no-redeclare": "off",
|
||||
"import/order": "off",
|
||||
...(turboConfigToUse.rules || {}),
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.js?(x)", "*.ts?(x)"],
|
||||
},
|
||||
{
|
||||
files: ["*.ts", "*.mts", "*.cts", "*.tsx"],
|
||||
// no-undef doesn't make sense in TS, see:
|
||||
// https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||
rules: {
|
||||
"no-undef": "off",
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
name: "redis",
|
||||
message:
|
||||
"Import redis explicitly from '@langfuse/shared/src/server' instead of using global.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
...(turboConfigToUse.overrides || []),
|
||||
],
|
||||
};
|
||||
@@ -1,79 +1,92 @@
|
||||
const { resolve } = require("node:path");
|
||||
import tseslint from "typescript-eslint";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
|
||||
import turboConfig from "eslint-config-turbo/flat";
|
||||
import "eslint-plugin-only-warn";
|
||||
|
||||
const project = resolve(process.cwd(), "tsconfig.json");
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const turboConfig = require("eslint-config-turbo");
|
||||
const turboConfigToUse = turboConfig.default || turboConfig;
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
});
|
||||
|
||||
/*
|
||||
* This is a custom ESLint configuration for use with
|
||||
* Next.js apps.
|
||||
*
|
||||
* This config extends the Vercel Engineering Style Guide.
|
||||
* For more information, see https://github.com/vercel/style-guide
|
||||
*
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
parser: "@typescript-eslint/parser", // Set the parser to @typescript-eslint/parser
|
||||
extends: [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/strict-type-checked",
|
||||
],
|
||||
rules: {
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-confusing-void-expression": "off",
|
||||
export default tseslint.config(
|
||||
// Global ignores - include config files
|
||||
{
|
||||
name: "langfuse/ignores",
|
||||
ignores: [
|
||||
"**/node_modules/",
|
||||
"**/dist/",
|
||||
"**/.next/",
|
||||
"**/coverage/",
|
||||
"eslint.config.mjs",
|
||||
],
|
||||
},
|
||||
parser: "@typescript-eslint/parser",
|
||||
|
||||
parserOptions: {
|
||||
project,
|
||||
},
|
||||
globals: {
|
||||
React: true,
|
||||
JSX: true,
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: ["next/core-web-vitals"],
|
||||
env: {
|
||||
es6: true,
|
||||
jest: true,
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
project,
|
||||
},
|
||||
// Next.js rules via FlatCompat (applies to all files)
|
||||
...compat.extends("next/core-web-vitals"),
|
||||
|
||||
// Turbo rules
|
||||
...turboConfig,
|
||||
|
||||
// Disable noisy turbo env var rule - project has many env vars not in turbo.json
|
||||
{
|
||||
name: "langfuse/next/turbo-overrides",
|
||||
rules: {
|
||||
"turbo/no-undeclared-env-vars": "off",
|
||||
},
|
||||
},
|
||||
ignorePatterns: ["node_modules/", "dist/"],
|
||||
// add rules configurations here
|
||||
rules: {
|
||||
...(turboConfigToUse.rules || {}),
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"warn",
|
||||
{
|
||||
prefer: "type-imports",
|
||||
fixStyle: "inline-type-imports",
|
||||
|
||||
// Prettier (last)
|
||||
eslintPluginPrettierRecommended,
|
||||
|
||||
// TypeScript config for TS files
|
||||
// Note: The old config had a bug (duplicate extends) that prevented TS rules from applying
|
||||
// Only adding parser + plugin + custom rules to match old behavior
|
||||
{
|
||||
name: "langfuse/next/typescript",
|
||||
files: ["**/*.ts", "**/*.tsx"],
|
||||
plugins: {
|
||||
"@typescript-eslint": tseslint.plugin,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
globals: {
|
||||
React: "readonly",
|
||||
JSX: "readonly",
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
// see: https://typescript-eslint.io/rules/no-unused-vars/#why-does-this-rule-report-variables-used-only-for-types
|
||||
// since v8, vars only used for types are unused at runtime and therefore throw a warning.
|
||||
// we fix those with workarounds (mostly just export the variables, as they are used as part of an API anyways)
|
||||
// see: https://github.com/typescript-eslint/typescript-eslint/issues/10266
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
typescript: {
|
||||
project: "./tsconfig.json",
|
||||
},
|
||||
},
|
||||
],
|
||||
"react/jsx-key": [
|
||||
"error",
|
||||
{
|
||||
warnOnDuplicates: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
rules: {
|
||||
"no-unused-vars": "off", // Use @typescript-eslint/no-unused-vars instead
|
||||
// Custom rules from old config
|
||||
"@typescript-eslint/consistent-type-imports": [
|
||||
"warn",
|
||||
{
|
||||
prefer: "type-imports",
|
||||
fixStyle: "inline-type-imports",
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
destructuredArrayIgnorePattern: "^_",
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
"react/jsx-key": ["error", { warnOnDuplicates: true }],
|
||||
},
|
||||
},
|
||||
};
|
||||
);
|
||||
|
||||
@@ -3,18 +3,31 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./base": "./base.js",
|
||||
"./next": "./next.js"
|
||||
},
|
||||
"files": [
|
||||
"library.js",
|
||||
"index.js",
|
||||
"base.js",
|
||||
"next.js"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^8.48.1",
|
||||
"@typescript-eslint/parser": "^8.48.1",
|
||||
"@vercel/style-guide": "^6.0.0",
|
||||
"eslint-config-next": "15.5.9",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-turbo": "^2.6.3",
|
||||
"dependencies": {
|
||||
"@eslint/compat": "^2.0.0",
|
||||
"@eslint/eslintrc": "^3.3.3",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"eslint-config-next": "15.5.10",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-config-turbo": "^2.7.6",
|
||||
"eslint-plugin-only-warn": "^1.1.0",
|
||||
"typescript": "^5.7.2"
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"globals": "^16.0.0",
|
||||
"typescript-eslint": "^8.50.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^9.39.0",
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ["@repo/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
ALTER TABLE scores ON CLUSTER default DROP COLUMN IF EXISTS long_string_value;
|
||||
+1
@@ -0,0 +1 @@
|
||||
ALTER TABLE scores ON CLUSTER default ADD COLUMN IF NOT EXISTS long_string_value String CODEC(ZSTD(3));
|
||||
+1
@@ -0,0 +1 @@
|
||||
ALTER TABLE scores DROP COLUMN IF EXISTS long_string_value;
|
||||
+1
@@ -0,0 +1 @@
|
||||
ALTER TABLE scores ADD COLUMN IF NOT EXISTS long_string_value String CODEC(ZSTD(3));
|
||||
@@ -75,6 +75,8 @@ clickhouse client \
|
||||
-- Create observations_batch_staging table for batch processing
|
||||
-- This table uses 3-minute partitions to efficiently process observations in batches
|
||||
-- and merge them with traces data into the events table.
|
||||
-- Partitions are automatically expired after 12 hours via TTL (ttl_only_drop_parts=1
|
||||
-- ensures only complete partitions are dropped, not individual rows).
|
||||
-- See LFE-7122 for implementation details.
|
||||
CREATE TABLE IF NOT EXISTS observations_batch_staging
|
||||
(
|
||||
@@ -123,7 +125,9 @@ ORDER BY (
|
||||
toDate(s3_first_seen_timestamp),
|
||||
trace_id,
|
||||
id
|
||||
);
|
||||
)
|
||||
TTL s3_first_seen_timestamp + INTERVAL 12 HOUR
|
||||
SETTINGS ttl_only_drop_parts = 1;
|
||||
|
||||
-- Create new events table for development setups.
|
||||
-- We expect this to be fully immutable and eventually replace observations.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import baseConfig from "@repo/eslint-config";
|
||||
|
||||
export default [...baseConfig];
|
||||
@@ -25,16 +25,26 @@
|
||||
"import": "./dist/src/server/auth/apiKeys.js",
|
||||
"require": "./dist/src/server/auth/apiKeys.js"
|
||||
},
|
||||
"./src/utils/chatml": {
|
||||
"import": "./dist/src/utils/chatml/index.js",
|
||||
"require": "./dist/src/utils/chatml/index.js"
|
||||
},
|
||||
"./encryption": {
|
||||
"import": "./dist/src/encryption/index.js",
|
||||
"require": "./dist/src/encryption/index.js"
|
||||
},
|
||||
"./src/server/ee/ingestionMasking": {
|
||||
"import": "./dist/src/server/ee/ingestionMasking/index.js",
|
||||
"require": "./dist/src/server/ee/ingestionMasking/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build:check": "tsc",
|
||||
"typecheck": "dotenv -e ../../.env -- tsc --noEmit --incremental --tsBuildInfoFile .tsbuildinfo",
|
||||
"dev": "tsc --watch",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0",
|
||||
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
||||
"lint": "eslint . --cache --cache-location dist/.eslintcache --max-warnings 0",
|
||||
"lint:fix": "eslint . --cache --cache-location dist/.eslintcache --fix",
|
||||
"db:migrate": "DISABLE_ERD=false dotenv -e ../../.env -- npx prisma migrate dev",
|
||||
"db:push": "DISABLE_ERD=false dotenv -e ../../.env -- npx prisma db push",
|
||||
"db:reset": "dotenv -e ../../.env npx -- prisma migrate reset",
|
||||
@@ -63,12 +73,12 @@
|
||||
"@aws-sdk/s3-request-presigner": "^3.679.0",
|
||||
"@azure/storage-blob": "^12.26.0",
|
||||
"@clickhouse/client": "^1.13.0",
|
||||
"@google-cloud/storage": "^7.17.0",
|
||||
"@google-cloud/storage": "^7.18.0",
|
||||
"@langchain/anthropic": "^0.3.32",
|
||||
"@langchain/aws": "^0.1.15",
|
||||
"@langchain/core": "^0.3.58",
|
||||
"@langchain/google-genai": "^0.2.12",
|
||||
"@langchain/google-vertexai": "^0.2.12",
|
||||
"@langchain/core": "^0.3.80",
|
||||
"@langchain/google-genai": "^0.2.18",
|
||||
"@langchain/google-vertexai": "^0.2.18",
|
||||
"@langchain/openai": "^0.5.13",
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0",
|
||||
"@prisma/client": "^6.17.1",
|
||||
@@ -90,11 +100,11 @@
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"jsonpath-plus": "10.3.0",
|
||||
"kysely": "^0.27.4",
|
||||
"langchain": "^0.3.30",
|
||||
"langchain": "^0.3.37",
|
||||
"langfuse-langchain": "3.38.6",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash": "^4.17.23",
|
||||
"lossless-json": "^4.1.1",
|
||||
"next-auth": "^4.24.12",
|
||||
"next-auth": "^4.24.13",
|
||||
"nodemailer": "^7.0.11",
|
||||
"prisma-extension-kysely": "^3.0.0",
|
||||
"safe-regex2": "^5.0.0",
|
||||
@@ -106,21 +116,20 @@
|
||||
"devDependencies": {
|
||||
"@repo/eslint-config": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/lodash": "^4.17.10",
|
||||
"@types/lodash": "^4.17.23",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/nodemailer": "^7.0.4",
|
||||
"@types/pg": "^8.11.10",
|
||||
"@types/react": "19.2.3",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/parser": "^8.48.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"@typescript-eslint/parser": "^8.50.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"kysely-codegen": "^0.16.8",
|
||||
"prettier": "^3.6.2",
|
||||
"prisma": "^6.17.1",
|
||||
"prisma-erd-generator": "^2.1.0",
|
||||
"prisma-kysely": "^1.8.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-watch": "^6.2.0",
|
||||
|
||||
@@ -47,12 +47,13 @@ export const LegacyPrismaScoreSource = {
|
||||
} as const;
|
||||
export type LegacyPrismaScoreSource =
|
||||
(typeof LegacyPrismaScoreSource)[keyof typeof LegacyPrismaScoreSource];
|
||||
export const ScoreDataType = {
|
||||
export const ScoreConfigDataType = {
|
||||
CATEGORICAL: "CATEGORICAL",
|
||||
NUMERIC: "NUMERIC",
|
||||
BOOLEAN: "BOOLEAN",
|
||||
} as const;
|
||||
export type ScoreDataType = (typeof ScoreDataType)[keyof typeof ScoreDataType];
|
||||
export type ScoreConfigDataType =
|
||||
(typeof ScoreConfigDataType)[keyof typeof ScoreConfigDataType];
|
||||
export const AnnotationQueueStatus = {
|
||||
PENDING: "PENDING",
|
||||
COMPLETED: "COMPLETED",
|
||||
@@ -135,6 +136,13 @@ export const BlobStorageExportMode = {
|
||||
} as const;
|
||||
export type BlobStorageExportMode =
|
||||
(typeof BlobStorageExportMode)[keyof typeof BlobStorageExportMode];
|
||||
export const AnalyticsIntegrationExportSource = {
|
||||
TRACES_OBSERVATIONS: "TRACES_OBSERVATIONS",
|
||||
TRACES_OBSERVATIONS_EVENTS: "TRACES_OBSERVATIONS_EVENTS",
|
||||
EVENTS: "EVENTS",
|
||||
} as const;
|
||||
export type AnalyticsIntegrationExportSource =
|
||||
(typeof AnalyticsIntegrationExportSource)[keyof typeof AnalyticsIntegrationExportSource];
|
||||
export const DashboardWidgetViews = {
|
||||
TRACES: "TRACES",
|
||||
OBSERVATIONS: "OBSERVATIONS",
|
||||
@@ -145,6 +153,7 @@ export type DashboardWidgetViews =
|
||||
(typeof DashboardWidgetViews)[keyof typeof DashboardWidgetViews];
|
||||
export const DashboardWidgetChartType = {
|
||||
LINE_TIME_SERIES: "LINE_TIME_SERIES",
|
||||
AREA_TIME_SERIES: "AREA_TIME_SERIES",
|
||||
BAR_TIME_SERIES: "BAR_TIME_SERIES",
|
||||
HORIZONTAL_BAR: "HORIZONTAL_BAR",
|
||||
VERTICAL_BAR: "VERTICAL_BAR",
|
||||
@@ -358,6 +367,7 @@ export type BlobStorageIntegration = {
|
||||
file_type: Generated<BlobStorageIntegrationFileType>;
|
||||
export_mode: Generated<BlobStorageExportMode>;
|
||||
export_start_date: Timestamp | null;
|
||||
export_source: Generated<AnalyticsIntegrationExportSource>;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
};
|
||||
@@ -379,6 +389,10 @@ export type Comment = {
|
||||
updated_at: Generated<Timestamp>;
|
||||
content: string;
|
||||
author_user_id: string | null;
|
||||
data_field: string | null;
|
||||
path: Generated<string[]>;
|
||||
range_start: Generated<number[]>;
|
||||
range_end: Generated<number[]>;
|
||||
};
|
||||
export type CommentReaction = {
|
||||
id: string;
|
||||
@@ -447,25 +461,10 @@ export type DatasetItem = {
|
||||
dataset_id: string;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
sys_id: Generated<string | null>;
|
||||
valid_from: Generated<Timestamp>;
|
||||
valid_to: Timestamp | null;
|
||||
is_deleted: Generated<boolean>;
|
||||
};
|
||||
export type DatasetItemEvent = {
|
||||
id: string;
|
||||
item_id: string;
|
||||
project_id: string;
|
||||
dataset_id: string;
|
||||
status: DatasetStatus | null;
|
||||
input: unknown | null;
|
||||
expected_output: unknown | null;
|
||||
metadata: unknown | null;
|
||||
source_trace_id: string | null;
|
||||
source_observation_id: string | null;
|
||||
created_at: Timestamp | null;
|
||||
deleted_at: Timestamp | null;
|
||||
};
|
||||
export type DatasetRunItems = {
|
||||
id: string;
|
||||
project_id: string;
|
||||
@@ -543,6 +542,7 @@ export type JobExecution = {
|
||||
job_input_trace_timestamp: Timestamp | null;
|
||||
job_input_observation_id: string | null;
|
||||
job_input_dataset_item_id: string | null;
|
||||
job_input_dataset_item_valid_from: Timestamp | null;
|
||||
job_output_score_id: string | null;
|
||||
execution_trace_id: string | null;
|
||||
};
|
||||
@@ -596,7 +596,7 @@ export type LegacyPrismaScore = {
|
||||
queue_id: string | null;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
data_type: Generated<ScoreDataType>;
|
||||
data_type: Generated<ScoreConfigDataType>;
|
||||
};
|
||||
export type LegacyPrismaTrace = {
|
||||
id: string;
|
||||
@@ -683,6 +683,7 @@ export type MixpanelIntegration = {
|
||||
last_sync_at: Timestamp | null;
|
||||
enabled: boolean;
|
||||
created_at: Generated<Timestamp>;
|
||||
export_source: Generated<AnalyticsIntegrationExportSource>;
|
||||
};
|
||||
export type Model = {
|
||||
id: string;
|
||||
@@ -756,6 +757,7 @@ export type PosthogIntegration = {
|
||||
last_sync_at: Timestamp | null;
|
||||
enabled: boolean;
|
||||
created_at: Generated<Timestamp>;
|
||||
export_source: Generated<AnalyticsIntegrationExportSource>;
|
||||
};
|
||||
export type Price = {
|
||||
id: string;
|
||||
@@ -785,6 +787,7 @@ export type Project = {
|
||||
deleted_at: Timestamp | null;
|
||||
name: string;
|
||||
retention_days: number | null;
|
||||
has_traces: Generated<boolean>;
|
||||
metadata: unknown | null;
|
||||
};
|
||||
export type ProjectMembership = {
|
||||
@@ -834,7 +837,7 @@ export type ScoreConfig = {
|
||||
updated_at: Generated<Timestamp>;
|
||||
project_id: string;
|
||||
name: string;
|
||||
data_type: ScoreDataType;
|
||||
data_type: ScoreConfigDataType;
|
||||
is_archived: Generated<boolean>;
|
||||
min_value: number | null;
|
||||
max_value: number | null;
|
||||
@@ -924,6 +927,7 @@ export type User = {
|
||||
password: string | null;
|
||||
image: string | null;
|
||||
admin: Generated<boolean>;
|
||||
v4_beta_enabled: Generated<boolean>;
|
||||
created_at: Generated<Timestamp>;
|
||||
updated_at: Generated<Timestamp>;
|
||||
feature_flags: Generated<string[]>;
|
||||
@@ -954,7 +958,6 @@ export type DB = {
|
||||
cron_jobs: CronJobs;
|
||||
dashboard_widgets: DashboardWidget;
|
||||
dashboards: Dashboard;
|
||||
dataset_item_events: DatasetItemEvent;
|
||||
dataset_items: DatasetItem;
|
||||
dataset_run_items: DatasetRunItems;
|
||||
dataset_runs: DatasetRuns;
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Changed the type of `data_type` on the `score_configs` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
|
||||
|
||||
*/
|
||||
-- Rename enum
|
||||
ALTER TYPE "ScoreDataType" RENAME TO "ScoreConfigDataType";
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
INSERT INTO background_migrations (id, name, script, args)
|
||||
VALUES ('01a0c890-2094-8e2f-c773-cf4d14638fa4', '20260106_encrypt_blob_storage_secrets', 'encryptBlobStorageSecrets', '{}');
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
-- Add inline comment positioning columns to comments table
|
||||
-- These columns enable character-based inline comments on trace/observation IO
|
||||
|
||||
-- data_field: which IO field the comment is on ('input' | 'output' | 'metadata')
|
||||
ALTER TABLE "comments" ADD COLUMN "data_field" TEXT;
|
||||
|
||||
-- path: JSON Path expressions array, e.g., ["$.messages[1].text"]
|
||||
ALTER TABLE "comments" ADD COLUMN "path" TEXT[] DEFAULT '{}';
|
||||
|
||||
-- range_start: start offsets per path (inclusive), UTF-16 code units
|
||||
ALTER TABLE "comments" ADD COLUMN "range_start" INTEGER[] DEFAULT '{}';
|
||||
|
||||
-- range_end: end offsets per path (exclusive), UTF-16 code units
|
||||
ALTER TABLE "comments" ADD COLUMN "range_end" INTEGER[] DEFAULT '{}';
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `sys_id` on the `dataset_items` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "dataset_items" DROP COLUMN "sys_id";
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "dataset_item_events" DROP CONSTRAINT "dataset_item_events_dataset_id_project_id_fkey";
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `dataset_item_events` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropTable
|
||||
DROP TABLE "dataset_item_events";
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "AnalyticsIntegrationExportSource" AS ENUM ('TRACES_OBSERVATIONS', 'TRACES_OBSERVATIONS_EVENTS', 'EVENTS');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "blob_storage_integrations" ADD COLUMN "export_source" "AnalyticsIntegrationExportSource" NOT NULL DEFAULT 'TRACES_OBSERVATIONS';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "mixpanel_integrations" ADD COLUMN "export_source" "AnalyticsIntegrationExportSource" NOT NULL DEFAULT 'TRACES_OBSERVATIONS';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "posthog_integrations" ADD COLUMN "export_source" "AnalyticsIntegrationExportSource" NOT NULL DEFAULT 'TRACES_OBSERVATIONS';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX CONCURRENTLY "media_project_id_created_at_idx" ON "media"("project_id", "created_at");
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "users" ADD COLUMN "v4_beta_enabled" BOOLEAN NOT NULL DEFAULT false;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "job_executions" ADD COLUMN "job_input_dataset_item_valid_from" TIMESTAMP(3);
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."pending_deletions_project_id_object_is_deleted_idx";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "pending_deletions_project_id_object_is_deleted_object_id_id_idx" ON "pending_deletions"("project_id", "object", "is_deleted", "object_id", "id");
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "projects" ADD COLUMN "has_traces" BOOLEAN NOT NULL DEFAULT false;
|
||||
+1
@@ -0,0 +1 @@
|
||||
ALTER TYPE "DashboardWidgetChartType" ADD VALUE 'AREA_TIME_SERIES';
|
||||
@@ -13,14 +13,6 @@ datasource db {
|
||||
shadowDatabaseUrl = env("SHADOW_DATABASE_URL")
|
||||
}
|
||||
|
||||
generator erd {
|
||||
provider = "prisma-erd-generator"
|
||||
ignoreTables = ["_prisma_migrations", "Session", "Account", "Example"]
|
||||
disabled = true
|
||||
ignoreEnums = true
|
||||
output = "database.svg"
|
||||
}
|
||||
|
||||
generator kysely {
|
||||
provider = "prisma-kysely"
|
||||
|
||||
@@ -72,6 +64,7 @@ model User {
|
||||
password String?
|
||||
image String?
|
||||
admin Boolean @default(false)
|
||||
v4BetaEnabled Boolean @default(false) @map("v4_beta_enabled")
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
organizationMemberships OrganizationMembership[]
|
||||
@@ -135,6 +128,7 @@ model Project {
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
name String
|
||||
retentionDays Int? @map("retention_days")
|
||||
hasTraces Boolean @default(false) @map("has_traces")
|
||||
metadata Json?
|
||||
projectMembers ProjectMembership[]
|
||||
organization Organization @relation(fields: [orgId], references: [id], onUpdate: Cascade, onDelete: Cascade)
|
||||
@@ -457,7 +451,7 @@ model LegacyPrismaScore {
|
||||
queueId String? @map("queue_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
dataType ScoreDataType @default(NUMERIC) @map("data_type")
|
||||
dataType ScoreConfigDataType @default(NUMERIC) @map("data_type")
|
||||
scoreConfig ScoreConfig? @relation(fields: [configId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@unique([id, projectId]) // used for upserts via prisma
|
||||
@@ -488,7 +482,7 @@ model ScoreConfig {
|
||||
projectId String @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
dataType ScoreDataType @map("data_type")
|
||||
dataType ScoreConfigDataType @map("data_type")
|
||||
isArchived Boolean @default(false) @map("is_archived")
|
||||
minValue Float? @map("min_value")
|
||||
maxValue Float? @map("max_value")
|
||||
@@ -506,7 +500,7 @@ model ScoreConfig {
|
||||
@@map("score_configs")
|
||||
}
|
||||
|
||||
enum ScoreDataType {
|
||||
enum ScoreConfigDataType {
|
||||
CATEGORICAL
|
||||
NUMERIC
|
||||
BOOLEAN
|
||||
@@ -592,21 +586,20 @@ model CronJobs {
|
||||
}
|
||||
|
||||
model Dataset {
|
||||
id String @default(cuid())
|
||||
projectId String @map("project_id")
|
||||
id String @default(cuid())
|
||||
projectId String @map("project_id")
|
||||
name String
|
||||
description String?
|
||||
metadata Json?
|
||||
remoteExperimentUrl String? @map("remote_experiment_url")
|
||||
remoteExperimentPayload Json? @map("remote_experiment_payload")
|
||||
inputSchema Json? @map("input_schema") @db.Json
|
||||
expectedOutputSchema Json? @map("expected_output_schema") @db.Json
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
remoteExperimentUrl String? @map("remote_experiment_url")
|
||||
remoteExperimentPayload Json? @map("remote_experiment_payload")
|
||||
inputSchema Json? @map("input_schema") @db.Json
|
||||
expectedOutputSchema Json? @map("expected_output_schema") @db.Json
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
datasetItems DatasetItem[]
|
||||
datasetRuns DatasetRuns[]
|
||||
datasetItemEvents DatasetItemEvent[]
|
||||
|
||||
@@id([id, projectId])
|
||||
@@unique([projectId, name])
|
||||
@@ -629,7 +622,6 @@ model DatasetItem {
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
// dataset version cols
|
||||
sysId String? @default(dbgenerated()) @map("sys_id")
|
||||
validFrom DateTime @default(now()) @map("valid_from")
|
||||
validTo DateTime? @map("valid_to")
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
@@ -645,25 +637,6 @@ model DatasetItem {
|
||||
@@map("dataset_items")
|
||||
}
|
||||
|
||||
model DatasetItemEvent {
|
||||
id String @id @default(cuid())
|
||||
itemId String @map("item_id")
|
||||
projectId String @map("project_id")
|
||||
datasetId String @map("dataset_id")
|
||||
dataset Dataset @relation(fields: [datasetId, projectId], references: [id, projectId], onDelete: Cascade)
|
||||
status DatasetStatus?
|
||||
input Json?
|
||||
expectedOutput Json? @map("expected_output")
|
||||
metadata Json?
|
||||
sourceTraceId String? @map("source_trace_id")
|
||||
sourceObservationId String? @map("source_observation_id")
|
||||
createdAt DateTime? @map("created_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
@@index([projectId, datasetId, itemId, createdAt])
|
||||
@@map("dataset_item_events")
|
||||
}
|
||||
|
||||
enum DatasetStatus {
|
||||
ACTIVE
|
||||
ARCHIVED
|
||||
@@ -722,6 +695,15 @@ model Comment {
|
||||
authorUserId String? @map("author_user_id") // no fk constraint, user can be deleted
|
||||
reactions CommentReaction[]
|
||||
|
||||
// Inline comment positioning (all must be set together or all null/empty)
|
||||
// dataField: which IO field the comment is on ('input' | 'output' | 'metadata')
|
||||
// path: Array of JSON Path expressions, e.g., ["$.messages[1].text"]
|
||||
// rangeStart/rangeEnd: parallel arrays for start/end offsets per path (exclusive end, UTF-16 code units)
|
||||
dataField String? @map("data_field")
|
||||
path String[] @default([]) @map("path")
|
||||
rangeStart Int[] @default([]) @map("range_start")
|
||||
rangeEnd Int[] @default([]) @map("range_end")
|
||||
|
||||
@@index([projectId, objectType, objectId])
|
||||
@@map("comments")
|
||||
}
|
||||
@@ -1032,6 +1014,8 @@ model JobExecution {
|
||||
|
||||
jobInputDatasetItemId String? @map("job_input_dataset_item_id") // no fk constraint - job execution sensible standalone
|
||||
|
||||
jobInputDatasetItemValidFrom DateTime? @map("job_input_dataset_item_valid_from")
|
||||
|
||||
jobOutputScoreId String? @map("job_output_score_id")
|
||||
|
||||
executionTraceId String? @map("execution_trace_id")
|
||||
@@ -1079,25 +1063,27 @@ model SsoConfig {
|
||||
}
|
||||
|
||||
model PosthogIntegration {
|
||||
projectId String @id @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
encryptedPosthogApiKey String @map("encrypted_posthog_api_key")
|
||||
posthogHostName String @map("posthog_host_name")
|
||||
lastSyncAt DateTime? @map("last_sync_at")
|
||||
projectId String @id @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
encryptedPosthogApiKey String @map("encrypted_posthog_api_key")
|
||||
posthogHostName String @map("posthog_host_name")
|
||||
lastSyncAt DateTime? @map("last_sync_at")
|
||||
enabled Boolean
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
exportSource AnalyticsIntegrationExportSource @default(TRACES_OBSERVATIONS) @map("export_source")
|
||||
|
||||
@@map("posthog_integrations")
|
||||
}
|
||||
|
||||
model MixpanelIntegration {
|
||||
projectId String @id @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
encryptedMixpanelProjectToken String @map("encrypted_mixpanel_project_token")
|
||||
mixpanelRegion String @map("mixpanel_region")
|
||||
lastSyncAt DateTime? @map("last_sync_at")
|
||||
projectId String @id @map("project_id")
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
encryptedMixpanelProjectToken String @map("encrypted_mixpanel_project_token")
|
||||
mixpanelRegion String @map("mixpanel_region")
|
||||
lastSyncAt DateTime? @map("last_sync_at")
|
||||
enabled Boolean
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
exportSource AnalyticsIntegrationExportSource @default(TRACES_OBSERVATIONS) @map("export_source")
|
||||
|
||||
@@map("mixpanel_integrations")
|
||||
}
|
||||
@@ -1117,13 +1103,14 @@ model BlobStorageIntegration {
|
||||
forcePathStyle Boolean @map("force_path_style")
|
||||
|
||||
// Integration Config
|
||||
nextSyncAt DateTime? @map("next_sync_at")
|
||||
lastSyncAt DateTime? @map("last_sync_at")
|
||||
nextSyncAt DateTime? @map("next_sync_at")
|
||||
lastSyncAt DateTime? @map("last_sync_at")
|
||||
enabled Boolean
|
||||
exportFrequency String @map("export_frequency")
|
||||
fileType BlobStorageIntegrationFileType @default(CSV) @map("file_type")
|
||||
exportMode BlobStorageExportMode @default(FULL_HISTORY) @map("export_mode")
|
||||
exportStartDate DateTime? @map("export_start_date")
|
||||
exportFrequency String @map("export_frequency")
|
||||
fileType BlobStorageIntegrationFileType @default(CSV) @map("file_type")
|
||||
exportMode BlobStorageExportMode @default(FULL_HISTORY) @map("export_mode")
|
||||
exportStartDate DateTime? @map("export_start_date")
|
||||
exportSource AnalyticsIntegrationExportSource @default(TRACES_OBSERVATIONS) @map("export_source")
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
@@ -1155,6 +1142,14 @@ enum BlobStorageExportMode {
|
||||
@@map("BlobStorageExportMode")
|
||||
}
|
||||
|
||||
enum AnalyticsIntegrationExportSource {
|
||||
TRACES_OBSERVATIONS
|
||||
TRACES_OBSERVATIONS_EVENTS
|
||||
EVENTS
|
||||
|
||||
@@map("AnalyticsIntegrationExportSource")
|
||||
}
|
||||
|
||||
model BatchExport {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
@@ -1227,6 +1222,7 @@ model Media {
|
||||
|
||||
@@unique([projectId, id])
|
||||
@@unique([projectId, sha256Hash])
|
||||
@@index([projectId, createdAt])
|
||||
@@map("media")
|
||||
}
|
||||
|
||||
@@ -1350,6 +1346,7 @@ enum DashboardWidgetViews {
|
||||
|
||||
enum DashboardWidgetChartType {
|
||||
LINE_TIME_SERIES
|
||||
AREA_TIME_SERIES
|
||||
BAR_TIME_SERIES
|
||||
HORIZONTAL_BAR
|
||||
VERTICAL_BAR
|
||||
@@ -1555,7 +1552,7 @@ model PendingDeletion {
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||
|
||||
@@index([projectId, object, isDeleted])
|
||||
@@index([projectId, object, isDeleted, objectId, id])
|
||||
@@index([objectId, object])
|
||||
@@map("pending_deletions")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Media Seeding Utilities
|
||||
*
|
||||
* Seeds test traces with media attachments (images, PDFs, audio) for testing
|
||||
* the JSON Beta view's media rendering capabilities.
|
||||
*
|
||||
* Integrated into the seeder flow - runs automatically during `dx` or `db:seed:examples`.
|
||||
*/
|
||||
|
||||
import crypto from "crypto";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { Readable } from "stream";
|
||||
|
||||
import { prisma } from "../../src/db";
|
||||
import { env } from "../../src/env";
|
||||
import { logger, StorageServiceFactory } from "../../src/server";
|
||||
|
||||
// Test file paths (relative to monorepo root)
|
||||
const TEST_FILES_DIR = path.join(
|
||||
__dirname,
|
||||
"../../../../web/src/__tests__/static",
|
||||
);
|
||||
|
||||
interface MediaFile {
|
||||
name: string;
|
||||
contentType: string;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
const MEDIA_FILES: Record<string, MediaFile> = {
|
||||
image: {
|
||||
name: "langfuse-logo.png",
|
||||
contentType: "image/png",
|
||||
filePath: path.join(TEST_FILES_DIR, "langfuse-logo.png"),
|
||||
},
|
||||
pdf: {
|
||||
name: "bitcoin.pdf",
|
||||
contentType: "application/pdf",
|
||||
filePath: path.join(TEST_FILES_DIR, "bitcoin.pdf"),
|
||||
},
|
||||
audio: {
|
||||
name: "sounds-of-mars.wav",
|
||||
contentType: "audio/wav",
|
||||
filePath: path.join(
|
||||
TEST_FILES_DIR,
|
||||
"sounds-of-mars-one-small-step-earth.wav",
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
// Deterministic trace IDs for media test traces
|
||||
export const MEDIA_TEST_TRACE_IDS = {
|
||||
imageOnly: "seed-media-image-only",
|
||||
allTypes: "seed-media-all-types",
|
||||
allTypesChatML: "seed-media-all-types-chatml",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Derive media ID from SHA256 hash (first 22 chars, URL-safe)
|
||||
*/
|
||||
function getMediaIdFromHash(sha256Hash: string): string {
|
||||
const urlSafeHash = sha256Hash.replaceAll("+", "-").replaceAll("/", "_");
|
||||
return urlSafeHash.slice(0, 22);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bucket path for a media file
|
||||
*/
|
||||
function getBucketPath(
|
||||
projectId: string,
|
||||
mediaId: string,
|
||||
contentType: string,
|
||||
): string {
|
||||
const extensionMap: Record<string, string> = {
|
||||
"image/png": "png",
|
||||
"image/jpeg": "jpg",
|
||||
"application/pdf": "pdf",
|
||||
"audio/wav": "wav",
|
||||
"audio/mpeg": "mp3",
|
||||
};
|
||||
const extension = extensionMap[contentType] || "bin";
|
||||
const prefix = env.LANGFUSE_S3_MEDIA_UPLOAD_PREFIX || "";
|
||||
return `${prefix}${projectId}/${mediaId}.${extension}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a media file to storage and create database records
|
||||
*/
|
||||
async function uploadAndCreateMediaRecord(
|
||||
projectId: string,
|
||||
traceId: string,
|
||||
field: "input" | "output" | "metadata",
|
||||
mediaFile: MediaFile,
|
||||
): Promise<void> {
|
||||
// Check if bucket is configured
|
||||
if (!env.LANGFUSE_S3_MEDIA_UPLOAD_BUCKET) {
|
||||
logger.warn(
|
||||
"[seed-media] LANGFUSE_S3_MEDIA_UPLOAD_BUCKET not configured, skipping media seeding",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(mediaFile.filePath)) {
|
||||
logger.warn(
|
||||
`[seed-media] Test file not found: ${mediaFile.filePath}, skipping`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileBytes = fs.readFileSync(mediaFile.filePath);
|
||||
const sha256Hash = crypto
|
||||
.createHash("sha256")
|
||||
.update(fileBytes)
|
||||
.digest("base64");
|
||||
const mediaId = getMediaIdFromHash(sha256Hash);
|
||||
const bucketPath = getBucketPath(projectId, mediaId, mediaFile.contentType);
|
||||
|
||||
// Check if media already exists
|
||||
const existingMedia = await prisma.media.findUnique({
|
||||
where: {
|
||||
projectId_sha256Hash: {
|
||||
projectId,
|
||||
sha256Hash,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingMedia && existingMedia.uploadHttpStatus === 200) {
|
||||
logger.debug(
|
||||
`[seed-media] Media already exists for ${mediaFile.name}, creating TraceMedia link only`,
|
||||
);
|
||||
|
||||
// Just create the TraceMedia link
|
||||
await prisma.$queryRaw`
|
||||
INSERT INTO "trace_media" ("id", "project_id", "trace_id", "media_id", "field")
|
||||
VALUES (${crypto.randomUUID()}, ${projectId}, ${traceId}, ${existingMedia.id}, ${field})
|
||||
ON CONFLICT DO NOTHING;
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload to storage
|
||||
try {
|
||||
const storageClient = StorageServiceFactory.getInstance({
|
||||
bucketName: env.LANGFUSE_S3_MEDIA_UPLOAD_BUCKET,
|
||||
accessKeyId: env.LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID,
|
||||
secretAccessKey: env.LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY,
|
||||
endpoint: env.LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT,
|
||||
region: env.LANGFUSE_S3_MEDIA_UPLOAD_REGION,
|
||||
forcePathStyle: env.LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE === "true",
|
||||
awsSse: env.LANGFUSE_S3_MEDIA_UPLOAD_SSE,
|
||||
awsSseKmsKeyId: env.LANGFUSE_S3_MEDIA_UPLOAD_SSE_KMS_KEY_ID,
|
||||
});
|
||||
|
||||
await storageClient.uploadFile({
|
||||
fileName: bucketPath,
|
||||
fileType: mediaFile.contentType,
|
||||
data: Readable.from(fileBytes),
|
||||
});
|
||||
|
||||
logger.debug(`[seed-media] Uploaded ${mediaFile.name} to ${bucketPath}`);
|
||||
} catch (error) {
|
||||
logger.error(`[seed-media] Failed to upload ${mediaFile.name}:`, error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create Media record
|
||||
await prisma.$queryRaw`
|
||||
INSERT INTO "media" (
|
||||
"id",
|
||||
"project_id",
|
||||
"sha_256_hash",
|
||||
"bucket_path",
|
||||
"bucket_name",
|
||||
"content_type",
|
||||
"content_length",
|
||||
"uploaded_at",
|
||||
"upload_http_status"
|
||||
)
|
||||
VALUES (
|
||||
${mediaId},
|
||||
${projectId},
|
||||
${sha256Hash},
|
||||
${bucketPath},
|
||||
${env.LANGFUSE_S3_MEDIA_UPLOAD_BUCKET},
|
||||
${mediaFile.contentType},
|
||||
${BigInt(fileBytes.length)},
|
||||
${new Date()},
|
||||
${200}
|
||||
)
|
||||
ON CONFLICT ("project_id", "sha_256_hash")
|
||||
DO UPDATE SET
|
||||
"bucket_name" = ${env.LANGFUSE_S3_MEDIA_UPLOAD_BUCKET},
|
||||
"bucket_path" = ${bucketPath},
|
||||
"content_type" = ${mediaFile.contentType},
|
||||
"content_length" = ${BigInt(fileBytes.length)},
|
||||
"uploaded_at" = ${new Date()},
|
||||
"upload_http_status" = ${200}
|
||||
`;
|
||||
|
||||
// Create TraceMedia link
|
||||
await prisma.$queryRaw`
|
||||
INSERT INTO "trace_media" ("id", "project_id", "trace_id", "media_id", "field")
|
||||
VALUES (${crypto.randomUUID()}, ${projectId}, ${traceId}, ${mediaId}, ${field})
|
||||
ON CONFLICT DO NOTHING;
|
||||
`;
|
||||
|
||||
logger.info(
|
||||
`[seed-media] Created media record for ${mediaFile.name} -> ${field}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed media test traces for a project
|
||||
*
|
||||
* Creates two test traces with media attachments:
|
||||
* 1. Image only (in input)
|
||||
* 2. All types (image in input, PDF in output, audio in metadata)
|
||||
*/
|
||||
export async function seedMediaTraces(projectId: string): Promise<void> {
|
||||
logger.info(`[seed-media] Seeding media traces for project ${projectId}`);
|
||||
|
||||
// Check if bucket is configured
|
||||
if (!env.LANGFUSE_S3_MEDIA_UPLOAD_BUCKET) {
|
||||
logger.warn(
|
||||
"[seed-media] LANGFUSE_S3_MEDIA_UPLOAD_BUCKET not configured, skipping media seeding",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Trace 1: Image only (input)
|
||||
const trace1Id = MEDIA_TEST_TRACE_IDS.imageOnly;
|
||||
await uploadAndCreateMediaRecord(
|
||||
projectId,
|
||||
trace1Id,
|
||||
"input",
|
||||
MEDIA_FILES.image,
|
||||
);
|
||||
|
||||
// Trace 2: All media types
|
||||
const trace2Id = MEDIA_TEST_TRACE_IDS.allTypes;
|
||||
await uploadAndCreateMediaRecord(
|
||||
projectId,
|
||||
trace2Id,
|
||||
"input",
|
||||
MEDIA_FILES.image,
|
||||
);
|
||||
await uploadAndCreateMediaRecord(
|
||||
projectId,
|
||||
trace2Id,
|
||||
"output",
|
||||
MEDIA_FILES.pdf,
|
||||
);
|
||||
await uploadAndCreateMediaRecord(
|
||||
projectId,
|
||||
trace2Id,
|
||||
"metadata",
|
||||
MEDIA_FILES.audio,
|
||||
);
|
||||
|
||||
// Trace 3: All media types with ChatML format (pretty-rendered)
|
||||
const trace3Id = MEDIA_TEST_TRACE_IDS.allTypesChatML;
|
||||
await uploadAndCreateMediaRecord(
|
||||
projectId,
|
||||
trace3Id,
|
||||
"input",
|
||||
MEDIA_FILES.image,
|
||||
);
|
||||
await uploadAndCreateMediaRecord(
|
||||
projectId,
|
||||
trace3Id,
|
||||
"output",
|
||||
MEDIA_FILES.pdf,
|
||||
);
|
||||
await uploadAndCreateMediaRecord(
|
||||
projectId,
|
||||
trace3Id,
|
||||
"metadata",
|
||||
MEDIA_FILES.audio,
|
||||
);
|
||||
|
||||
logger.info("[seed-media] Media seeding completed");
|
||||
}
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
PrismaClient,
|
||||
type Project,
|
||||
ScoreConfigCategoryDomain,
|
||||
ScoreDataType,
|
||||
ScoreDataTypeEnum,
|
||||
type ScoreDataTypeType,
|
||||
} from "../../src/index";
|
||||
import { getDisplaySecretKey, hashSecretKey, logger } from "../../src/server";
|
||||
import { redis } from "../../src/server/redis/redis";
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
generateEvalTraceId,
|
||||
} from "./utils/seed-helpers";
|
||||
import { seedDatasetVersions } from "./seed-dataset-versions";
|
||||
import { seedMediaTraces } from "./seed-media";
|
||||
|
||||
const options = {
|
||||
environment: { type: "string" },
|
||||
@@ -349,6 +351,9 @@ async function main() {
|
||||
await createDashboardsAndWidgets([project1, project2]);
|
||||
await seedDatasetVersions(prisma, [project1.id, project2.id]);
|
||||
|
||||
// Seed media test traces (uploads to MinIO + creates Media/TraceMedia records)
|
||||
await seedMediaTraces(project1.id);
|
||||
|
||||
await prisma.llmSchema.createMany({
|
||||
data: [
|
||||
{
|
||||
@@ -536,6 +541,8 @@ export async function createDatasets(
|
||||
datasetItemIds.push(itemId);
|
||||
|
||||
// Create dataset items in versioned format with all required fields
|
||||
// Set validFrom to 30 days ago so experiment runs can reference this version
|
||||
const validFrom = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
itemsToCreate.push({
|
||||
id: itemId,
|
||||
projectId,
|
||||
@@ -546,7 +553,7 @@ export async function createDatasets(
|
||||
expectedOutput: item.output,
|
||||
metadata: Math.random() > 0.5 ? { key: "value" } : undefined,
|
||||
status: "ACTIVE" as const,
|
||||
validFrom: new Date(),
|
||||
validFrom,
|
||||
isDeleted: false,
|
||||
});
|
||||
}
|
||||
@@ -586,6 +593,121 @@ export async function createDatasets(
|
||||
update: {},
|
||||
});
|
||||
}
|
||||
|
||||
// Create multiple versions for test-dataset-versioning
|
||||
if (datasetName === "test-dataset-versioning") {
|
||||
const itemId = `test-version-item-${projectId.slice(-8)}`;
|
||||
const baseTime = new Date("2025-01-20T10:00:00Z");
|
||||
|
||||
// Version 1: Initial version
|
||||
const v1Time = new Date(baseTime.getTime());
|
||||
await prisma.datasetItem.create({
|
||||
data: {
|
||||
id: itemId,
|
||||
projectId,
|
||||
datasetId: dataset.id,
|
||||
sourceTraceId: null,
|
||||
sourceObservationId: null,
|
||||
input: { color: "red" },
|
||||
expectedOutput: "#FF0000",
|
||||
metadata: { version: "1", description: "Initial version" },
|
||||
status: "ACTIVE",
|
||||
validFrom: v1Time,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Version 2: Updated output (2 hours later)
|
||||
const v2Time = new Date(baseTime.getTime() + 2 * 60 * 60 * 1000);
|
||||
await prisma.$executeRaw`
|
||||
UPDATE dataset_items
|
||||
SET valid_to = ${v2Time}
|
||||
WHERE project_id = ${projectId}
|
||||
AND dataset_id = ${dataset.id}
|
||||
AND id = ${itemId}
|
||||
AND valid_from = ${v1Time}
|
||||
`;
|
||||
|
||||
await prisma.datasetItem.create({
|
||||
data: {
|
||||
id: itemId,
|
||||
projectId,
|
||||
datasetId: dataset.id,
|
||||
sourceTraceId: null,
|
||||
sourceObservationId: null,
|
||||
input: { color: "red" },
|
||||
expectedOutput: "#FF0001", // Slightly different
|
||||
metadata: {
|
||||
version: "2",
|
||||
description: "Fixed hex value precision",
|
||||
},
|
||||
status: "ACTIVE",
|
||||
validFrom: v2Time,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Version 3: Updated input and output (1 day later)
|
||||
const v3Time = new Date(baseTime.getTime() + 24 * 60 * 60 * 1000);
|
||||
await prisma.$executeRaw`
|
||||
UPDATE dataset_items
|
||||
SET valid_to = ${v3Time}
|
||||
WHERE project_id = ${projectId}
|
||||
AND dataset_id = ${dataset.id}
|
||||
AND id = ${itemId}
|
||||
AND valid_from = ${v2Time}
|
||||
`;
|
||||
|
||||
await prisma.datasetItem.create({
|
||||
data: {
|
||||
id: itemId,
|
||||
projectId,
|
||||
datasetId: dataset.id,
|
||||
sourceTraceId: null,
|
||||
sourceObservationId: null,
|
||||
input: { color: "crimson" }, // Changed input
|
||||
expectedOutput: "#DC143C",
|
||||
metadata: {
|
||||
version: "3",
|
||||
description: "Changed to crimson color",
|
||||
},
|
||||
status: "ACTIVE",
|
||||
validFrom: v3Time,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Version 4: Current version (2 days after initial)
|
||||
const v4Time = new Date(baseTime.getTime() + 2 * 24 * 60 * 60 * 1000);
|
||||
await prisma.$executeRaw`
|
||||
UPDATE dataset_items
|
||||
SET valid_to = ${v4Time}
|
||||
WHERE project_id = ${projectId}
|
||||
AND dataset_id = ${dataset.id}
|
||||
AND id = ${itemId}
|
||||
AND valid_from = ${v3Time}
|
||||
`;
|
||||
|
||||
await prisma.datasetItem.create({
|
||||
data: {
|
||||
id: itemId,
|
||||
projectId,
|
||||
datasetId: dataset.id,
|
||||
sourceTraceId: null,
|
||||
sourceObservationId: null,
|
||||
input: { color: "blue" }, // Changed to blue
|
||||
expectedOutput: "#0000FF",
|
||||
metadata: { version: "4", description: "Changed to blue color" },
|
||||
status: "ACTIVE",
|
||||
validFrom: v4Time,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`Created 4 versions for test-dataset-versioning in project ${projectId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -753,7 +875,7 @@ async function generateConfigsForProject(projects: Project[]) {
|
||||
{
|
||||
name: string;
|
||||
id: string;
|
||||
dataType: ScoreDataType;
|
||||
dataType: ScoreDataTypeType;
|
||||
categories: ScoreConfigCategoryDomain[] | null;
|
||||
}[]
|
||||
> = new Map();
|
||||
@@ -803,7 +925,7 @@ async function generateConfigs(project: Project) {
|
||||
const configNameAndId: {
|
||||
name: string;
|
||||
id: string;
|
||||
dataType: ScoreDataType;
|
||||
dataType: ScoreDataTypeType;
|
||||
categories: ScoreConfigCategoryDomain[] | null;
|
||||
}[] = [];
|
||||
|
||||
@@ -811,7 +933,7 @@ async function generateConfigs(project: Project) {
|
||||
{
|
||||
id: `config-${v4()}`,
|
||||
name: "manual-score",
|
||||
dataType: ScoreDataType.NUMERIC,
|
||||
dataType: ScoreDataTypeEnum.NUMERIC,
|
||||
projectId: project.id,
|
||||
isArchived: false,
|
||||
},
|
||||
@@ -819,7 +941,7 @@ async function generateConfigs(project: Project) {
|
||||
id: `config-${v4()}`,
|
||||
projectId: project.id,
|
||||
name: "Accuracy",
|
||||
dataType: ScoreDataType.CATEGORICAL,
|
||||
dataType: ScoreDataTypeEnum.CATEGORICAL,
|
||||
categories: [
|
||||
{ label: "Incorrect", value: 0 },
|
||||
{ label: "Partially Correct", value: 1 },
|
||||
@@ -831,7 +953,7 @@ async function generateConfigs(project: Project) {
|
||||
id: `config-${v4()}`,
|
||||
projectId: project.id,
|
||||
name: "Toxicity",
|
||||
dataType: ScoreDataType.BOOLEAN,
|
||||
dataType: ScoreDataTypeEnum.BOOLEAN,
|
||||
categories: [
|
||||
{ label: "True", value: 1 },
|
||||
{ label: "False", value: 0 },
|
||||
@@ -880,7 +1002,7 @@ async function generateQueuesForProject(
|
||||
{
|
||||
name: string;
|
||||
id: string;
|
||||
dataType: ScoreDataType;
|
||||
dataType: ScoreDataTypeType;
|
||||
categories: ScoreConfigCategoryDomain[] | null;
|
||||
}[]
|
||||
>,
|
||||
@@ -904,7 +1026,7 @@ async function generateQueues(
|
||||
configIdsAndNames: {
|
||||
name: string;
|
||||
id: string;
|
||||
dataType: ScoreDataType;
|
||||
dataType: ScoreDataTypeType;
|
||||
categories: ScoreConfigCategoryDomain[] | null;
|
||||
}[],
|
||||
) {
|
||||
|
||||
@@ -40,7 +40,7 @@ await orchestrator.createSyntheticData(projectIds, config);
|
||||
### 1. Dataset Experiment Data
|
||||
|
||||
- **Purpose**: Realistic experiment traces based on actual datasets
|
||||
- **Environment**: `langfuse-prompt-experiments`
|
||||
- **Environment**: `langfuse-prompt-experiment`
|
||||
- **Structure**: Each dataset item links to a trace with a single generation observation
|
||||
- **ID Pattern**: `trace-dataset-{datasetName}-{itemIndex}-{projectId}-{runNumber}`
|
||||
|
||||
|
||||
@@ -195,7 +195,11 @@ export class ClickHouseQueryBuilder {
|
||||
start_time AS event_ts,
|
||||
0 AS is_deleted,
|
||||
'' AS usage_pricing_tier_id,
|
||||
'' AS usage_pricing_tier_name
|
||||
'' AS usage_pricing_tier_name,
|
||||
map() AS tool_definitions,
|
||||
[] AS tool_calls,
|
||||
[] AS tool_call_names
|
||||
|
||||
FROM numbers(${totalObservations});
|
||||
`;
|
||||
}
|
||||
@@ -250,7 +254,8 @@ export class ClickHouseQueryBuilder {
|
||||
timestamp AS updated_at,
|
||||
timestamp AS event_ts,
|
||||
0 AS is_deleted,
|
||||
NULL AS execution_trace_id
|
||||
NULL AS execution_trace_id,
|
||||
'' AS long_string_value
|
||||
FROM numbers(${totalScores});
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
* Generates realistic test data for traces, observations, and scores.
|
||||
*
|
||||
* Use generateXxxTraces() for creating different data types:
|
||||
* - generateDatasetTrace(): For dataset experiment runs (langfuse-prompt-experiments env)
|
||||
* - generateDatasetTrace(): For dataset experiment runs (langfuse-prompt-experiment env)
|
||||
* - generateEvaluationTraces(): For evaluation data (langfuse-evaluation env)
|
||||
* - generateSyntheticTraces(): For large-scale synthetic data (default env)
|
||||
*/
|
||||
@@ -151,8 +151,8 @@ export class DataGenerator {
|
||||
name: `dataset-run-item-${uuidv4()}`,
|
||||
input: traceInput,
|
||||
output: traceOutput,
|
||||
environment: "langfuse-prompt-experiments",
|
||||
metadata: { experimentType: "langfuse-prompt-experiments" },
|
||||
environment: "langfuse-prompt-experiment",
|
||||
metadata: { experimentType: "langfuse-prompt-experiment" },
|
||||
public: false,
|
||||
bookmarked: false,
|
||||
session_id: null,
|
||||
@@ -212,7 +212,7 @@ export class DataGenerator {
|
||||
total: Math.round(totalCost * 100000) / 100000,
|
||||
},
|
||||
total_cost: Math.round(totalCost * 100000) / 100000,
|
||||
environment: "langfuse-prompt-experiments",
|
||||
environment: "langfuse-prompt-experiment",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ export class DataGenerator {
|
||||
string_value: undefined,
|
||||
data_type: "NUMERIC",
|
||||
source: "API",
|
||||
environment: "langfuse-prompt-experiments",
|
||||
environment: "langfuse-prompt-experiment",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ export class DataGenerator {
|
||||
string_value: undefined,
|
||||
data_type: "NUMERIC",
|
||||
source: "API",
|
||||
environment: "langfuse-prompt-experiments",
|
||||
environment: "langfuse-prompt-experiment",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -759,6 +759,8 @@ export class DataGenerator {
|
||||
);
|
||||
|
||||
// 6. GENERATION - Final summary generation
|
||||
const toolObservation = observations[observations.length - 1];
|
||||
const toolName = toolObservation.name || "WebSearchTool";
|
||||
observations.push(
|
||||
createObservation({
|
||||
id: `${traceId}-generation`,
|
||||
@@ -800,6 +802,14 @@ export class DataGenerator {
|
||||
output: this.randomInt(35, 45) / 100000,
|
||||
total: this.randomInt(50, 70) / 100000,
|
||||
},
|
||||
tool_definitions: {
|
||||
WebSearchTool: "Search the web for information",
|
||||
CalculatorTool: "Perform mathematical calculations",
|
||||
WeatherForecastTool: "Get weather forecasts and data",
|
||||
EmailSenderTool: "Send emails to recipients",
|
||||
},
|
||||
tool_calls: ["call_workflow_1"],
|
||||
tool_call_names: [toolName],
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1121,6 +1131,16 @@ export class DataGenerator {
|
||||
updated_at: end,
|
||||
event_ts: start,
|
||||
is_deleted: 0,
|
||||
tool_definitions: d.tool
|
||||
? {
|
||||
"billing.lookup":
|
||||
"Look up billing records and transaction history",
|
||||
"billing.refund":
|
||||
"Issue refunds for duplicate or erroneous charges",
|
||||
}
|
||||
: undefined,
|
||||
tool_calls: d.tool ? [`call_${index}`] : undefined,
|
||||
tool_call_names: d.tool ? [d.tool.name] : undefined,
|
||||
};
|
||||
|
||||
if (!d.tool) return [baseGen];
|
||||
@@ -1157,6 +1177,8 @@ export class DataGenerator {
|
||||
updated_at: start - 5,
|
||||
event_ts: start - 40,
|
||||
is_deleted: 0,
|
||||
tool_definitions: undefined,
|
||||
tool_calls: undefined,
|
||||
};
|
||||
|
||||
return [toolObs, baseGen];
|
||||
@@ -1184,6 +1206,7 @@ export class DataGenerator {
|
||||
config_id: null,
|
||||
data_type: "NUMERIC",
|
||||
string_value: null,
|
||||
long_string_value: "",
|
||||
queue_id: null,
|
||||
created_at: baseTs,
|
||||
updated_at: baseTs,
|
||||
@@ -1203,6 +1226,7 @@ export class DataGenerator {
|
||||
environment: "default",
|
||||
name: "safe",
|
||||
value: safeVal,
|
||||
long_string_value: "",
|
||||
source: "API",
|
||||
comment: "Content safety",
|
||||
metadata: {},
|
||||
@@ -1230,6 +1254,7 @@ export class DataGenerator {
|
||||
observation_id: null,
|
||||
environment: "default",
|
||||
name: "resolved",
|
||||
long_string_value: "",
|
||||
value: 1,
|
||||
source: "API",
|
||||
comment: "Conversation resolved",
|
||||
|
||||
@@ -182,6 +182,10 @@ const SEED_DATASET_ITEMS_GREETINGS = [
|
||||
{ input: { language: "German" }, output: "Guten Tag" },
|
||||
];
|
||||
|
||||
const SEED_DATASET_ITEMS_VERSION_TEST = [
|
||||
{ input: { color: "red" }, output: "#FF0000" },
|
||||
];
|
||||
|
||||
export const SEED_DATASETS = [
|
||||
{
|
||||
name: "demo-countries-dataset",
|
||||
@@ -238,6 +242,15 @@ export const SEED_DATASETS = [
|
||||
items: SEED_DATASET_ITEMS_GREETINGS,
|
||||
shouldRunExperiment: false,
|
||||
},
|
||||
{
|
||||
name: "test-dataset-versioning",
|
||||
description: "Test dataset with multiple versions of a single item",
|
||||
metadata: {
|
||||
purpose: "testing dataset versioning feature",
|
||||
},
|
||||
items: SEED_DATASET_ITEMS_VERSION_TEST,
|
||||
shouldRunExperiment: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Prompts
|
||||
|
||||
@@ -7,8 +7,10 @@ import { DataGenerator } from "./data-generators";
|
||||
import { ClickHouseQueryBuilder } from "./clickhouse-builder";
|
||||
import { FrameworkTraceLoader } from "./framework-traces/framework-trace-loader";
|
||||
import { EVAL_TRACE_COUNT, SEED_DATASETS } from "./postgres-seed-constants";
|
||||
import { MEDIA_TEST_TRACE_IDS } from "../seed-media";
|
||||
import {
|
||||
clickhouseClient,
|
||||
createTrace,
|
||||
DatasetRunItemRecordInsertType,
|
||||
logger,
|
||||
ObservationRecordInsertType,
|
||||
@@ -29,7 +31,7 @@ const DATASET_RUN_SCORE_NAMES = [
|
||||
* Orchestrates seeding operations across ClickHouse and PostgreSQL.
|
||||
*
|
||||
* Use createXxxData() for specific data types:
|
||||
* - createDatasetExperimentData(): Dataset runs in langfuse-prompt-experiments env
|
||||
* - createDatasetExperimentData(): Dataset runs in langfuse-prompt-experiment env
|
||||
* - createEvaluationData(): Evaluation data in langfuse-evaluation env
|
||||
* - createSyntheticData(): Large synthetic data in default env
|
||||
* - executeFullSeed(): All data types together
|
||||
@@ -338,6 +340,9 @@ export class SeederOrchestrator {
|
||||
// create traces from real examples for each framework source
|
||||
await this.createFrameworkTraces(projectIds);
|
||||
|
||||
// Create traces for media attachment testing
|
||||
await this.createMediaTestTraces(projectIds);
|
||||
|
||||
// Log completion statistics (commented out to reduce terminal noise)
|
||||
await this.logStatistics();
|
||||
|
||||
@@ -448,4 +453,106 @@ export class SeederOrchestrator {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates test traces for media attachment testing (JSON Beta view).
|
||||
* Use for: Testing media rendering in the trace detail view.
|
||||
*/
|
||||
async createMediaTestTraces(projectIds: string[]): Promise<void> {
|
||||
logger.info(
|
||||
`Creating media test traces for ${projectIds.length} projects.`,
|
||||
);
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
for (const projectId of projectIds) {
|
||||
logger.info(`Processing media test traces for project ${projectId}`);
|
||||
|
||||
const traces: TraceRecordInsertType[] = [
|
||||
// Trace 1: Image only (in input)
|
||||
createTrace({
|
||||
id: MEDIA_TEST_TRACE_IDS.imageOnly,
|
||||
project_id: projectId,
|
||||
name: "Media Test: Image Only",
|
||||
timestamp: now,
|
||||
input: JSON.stringify({
|
||||
message: "This trace has an image attachment in input",
|
||||
description: "Used for testing media rendering in JSON Beta view",
|
||||
}),
|
||||
output: JSON.stringify({ status: "success" }),
|
||||
metadata: { test_type: "media", media_types: "image" },
|
||||
tags: ["media-test", "image"],
|
||||
environment: "default",
|
||||
}),
|
||||
// Trace 2: All media types
|
||||
createTrace({
|
||||
id: MEDIA_TEST_TRACE_IDS.allTypes,
|
||||
project_id: projectId,
|
||||
name: "Media Test: All Types",
|
||||
timestamp: now + 1000,
|
||||
input: JSON.stringify({
|
||||
message: "This trace has an image in input",
|
||||
description: "Testing image attachment",
|
||||
}),
|
||||
output: JSON.stringify({
|
||||
message: "This trace has a PDF in output",
|
||||
description: "Testing PDF attachment",
|
||||
}),
|
||||
metadata: {
|
||||
message: "This trace has audio in metadata",
|
||||
description: "Testing audio attachment",
|
||||
test_type: "media",
|
||||
media_types: "image,pdf,audio",
|
||||
},
|
||||
tags: ["media-test", "all-types"],
|
||||
environment: "default",
|
||||
}),
|
||||
// Trace 3: All media types with ChatML format (pretty-rendered)
|
||||
createTrace({
|
||||
id: MEDIA_TEST_TRACE_IDS.allTypesChatML,
|
||||
project_id: projectId,
|
||||
name: "Media Test: All Types (ChatML)",
|
||||
timestamp: now + 2000,
|
||||
input: JSON.stringify([
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"You are a helpful assistant that can analyze images, documents, and audio files.",
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
"Please analyze the attached image and describe what you see.",
|
||||
},
|
||||
]),
|
||||
output: JSON.stringify([
|
||||
{
|
||||
role: "assistant",
|
||||
content:
|
||||
"I can see the Langfuse logo in the image. It appears to be a modern, clean design with distinctive branding elements. The attached PDF contains additional documentation about the Bitcoin whitepaper.",
|
||||
},
|
||||
]),
|
||||
metadata: {
|
||||
message: "This trace has audio in metadata",
|
||||
description: "Testing audio attachment with ChatML format",
|
||||
test_type: "media",
|
||||
media_types: "image,pdf,audio",
|
||||
format: "chatml",
|
||||
},
|
||||
tags: ["media-test", "all-types", "chatml"],
|
||||
environment: "default",
|
||||
}),
|
||||
];
|
||||
|
||||
try {
|
||||
await this.queryBuilder.executeTracesInsert(traces);
|
||||
logger.info(
|
||||
`✓ Created ${traces.length} media test traces for project ${projectId}`,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(`✗ Media test traces insert failed:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
// disable lint as this is exported and used in packages
|
||||
export enum ModelUsageUnit {
|
||||
Characters = "CHARACTERS",
|
||||
|
||||
@@ -3,7 +3,6 @@ import { FilterState } from "../types";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export enum TriggerEventSource {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
Prompt = "prompt",
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export const DatasetRunItemSchema = z.object({
|
||||
datasetItemInput: jsonSchema,
|
||||
datasetItemExpectedOutput: jsonSchema,
|
||||
datasetItemMetadata: MetadataDomain,
|
||||
datasetItemVersion: z.date().nullable(),
|
||||
});
|
||||
|
||||
// Conditional type for dataset run item domain with optional IO
|
||||
|
||||
@@ -93,6 +93,10 @@ export const ObservationSchema = z.object({
|
||||
// pricing tier information
|
||||
usagePricingTierId: z.string().nullable(),
|
||||
usagePricingTierName: z.string().nullable(),
|
||||
// tool data
|
||||
toolDefinitions: z.record(z.string(), z.string()).nullable(),
|
||||
toolCalls: z.array(z.string()).nullable(),
|
||||
toolCallNames: z.array(z.string()).nullable(),
|
||||
});
|
||||
|
||||
export type Observation = z.infer<typeof ObservationSchema>;
|
||||
@@ -105,6 +109,7 @@ export type ObservationCoreFields = Pick<
|
||||
export const EventsObservationSchema = ObservationSchema.extend({
|
||||
userId: z.string().nullable(),
|
||||
sessionId: z.string().nullable(),
|
||||
traceName: z.string().nullable(),
|
||||
});
|
||||
|
||||
export type EventsObservation = z.infer<typeof EventsObservationSchema>;
|
||||
|
||||
@@ -10,15 +10,19 @@ export const ScoreSourceEnum = {
|
||||
export const ScoreSourceDomain = z.enum(ScoreSourceArray);
|
||||
export type ScoreSourceType = z.infer<typeof ScoreSourceDomain>;
|
||||
|
||||
export const CORRECTION_NAME = "output" as const;
|
||||
|
||||
export const ScoreDataTypeArray = [
|
||||
"NUMERIC",
|
||||
"CATEGORICAL",
|
||||
"BOOLEAN",
|
||||
"CORRECTION",
|
||||
] as const;
|
||||
export const ScoreDataTypeEnum = {
|
||||
NUMERIC: "NUMERIC",
|
||||
CATEGORICAL: "CATEGORICAL",
|
||||
BOOLEAN: "BOOLEAN",
|
||||
CORRECTION: "CORRECTION",
|
||||
} as const;
|
||||
export const ScoreDataTypeDomain = z.enum(ScoreDataTypeArray);
|
||||
export type ScoreDataTypeType = z.infer<typeof ScoreDataTypeDomain>;
|
||||
@@ -38,6 +42,11 @@ export const BooleanData = z.object({
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
});
|
||||
|
||||
const CorrectionData = z.object({
|
||||
stringValue: z.null(),
|
||||
dataType: z.literal("CORRECTION"),
|
||||
});
|
||||
|
||||
// Only used for backwards compatibility with old score API schemas
|
||||
export const ScoreSchemaExclReferencesAndDates = z.object({
|
||||
// Core identifiers
|
||||
@@ -70,11 +79,43 @@ const ScoreFoundationSchema = ScoreSchemaExclReferencesAndDates.and(
|
||||
sessionId: z.string().nullable(),
|
||||
datasetRunId: z.string().nullable(),
|
||||
observationId: z.string().nullable(),
|
||||
longStringValue: z.string().default(""),
|
||||
}),
|
||||
);
|
||||
|
||||
export const ScoreSchema = ScoreFoundationSchema.and(
|
||||
z.discriminatedUnion("dataType", [NumericData, CategoricalData, BooleanData]),
|
||||
z.discriminatedUnion("dataType", [
|
||||
NumericData,
|
||||
CategoricalData,
|
||||
BooleanData,
|
||||
CorrectionData,
|
||||
]),
|
||||
);
|
||||
|
||||
export type ScoreDomain = z.infer<typeof ScoreSchema>;
|
||||
|
||||
export type ScoreByDataType<T extends ScoreDataTypeType> = ScoreDomain & {
|
||||
dataType: T;
|
||||
};
|
||||
|
||||
export type ScoresByDataTypes<T extends readonly ScoreDataTypeType[]> =
|
||||
T extends readonly (infer U)[]
|
||||
? U extends ScoreDataTypeType
|
||||
? ScoreByDataType<U>
|
||||
: never
|
||||
: never;
|
||||
|
||||
// Aggregatable score types - used in most read queries to exclude CORRECTION scores
|
||||
export const AGGREGATABLE_SCORE_TYPES = [
|
||||
"NUMERIC",
|
||||
"BOOLEAN",
|
||||
"CATEGORICAL",
|
||||
] as const satisfies readonly ScoreDataTypeType[];
|
||||
|
||||
export type AggregatableScoreDataType =
|
||||
(typeof AGGREGATABLE_SCORE_TYPES)[number];
|
||||
|
||||
// Type helper for functions that return only aggregatable scores
|
||||
export type AggregatableScore = ScoresByDataTypes<
|
||||
typeof AGGREGATABLE_SCORE_TYPES
|
||||
>;
|
||||
|
||||
@@ -3,13 +3,15 @@ import { orderBy } from "../interfaces/orderBy";
|
||||
import z from "zod/v4";
|
||||
|
||||
export enum TableViewPresetTableName {
|
||||
Traces = "traces", // eslint-disable-line no-unused-vars
|
||||
Observations = "observations", // eslint-disable-line no-unused-vars
|
||||
Scores = "scores", // eslint-disable-line no-unused-vars
|
||||
Sessions = "sessions", // eslint-disable-line no-unused-vars
|
||||
Datasets = "datasets", // eslint-disable-line no-unused-vars
|
||||
Traces = "traces",
|
||||
Observations = "observations",
|
||||
Scores = "scores",
|
||||
Sessions = "sessions",
|
||||
SessionDetail = "session-detail",
|
||||
Datasets = "datasets",
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const TableViewPresetDomainSchema = z.object({
|
||||
id: z.string(),
|
||||
projectId: z.string().nullable(),
|
||||
|
||||
@@ -51,6 +51,7 @@ const EnvSchema = z.object({
|
||||
LANGFUSE_CACHE_PROMPT_TTL_SECONDS: z.coerce.number().default(300), // 5 minutes
|
||||
CLICKHOUSE_URL: z.string().url(),
|
||||
CLICKHOUSE_READ_ONLY_URL: z.string().url().optional(),
|
||||
CLICKHOUSE_EVENTS_READ_ONLY_URL: z.string().url().optional(),
|
||||
CLICKHOUSE_CLUSTER_NAME: z.string().default("default"),
|
||||
CLICKHOUSE_DB: z.string().default("default"),
|
||||
CLICKHOUSE_USER: z.string(),
|
||||
@@ -60,6 +61,11 @@ const EnvSchema = z.object({
|
||||
// Optional to allow for server-setting fallbacks
|
||||
CLICKHOUSE_ASYNC_INSERT_MAX_DATA_SIZE: z.string().optional(),
|
||||
CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MS: z.coerce.number().int().optional(),
|
||||
CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MIN_MS: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(50)
|
||||
.optional(),
|
||||
CLICKHOUSE_LIGHTWEIGHT_DELETE_MODE: z
|
||||
.enum(["alter_update", "lightweight_update", "lightweight_update_force"])
|
||||
.default("alter_update"),
|
||||
@@ -192,7 +198,7 @@ const EnvSchema = z.object({
|
||||
}
|
||||
|
||||
return map;
|
||||
} catch (err) {
|
||||
} catch {
|
||||
return new Map<string, number>();
|
||||
}
|
||||
}),
|
||||
@@ -277,9 +283,34 @@ const EnvSchema = z.object({
|
||||
LANGFUSE_DATASET_SERVICE_READ_FROM_VERSIONED_IMPLEMENTATION: z
|
||||
.enum(["true", "false"])
|
||||
.default("true"),
|
||||
|
||||
// EE License
|
||||
LANGFUSE_EE_LICENSE_KEY: z.string().optional(),
|
||||
|
||||
// Ingestion Masking (EE feature)
|
||||
LANGFUSE_INGESTION_MASKING_CALLBACK_URL: z.string().url().optional(),
|
||||
LANGFUSE_INGESTION_MASKING_CALLBACK_TIMEOUT_MS: z.coerce
|
||||
.number()
|
||||
.positive()
|
||||
.default(500),
|
||||
LANGFUSE_INGESTION_MASKING_CALLBACK_FAIL_CLOSED: z
|
||||
.enum(["true", "false"])
|
||||
.default("false"),
|
||||
LANGFUSE_INGESTION_MASKING_MAX_RETRIES: z.coerce
|
||||
.number()
|
||||
.nonnegative()
|
||||
.default(1),
|
||||
LANGFUSE_INGESTION_MASKING_PROPAGATED_HEADERS: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((s) =>
|
||||
s ? s.split(",").map((h) => h.toLowerCase().trim()) : [],
|
||||
),
|
||||
});
|
||||
|
||||
export const env: z.infer<typeof EnvSchema> =
|
||||
export type SharedEnv = z.infer<typeof EnvSchema>;
|
||||
|
||||
export const env: SharedEnv =
|
||||
process.env.DOCKER_BUILD === "1" // eslint-disable-line turbo/no-undeclared-env-vars
|
||||
? (process.env as any)
|
||||
: EnvSchema.parse(removeEmptyEnvVariables(process.env));
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { BaseError } from "./BaseError";
|
||||
|
||||
export class NotImplementedError extends BaseError {
|
||||
constructor(description = "Not Implemented") {
|
||||
super("NotImplementedError", 501, description, true);
|
||||
}
|
||||
}
|
||||
@@ -8,3 +8,4 @@ export { ApiError } from "./ApiError";
|
||||
export { InternalServerError } from "./InternalServerError";
|
||||
export { LangfuseConflictError } from "./ConflictError";
|
||||
export { ServiceUnavailableError } from "./ServiceUnavailableError";
|
||||
export { NotImplementedError } from "./NotImplementedError";
|
||||
|
||||
@@ -73,6 +73,14 @@ export const eventsTableCols: ColumnDefinition[] = [
|
||||
internal: "e.session_id",
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Trace Name",
|
||||
id: "traceName",
|
||||
type: "stringOptions",
|
||||
internal: "e.trace_name",
|
||||
options: [], // to be added at runtime
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Level",
|
||||
id: "level",
|
||||
@@ -235,4 +243,34 @@ export const eventsTableCols: ColumnDefinition[] = [
|
||||
type: "string",
|
||||
internal: "", // handled by comment filter helpers
|
||||
},
|
||||
{
|
||||
name: "Has Parent Observation",
|
||||
id: "hasParentObservation",
|
||||
type: "boolean",
|
||||
internal: "e.parent_span_id != ''",
|
||||
},
|
||||
{
|
||||
name: "Experiment Dataset ID",
|
||||
id: "experimentDatasetId",
|
||||
type: "stringOptions",
|
||||
internal: "e.experiment_dataset_id",
|
||||
options: [], // to be added at runtime
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Experiment ID",
|
||||
id: "experimentId",
|
||||
type: "stringOptions",
|
||||
internal: "e.experiment_id",
|
||||
options: [], // to be added at runtime
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Experiment Name",
|
||||
id: "experimentName",
|
||||
type: "stringOptions",
|
||||
internal: "e.experiment_name",
|
||||
options: [], // to be added at runtime
|
||||
nullable: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Export source options for analytics integrations (PostHog, Mixpanel, Blob Storage)
|
||||
// This is a client-safe file that can be imported from @langfuse/shared
|
||||
|
||||
import { AnalyticsIntegrationExportSource } from "@prisma/client";
|
||||
|
||||
export const EXPORT_SOURCE_OPTIONS: Array<{
|
||||
value: AnalyticsIntegrationExportSource;
|
||||
label: string;
|
||||
description: string;
|
||||
}> = [
|
||||
{
|
||||
value: "TRACES_OBSERVATIONS" as const,
|
||||
label: "Traces and observations (legacy)",
|
||||
description:
|
||||
"Export traces, observations and scores. This is the legacy behavior prior to tracking traces and observations in separate tables. It is recommended to use the enriched observations option instead.",
|
||||
},
|
||||
{
|
||||
value: "TRACES_OBSERVATIONS_EVENTS" as const,
|
||||
label: "Traces and observations (legacy) and enriched observations",
|
||||
description:
|
||||
"Export traces, observations, scores and enriched observations. This exports both the legacy data source (traces, observations) and the new one (enriched observations) and essentially exports duplicate data. Therefore, it should only be used to migrate existing integrations to the new recommended enriched observations and check validity of the data for downstream consumers of the export data.",
|
||||
},
|
||||
{
|
||||
value: "EVENTS" as const,
|
||||
label: "Enriched observations (recommended)",
|
||||
description:
|
||||
"Export enriched observations and scores. This is the recommended data source for integrations and will be the default for new integrations.",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type ExportSourceOption = (typeof EXPORT_SOURCE_OPTIONS)[number];
|
||||
export type ExportSourceValue = ExportSourceOption["value"];
|
||||
@@ -1,10 +1,11 @@
|
||||
import { JSONPath } from "jsonpath-plus";
|
||||
import { set } from "lodash";
|
||||
import set from "lodash/set";
|
||||
import type {
|
||||
FieldMappingConfig,
|
||||
SourceField,
|
||||
AddToDatasetMapping,
|
||||
} from "./addToDatasetTypes";
|
||||
import { parseJsonPrioritised } from "../../utils/json";
|
||||
|
||||
type ObservationData = {
|
||||
input: unknown;
|
||||
@@ -37,9 +38,14 @@ export function testJsonPath(props: { jsonPath: string; data: unknown }): {
|
||||
*/
|
||||
export function evaluateJsonPath(data: unknown, jsonPath: string): unknown {
|
||||
try {
|
||||
const parsed = typeof data === "string" ? JSON.parse(data) : data;
|
||||
const results = JSONPath({ path: jsonPath, json: parsed });
|
||||
return results?.[0];
|
||||
const parsed = typeof data === "string" ? parseJsonPrioritised(data) : data;
|
||||
const result = JSONPath({
|
||||
path: jsonPath,
|
||||
json: parsed as string | object,
|
||||
wrap: false,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { orderBy } from "../../interfaces/orderBy";
|
||||
import { BatchTableNames } from "../../interfaces/tableNames";
|
||||
import { TracingSearchType } from "../../interfaces/search";
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
export enum BatchActionType {
|
||||
Create = "create",
|
||||
Delete = "delete",
|
||||
|
||||
@@ -8,17 +8,17 @@ import { BatchTableNames } from "../../interfaces/tableNames";
|
||||
import { TracingSearchType } from "../../interfaces/search";
|
||||
|
||||
export enum BatchExportStatus {
|
||||
QUEUED = "QUEUED", // eslint-disable-line no-unused-vars
|
||||
PROCESSING = "PROCESSING", // eslint-disable-line no-unused-vars
|
||||
COMPLETED = "COMPLETED", // eslint-disable-line no-unused-vars
|
||||
FAILED = "FAILED", // eslint-disable-line no-unused-vars
|
||||
CANCELLED = "CANCELLED", // eslint-disable-line no-unused-vars
|
||||
QUEUED = "QUEUED",
|
||||
PROCESSING = "PROCESSING",
|
||||
COMPLETED = "COMPLETED",
|
||||
FAILED = "FAILED",
|
||||
CANCELLED = "CANCELLED",
|
||||
}
|
||||
|
||||
export enum BatchExportFileFormat {
|
||||
JSON = "JSON", // eslint-disable-line no-unused-vars
|
||||
CSV = "CSV", // eslint-disable-line no-unused-vars
|
||||
JSONL = "JSONL", // eslint-disable-line no-unused-vars
|
||||
JSON = "JSON",
|
||||
CSV = "CSV",
|
||||
JSONL = "JSONL",
|
||||
}
|
||||
|
||||
// Use shared BatchTableNames enum for consistency across batch operations
|
||||
|
||||
@@ -2,19 +2,73 @@ import { z } from "zod/v4";
|
||||
|
||||
const MAX_COMMENT_LENGTH = 5000;
|
||||
|
||||
const COMMENT_OBJECT_TYPES = [
|
||||
export const COMMENT_OBJECT_TYPES = [
|
||||
"TRACE",
|
||||
"OBSERVATION",
|
||||
"SESSION",
|
||||
"PROMPT",
|
||||
] as const;
|
||||
|
||||
export const CreateCommentData = z.object({
|
||||
projectId: z.string(),
|
||||
content: z.string().trim().min(1).max(MAX_COMMENT_LENGTH),
|
||||
objectId: z.string(),
|
||||
objectType: z.enum(COMMENT_OBJECT_TYPES),
|
||||
});
|
||||
export const COMMENT_DATA_FIELDS = ["input", "output", "metadata"] as const;
|
||||
|
||||
// JSON Path validation
|
||||
// TODO: simplify
|
||||
// Allows: $ (root), $.foo, $[0], $.foo[0].bar, $['key-with-dash'], etc.
|
||||
export const jsonPathSchema = z
|
||||
.string()
|
||||
.regex(
|
||||
/^\$(\.[a-zA-Z_][a-zA-Z0-9_-]*|\[\d+\]|\['[^']+'\]|\["[^"]+"\])*$/,
|
||||
"Invalid JSON Path syntax",
|
||||
);
|
||||
|
||||
export const CreateCommentData = z
|
||||
.object({
|
||||
projectId: z.string(),
|
||||
content: z.string().trim().min(1).max(MAX_COMMENT_LENGTH),
|
||||
objectId: z.string(),
|
||||
objectType: z.enum(COMMENT_OBJECT_TYPES),
|
||||
// Optional inline positioning (parallel arrays)
|
||||
dataField: z.enum(COMMENT_DATA_FIELDS).nullish(),
|
||||
path: z.array(jsonPathSchema).nullish(),
|
||||
rangeStart: z.array(z.number().int().min(0)).nullish(),
|
||||
rangeEnd: z.array(z.number().int().min(1)).nullish(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const hasDataField = data.dataField != null;
|
||||
const hasPath = data.path != null && data.path.length > 0;
|
||||
const hasRangeStart =
|
||||
data.rangeStart != null && data.rangeStart.length > 0;
|
||||
const hasRangeEnd = data.rangeEnd != null && data.rangeEnd.length > 0;
|
||||
|
||||
// All must be set together, or all must be null/empty
|
||||
if (hasDataField || hasPath || hasRangeStart || hasRangeEnd) {
|
||||
return (
|
||||
hasDataField &&
|
||||
hasPath &&
|
||||
hasRangeStart &&
|
||||
hasRangeEnd &&
|
||||
data.path!.length === data.rangeStart!.length &&
|
||||
data.path!.length === data.rangeEnd!.length
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message:
|
||||
"dataField, path, rangeStart, rangeEnd must all be set together with matching lengths, or all be null/empty",
|
||||
},
|
||||
)
|
||||
.refine(
|
||||
(data) => {
|
||||
// Validate each range: start must be < end
|
||||
if (data.rangeStart && data.rangeEnd) {
|
||||
return data.rangeStart.every((start, i) => start < data.rangeEnd![i]);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ message: "Each rangeStart must be less than corresponding rangeEnd" },
|
||||
);
|
||||
|
||||
export const DeleteCommentData = z.object({
|
||||
projectId: z.string(),
|
||||
@@ -22,3 +76,5 @@ export const DeleteCommentData = z.object({
|
||||
objectId: z.string(),
|
||||
objectType: z.enum(COMMENT_OBJECT_TYPES),
|
||||
});
|
||||
|
||||
export type CreateCommentInput = z.infer<typeof CreateCommentData>;
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
import { z } from "zod/v4";
|
||||
import { DEFAULT_TRACE_ENVIRONMENT } from "../../server/ingestion/types";
|
||||
import { type EventRecordBaseType } from "../../server/repositories/definitions";
|
||||
import { ObservationLevel, ObservationType } from "../../domain";
|
||||
import { SingleValueOption } from "../../tableDefinitions";
|
||||
import { ColumnDefinition } from "../../tableDefinitions";
|
||||
import { formatColumnOptions } from "../../tableDefinitions/typeHelpers";
|
||||
|
||||
const flexibleUsageCostSchema = z.record(z.string(), z.number().nullable());
|
||||
|
||||
export const observationForEvalSchema = z.object({
|
||||
// Identifiers
|
||||
span_id: z.string(),
|
||||
trace_id: z.string(),
|
||||
project_id: z.string(),
|
||||
parent_span_id: z.string().nullish(),
|
||||
|
||||
// Core properties
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
environment: z.string().default(DEFAULT_TRACE_ENVIRONMENT),
|
||||
version: z.string().nullish(),
|
||||
level: z.string().default(ObservationLevel.DEFAULT),
|
||||
status_message: z.string().nullish(),
|
||||
|
||||
// Trace-level properties
|
||||
trace_name: z.string().nullish(),
|
||||
user_id: z.string().nullish(),
|
||||
session_id: z.string().nullish(),
|
||||
tags: z.array(z.string()).default([]),
|
||||
release: z.string().nullish(),
|
||||
|
||||
// Model
|
||||
provided_model_name: z.string().nullish(),
|
||||
model_parameters: z.unknown().nullish(),
|
||||
|
||||
// Prompt
|
||||
prompt_id: z.string().nullish(),
|
||||
prompt_name: z.string().nullish(),
|
||||
// Accepts string, number, or any other type from ingestion
|
||||
prompt_version: z.union([z.string().nullish(), z.number().nullish()]),
|
||||
|
||||
// Usage & Cost - accepts number values directly from ingestion
|
||||
provided_usage_details: flexibleUsageCostSchema,
|
||||
provided_cost_details: flexibleUsageCostSchema,
|
||||
usage_details: flexibleUsageCostSchema,
|
||||
cost_details: flexibleUsageCostSchema,
|
||||
|
||||
// Tool calls
|
||||
tool_definitions: z.record(z.string(), z.unknown()).default({}),
|
||||
tool_calls: z.array(z.unknown()).default([]),
|
||||
tool_call_names: z.array(z.string()).default([]),
|
||||
|
||||
// Experiment
|
||||
experiment_id: z.string().nullish(),
|
||||
experiment_name: z.string().nullish(),
|
||||
experiment_description: z.string().nullish(),
|
||||
experiment_dataset_id: z.string().nullish(),
|
||||
experiment_item_id: z.string().nullish(),
|
||||
experiment_item_expected_output: z.string().nullish(),
|
||||
experiment_item_root_span_id: z.string().nullish(),
|
||||
|
||||
// Data - accepts any type (string, array, object) from different OTEL SDKs
|
||||
input: z.unknown().nullish(),
|
||||
output: z.unknown().nullish(),
|
||||
metadata: z.record(z.string(), z.unknown()).nullish(),
|
||||
});
|
||||
|
||||
export type ObservationForEval = z.infer<typeof observationForEvalSchema>;
|
||||
|
||||
export function convertEventRecordToObservationForEval(
|
||||
record: EventRecordBaseType,
|
||||
): ObservationForEval {
|
||||
return observationForEvalSchema.parse(record);
|
||||
}
|
||||
|
||||
export type ObservationEvalFilterColumnInternal =
|
||||
/** Column identifier (must match an ObservationForEval field name) */
|
||||
keyof Pick<
|
||||
ObservationForEval,
|
||||
| "type"
|
||||
| "name"
|
||||
| "environment"
|
||||
| "level"
|
||||
| "version"
|
||||
| "trace_name"
|
||||
| "user_id"
|
||||
| "session_id"
|
||||
| "tags"
|
||||
| "experiment_dataset_id"
|
||||
| "metadata"
|
||||
>;
|
||||
|
||||
export type ObservationEvalMappingColumnInternal = keyof Pick<
|
||||
ObservationForEval,
|
||||
"input" | "output" | "metadata" | "experiment_item_expected_output"
|
||||
>;
|
||||
|
||||
export interface ObservationEvalVariableColumn {
|
||||
/** Column identifier (must match an ObservationForEval field name) */
|
||||
id: string;
|
||||
/** Display name for UI */
|
||||
name: string;
|
||||
/** Description for UI tooltips */
|
||||
description: string;
|
||||
/** Optional type hint for special handling (e.g., stringObject for metadata) */
|
||||
type?: "stringObject";
|
||||
internal: ObservationEvalMappingColumnInternal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Columns available for variable extraction in observation-based evals.
|
||||
* These are the fields that can be mapped to template variables.
|
||||
*
|
||||
* When configuring an eval, users can map these columns to template
|
||||
* variables like {{input}}, {{output}}, {{expected_output}}, etc.
|
||||
*/
|
||||
export const observationEvalVariableColumns: ObservationEvalVariableColumn[] = [
|
||||
{
|
||||
id: "input",
|
||||
name: "Input",
|
||||
description: "Observation input data",
|
||||
internal: "input",
|
||||
},
|
||||
{
|
||||
id: "output",
|
||||
name: "Output",
|
||||
description: "Observation output data",
|
||||
internal: "output",
|
||||
},
|
||||
{
|
||||
id: "metadata",
|
||||
name: "Metadata",
|
||||
description: "Observation metadata",
|
||||
type: "stringObject",
|
||||
internal: "metadata",
|
||||
},
|
||||
{
|
||||
id: "experimentItemExpectedOutput",
|
||||
name: "Expected Output",
|
||||
description: "Expected output from experiment item",
|
||||
internal: "experiment_item_expected_output",
|
||||
},
|
||||
];
|
||||
|
||||
export const availableObservationEvalVariableColumns = [
|
||||
...observationEvalVariableColumns,
|
||||
{
|
||||
id: "toolCalls",
|
||||
name: "Tool Calls",
|
||||
description: "Tool calls",
|
||||
internal: "tool_calls",
|
||||
},
|
||||
{
|
||||
id: "toolDefinitions",
|
||||
name: "Tool Definitions",
|
||||
description: "Tool definitions",
|
||||
internal: "tool_definitions",
|
||||
},
|
||||
{
|
||||
id: "toolCallNames",
|
||||
name: "Tool Call Names",
|
||||
description: "Tool call names",
|
||||
internal: "tool_call_names",
|
||||
},
|
||||
{
|
||||
id: "providedModelName",
|
||||
name: "Model",
|
||||
description: "Model",
|
||||
internal: "provided_model_name",
|
||||
},
|
||||
{
|
||||
id: "modelParameters",
|
||||
name: "Model Parameters",
|
||||
description: "Model parameters",
|
||||
internal: "model_parameters",
|
||||
},
|
||||
{
|
||||
id: "usageDetails",
|
||||
name: "Usage Details",
|
||||
description: "Usage details",
|
||||
internal: "usage_details",
|
||||
},
|
||||
{
|
||||
id: "costDetails",
|
||||
name: "Cost Details",
|
||||
description: "Cost details",
|
||||
internal: "cost_details",
|
||||
},
|
||||
];
|
||||
|
||||
type ObservationEvalColumnDef = ColumnDefinition & {
|
||||
internal: ObservationEvalFilterColumnInternal;
|
||||
};
|
||||
|
||||
/**
|
||||
* Columns available for filtering in observation-based evals.
|
||||
*
|
||||
* These columns can be used in filter conditions to determine
|
||||
* which observations should be evaluated.
|
||||
*/
|
||||
export const observationEvalFilterColumns: ObservationEvalColumnDef[] = [
|
||||
{
|
||||
name: "Type",
|
||||
id: "type",
|
||||
type: "stringOptions",
|
||||
internal: "type",
|
||||
options: Object.values(ObservationType).map((key) => ({ value: key })),
|
||||
},
|
||||
{
|
||||
name: "Name",
|
||||
id: "name",
|
||||
type: "stringOptions",
|
||||
internal: "name",
|
||||
options: [], // to be filled at runtime
|
||||
},
|
||||
{
|
||||
name: "Environment",
|
||||
id: "environment",
|
||||
type: "stringOptions",
|
||||
internal: "environment",
|
||||
options: [], // to be filled at runtime
|
||||
},
|
||||
{
|
||||
name: "Level",
|
||||
id: "level",
|
||||
type: "stringOptions",
|
||||
internal: "level",
|
||||
options: Object.values(ObservationLevel).map((key) => ({ value: key })),
|
||||
},
|
||||
{
|
||||
name: "Version",
|
||||
id: "version",
|
||||
type: "string",
|
||||
internal: "version",
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Trace Name",
|
||||
id: "traceName",
|
||||
type: "stringOptions",
|
||||
internal: "trace_name",
|
||||
options: [], // to be filled at runtime
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "User ID",
|
||||
id: "userId",
|
||||
type: "string",
|
||||
internal: "user_id",
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Session ID",
|
||||
id: "sessionId",
|
||||
type: "string",
|
||||
internal: "session_id",
|
||||
nullable: true,
|
||||
},
|
||||
{
|
||||
name: "Tags",
|
||||
id: "tags",
|
||||
type: "arrayOptions",
|
||||
internal: "tags",
|
||||
options: [], // to be filled at runtime
|
||||
},
|
||||
{
|
||||
name: "Metadata",
|
||||
id: "metadata",
|
||||
type: "stringObject",
|
||||
internal: "metadata",
|
||||
},
|
||||
];
|
||||
|
||||
export const experimentEvalFilterColumns: ObservationEvalColumnDef[] = [
|
||||
{
|
||||
name: "Dataset",
|
||||
id: "experimentDatasetId",
|
||||
type: "stringOptions",
|
||||
internal: "experiment_dataset_id",
|
||||
options: [], // to be filled at runtime
|
||||
},
|
||||
];
|
||||
|
||||
export const eventsEvalFilterColumns: ObservationEvalColumnDef[] = [
|
||||
...observationEvalFilterColumns,
|
||||
...experimentEvalFilterColumns,
|
||||
];
|
||||
|
||||
// Options type for observation eval filters
|
||||
export type ObservationEvalOptions = {
|
||||
environment?: Array<SingleValueOption>;
|
||||
tags?: Array<SingleValueOption>;
|
||||
traceName?: Array<SingleValueOption>;
|
||||
name?: Array<SingleValueOption>;
|
||||
};
|
||||
|
||||
export type ExperimentEvalOptions = {
|
||||
experimentDatasetId?: Array<SingleValueOption>;
|
||||
};
|
||||
|
||||
export function observationEvalFilterColsWithOptions(
|
||||
options?: ObservationEvalOptions,
|
||||
cols: ColumnDefinition[] = observationEvalFilterColumns,
|
||||
): ColumnDefinition[] {
|
||||
return cols.map((col) => {
|
||||
if (col.id === "environment") {
|
||||
return formatColumnOptions(col, options?.environment ?? []);
|
||||
}
|
||||
if (col.id === "tags") {
|
||||
return formatColumnOptions(col, options?.tags ?? []);
|
||||
}
|
||||
if (col.id === "traceName") {
|
||||
return formatColumnOptions(col, options?.traceName ?? []);
|
||||
}
|
||||
if (col.id === "name") {
|
||||
return formatColumnOptions(col, options?.name ?? []);
|
||||
}
|
||||
return col;
|
||||
});
|
||||
}
|
||||
|
||||
export function experimentEvalFilterColsWithOptions(
|
||||
options?: ExperimentEvalOptions,
|
||||
cols: ColumnDefinition[] = experimentEvalFilterColumns,
|
||||
): ColumnDefinition[] {
|
||||
return cols.map((col) => {
|
||||
if (col.id === "experimentDatasetId") {
|
||||
return formatColumnOptions(col, options?.experimentDatasetId ?? []);
|
||||
}
|
||||
return col;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Field mapper for observation eval filters.
|
||||
* Maps camelCase filter column IDs to snake_case observation fields.
|
||||
* Based on events table column definitions.
|
||||
*
|
||||
* @param observation - The observation data object
|
||||
* @param column - The camelCase column ID from filter definitions
|
||||
* @returns The value from the observation object
|
||||
*/
|
||||
export function mapEventEvalFilterColumnIdToField(
|
||||
observation: ObservationForEval,
|
||||
column: string,
|
||||
) {
|
||||
const columnMapping = eventsEvalFilterColumns.find((c) => c.id === column);
|
||||
if (!columnMapping) {
|
||||
return undefined;
|
||||
}
|
||||
return observation[columnMapping.internal];
|
||||
}
|
||||
@@ -1,5 +1,17 @@
|
||||
import z from "zod/v4";
|
||||
|
||||
export const EvalTargetObject = {
|
||||
TRACE: "trace",
|
||||
DATASET: "dataset",
|
||||
EVENT: "event",
|
||||
EXPERIMENT: "experiment",
|
||||
} as const;
|
||||
|
||||
export type EvalTargetObject =
|
||||
(typeof EvalTargetObject)[keyof typeof EvalTargetObject];
|
||||
|
||||
export const EvalTargetObjectSchema = z.enum(Object.values(EvalTargetObject));
|
||||
|
||||
export const langfuseObjects = [
|
||||
"trace",
|
||||
"span",
|
||||
@@ -23,25 +35,32 @@ export const variableMapping = z
|
||||
.object({
|
||||
templateVariable: z.string(), // variable name in the template
|
||||
// name of the observation to extract the variable from
|
||||
// not required for trace, as we only have one.
|
||||
// not required for trace or dataset_item, as we only have one of each.
|
||||
objectName: z.string().nullish(),
|
||||
langfuseObject: langfuseObject,
|
||||
selectedColumnId: z.string(),
|
||||
jsonSelector: z.string().nullish(),
|
||||
})
|
||||
.refine(
|
||||
(value) => value.langfuseObject === "trace" || value.objectName !== null,
|
||||
(value) =>
|
||||
value.langfuseObject === "trace" ||
|
||||
value.langfuseObject === "dataset_item" ||
|
||||
value.objectName !== null,
|
||||
{
|
||||
message: "objectName is required for langfuseObjects other than trace",
|
||||
message:
|
||||
"objectName is required for observation objects (generation, span, score)",
|
||||
},
|
||||
);
|
||||
|
||||
export const variableMappingList = z.array(variableMapping);
|
||||
|
||||
// WIP version for forms - langfuseObject optional to support both:
|
||||
// - Trace/Dataset evals: Include langfuseObject and objectName to specify which observation
|
||||
// - Event/Experiment evals: Omit them since the observation is already selected
|
||||
export const wipVariableMapping = z.object({
|
||||
templateVariable: z.string(),
|
||||
objectName: z.string().nullish(),
|
||||
langfuseObject: langfuseObject,
|
||||
langfuseObject: langfuseObject.optional(),
|
||||
selectedColumnId: z.string().nullish(),
|
||||
jsonSelector: z.string().nullish(),
|
||||
});
|
||||
@@ -157,3 +176,19 @@ export const JobTimeScopeZod = z.enum(["NEW", "EXISTING"]);
|
||||
export type JobTimeScope = z.infer<typeof JobTimeScopeZod>;
|
||||
|
||||
export const TimeScopeSchema = z.array(JobTimeScopeZod).default(["NEW"]);
|
||||
|
||||
// Simplified variable mapping for observation-based evals.
|
||||
// Unlike trace-based evals, we don't need objectName since we're directly
|
||||
// targeting a specific observation - no need to specify which observation to extract from.
|
||||
export const observationVariableMapping = z.object({
|
||||
templateVariable: z.string(), // variable name in the template
|
||||
selectedColumnId: z.string(), // column to extract (must match observationEvalVariableColumns.id)
|
||||
jsonSelector: z.string().nullish(), // optional JSON path selector
|
||||
});
|
||||
|
||||
export const observationVariableMappingList = z.array(
|
||||
observationVariableMapping,
|
||||
);
|
||||
export type ObservationVariableMapping = z.infer<
|
||||
typeof observationVariableMapping
|
||||
>;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import z from "zod/v4";
|
||||
import { JSONPath } from "jsonpath-plus";
|
||||
import { variableMapping } from "./types";
|
||||
|
||||
/**
|
||||
* Parses an unknown value to a string representation
|
||||
@@ -67,10 +65,11 @@ function parseJsonDefault(selectedColumn: unknown, jsonSelector: string) {
|
||||
|
||||
export function extractValueFromObject(
|
||||
obj: Record<string, unknown>,
|
||||
mapping: z.infer<typeof variableMapping>,
|
||||
parseJson?: (selectedColumn: unknown, jsonSelector: string) => unknown, // eslint-disable-line no-unused-vars
|
||||
selectedColumnId: string,
|
||||
jsonSelector?: string,
|
||||
parseJson?: (selectedColumn: unknown, jsonSelector: string) => unknown,
|
||||
): { value: string; error: Error | null } {
|
||||
let selectedColumn = obj[mapping.selectedColumnId];
|
||||
let selectedColumn = obj[selectedColumnId];
|
||||
|
||||
// Simple preprocessing: attempt to parse to valid JSON object
|
||||
if (typeof selectedColumn === "string") {
|
||||
@@ -82,9 +81,9 @@ export function extractValueFromObject(
|
||||
let jsonSelectedColumn;
|
||||
let error: Error | null = null;
|
||||
|
||||
if (mapping.jsonSelector && selectedColumn) {
|
||||
if (jsonSelector && selectedColumn) {
|
||||
try {
|
||||
jsonSelectedColumn = jsonParser(selectedColumn, mapping.jsonSelector);
|
||||
jsonSelectedColumn = jsonParser(selectedColumn, jsonSelector);
|
||||
} catch (err) {
|
||||
error =
|
||||
err instanceof Error
|
||||
|
||||
@@ -14,9 +14,8 @@ export const SingleChatMessageSchema = PromptChatMessageSchema;
|
||||
export type SingleChatMessage = z.infer<typeof SingleChatMessageSchema>;
|
||||
|
||||
export enum PromptType {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
Chat = "chat",
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
Text = "text",
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
*/
|
||||
export const filterAndValidateDbScoreConfigList = (
|
||||
scoreConfigs: ScoreConfigDbType[],
|
||||
onParseError?: (error: z.ZodError) => void, // eslint-disable-line no-unused-vars
|
||||
onParseError?: (error: z.ZodError) => void,
|
||||
): ScoreConfigDomain[] =>
|
||||
scoreConfigs.reduce((acc, ts) => {
|
||||
const result = ScoreConfigSchema.safeParse(ts);
|
||||
|
||||
@@ -7,9 +7,14 @@ import {
|
||||
ScoreDataTypeDomain,
|
||||
ScoreSourceDomain,
|
||||
} from "../../../../domain/scores";
|
||||
import { singleFilter } from "../../../../interfaces/filters";
|
||||
import { InvalidRequestError } from "../../../../errors";
|
||||
|
||||
const operators = ["<", ">", "<=", ">=", "!=", "="] as const;
|
||||
|
||||
export const SCORE_FIELD_GROUPS = ["score", "trace"] as const;
|
||||
export type ScoreFieldGroup = (typeof SCORE_FIELD_GROUPS)[number];
|
||||
|
||||
/**
|
||||
* Endpoints
|
||||
*/
|
||||
@@ -41,6 +46,31 @@ export const GetScoresQuery = z.object({
|
||||
message: "Each score ID must be a string",
|
||||
})
|
||||
.nullish(),
|
||||
fields: z
|
||||
.string()
|
||||
.nullish()
|
||||
.transform((v) => {
|
||||
if (!v) return null;
|
||||
return v
|
||||
.split(",")
|
||||
.map((f) => f.trim())
|
||||
.filter((f) => SCORE_FIELD_GROUPS.includes(f as ScoreFieldGroup));
|
||||
})
|
||||
.pipe(z.array(z.enum(SCORE_FIELD_GROUPS)).nullable()),
|
||||
filter: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((str) => {
|
||||
if (!str) return undefined;
|
||||
try {
|
||||
const parsed = JSON.parse(str);
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
if (e instanceof InvalidRequestError) throw e;
|
||||
throw new InvalidRequestError("Invalid JSON in filter parameter");
|
||||
}
|
||||
})
|
||||
.pipe(z.array(singleFilter).optional()),
|
||||
});
|
||||
|
||||
// POST /scores
|
||||
@@ -69,6 +99,11 @@ export const PostScoresBody = applyScoreValidation(
|
||||
dataType: z.literal("BOOLEAN"),
|
||||
configId: z.string().nullish(),
|
||||
}),
|
||||
z.object({
|
||||
value: z.string(), // Corrected output text
|
||||
dataType: z.literal("CORRECTION"),
|
||||
configId: z.undefined().nullish(), // Cannot have config
|
||||
}),
|
||||
z.object({
|
||||
value: z.union([z.string(), z.number()]),
|
||||
dataType: z.undefined(),
|
||||
|
||||
@@ -11,7 +11,7 @@ import { GetScoreResponseDataV1 } from "./endpoints";
|
||||
*/
|
||||
export const filterAndValidateV1GetScoreList = (
|
||||
scores: unknown[],
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
onParseError?: (error: z.ZodError) => void,
|
||||
): z.infer<typeof GetScoreResponseDataV1>[] =>
|
||||
scores.reduce(
|
||||
|
||||
@@ -12,19 +12,28 @@ export const GetScoresQueryV2 = GetScoresQuery.extend({
|
||||
sessionId: z.string().nullish(),
|
||||
traceId: z.string().nullish(),
|
||||
datasetRunId: z.string().nullish(),
|
||||
observationId: z
|
||||
.string()
|
||||
.transform((str) => str.split(",").map((id) => id.trim()))
|
||||
.refine((arr) => arr.every((id) => typeof id === "string"), {
|
||||
message: "Each observation ID must be a string",
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
export const GetScoreResponseDataV2 = z.intersection(
|
||||
APIScoreSchemaV2,
|
||||
z.object({
|
||||
trace: z
|
||||
.object({
|
||||
userId: z.string().nullish(),
|
||||
tags: z.array(z.string()).nullish(),
|
||||
environment: z.string().nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
}),
|
||||
);
|
||||
export const GetScoreResponseDataV2 = z
|
||||
.intersection(
|
||||
APIScoreSchemaV2,
|
||||
z.object({
|
||||
trace: z
|
||||
.object({
|
||||
userId: z.string().nullish(),
|
||||
tags: z.array(z.string()).nullish(),
|
||||
environment: z.string().nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
}),
|
||||
)
|
||||
.or(APIScoreSchemaV2);
|
||||
|
||||
export const GetScoresResponseV2 = z.object({
|
||||
data: z.array(GetScoreResponseDataV2),
|
||||
|
||||
@@ -6,6 +6,11 @@ import {
|
||||
} from "../../../../../domain";
|
||||
import z from "zod/v4";
|
||||
|
||||
const CorrectionData = z.object({
|
||||
stringValue: z.string(),
|
||||
dataType: z.literal("CORRECTION"),
|
||||
});
|
||||
|
||||
/**
|
||||
* Foundation schema for scores API v2 i.e. trace, observation AND session scores
|
||||
*
|
||||
@@ -25,7 +30,12 @@ const ScoreFoundationSchemaV2 = ScoreSchemaExclReferencesAndDates.extend({
|
||||
});
|
||||
|
||||
export const APIScoreSchemaV2 = ScoreFoundationSchemaV2.and(
|
||||
z.discriminatedUnion("dataType", [NumericData, CategoricalData, BooleanData]),
|
||||
z.discriminatedUnion("dataType", [
|
||||
NumericData,
|
||||
CategoricalData,
|
||||
BooleanData,
|
||||
CorrectionData,
|
||||
]),
|
||||
);
|
||||
|
||||
export type APIScoreV2 = z.infer<typeof APIScoreSchemaV2>;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user