traicebox 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +256 -0
- package/package.json +3 -3
- package/dist/compose-1b8sxndd.yml +0 -266
- package/dist/config-tt3vhpk0.yaml +0 -101
- package/dist/server-d2v9t7c1.ts +0 -257
- package/dist/server-k3xr84w3.ts +0 -259
- package/dist/traicebox-7pysd22b.yaml +0 -2
package/README.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Traicebox
|
|
2
|
+
|
|
3
|
+
**Traicebox** is a zero-config local developer stack for tracing and session tracking around LLM and AI model workflows. It enables developers to have a working local tracing and inspection setup within minutes.
|
|
4
|
+
|
|
5
|
+
### Key Use Cases
|
|
6
|
+
|
|
7
|
+
- **Tool & Prompt Development**: Debug plugins, skills, or prompts for harness tools like [OpenCode](https://opencode.ai/) with full visibility.
|
|
8
|
+
- **Model Evaluation**: Evaluate and compare local models with detailed trace logging. The built-in config generator makes setting up tools remarkably simple.
|
|
9
|
+
- **Application Development**: Build AI applications with minimal code changes. Requests routed through the built-in LiteLLM proxy automatically generate traces in Langfuse. You can group these traces into sessions by adding an `x-litellm-session-id` header to your requests.
|
|
10
|
+
|
|
11
|
+
### Included in the Stack
|
|
12
|
+
|
|
13
|
+
- **LiteLLM**: Unified proxy for LLM backends with custom session-tracking callbacks.
|
|
14
|
+
- **Langfuse**: Open-source tracing and observability for LLM applications.
|
|
15
|
+
- **Auto-login Proxies**: Dynamic proxies providing immediate, authenticated access to LiteLLM UI and Langfuse.
|
|
16
|
+
- **Harness Integration**: Automatic configuration generation for external tools like OpenCode.
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
Traicebox requires [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) to be installed and running on your system.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
### Published Package (Recommended)
|
|
25
|
+
|
|
26
|
+
You can install Traicebox via NPM or Bun:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Using Bun
|
|
30
|
+
bun install -g traicebox
|
|
31
|
+
|
|
32
|
+
# Using NPM
|
|
33
|
+
npm install -g traicebox
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Binary Release
|
|
37
|
+
|
|
38
|
+
Download the latest prebuilt binary for your platform from the [GitHub Releases](https://github.com/fardjad/traicebox/releases) page.
|
|
39
|
+
|
|
40
|
+
Once downloaded, make it executable and move it to your path:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
chmod +x traicebox-macos-arm64
|
|
44
|
+
mv traicebox-macos-arm64 /usr/local/bin/traicebox
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Shell Completion
|
|
48
|
+
|
|
49
|
+
Traicebox supports shell completion for Bash and Zsh.
|
|
50
|
+
|
|
51
|
+
### Zsh
|
|
52
|
+
|
|
53
|
+
Add the following to your `~/.zshrc`:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Enable completion if not already enabled
|
|
57
|
+
autoload -Uz compinit && compinit
|
|
58
|
+
|
|
59
|
+
source <(traicebox completion)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Bash
|
|
63
|
+
|
|
64
|
+
Add the following to your `~/.bashrc`:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
source <(traicebox completion)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
### 1. Initial Setup
|
|
73
|
+
|
|
74
|
+
Initialize the Traicebox home directory:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
traicebox setup
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 2. Import Models
|
|
81
|
+
|
|
82
|
+
Before starting the stack, you need to import your available models into LiteLLM. Traicebox requires an endpoint URL or alias to fetch models from.
|
|
83
|
+
|
|
84
|
+
Supported aliases for common local tools:
|
|
85
|
+
|
|
86
|
+
| Alias | Default Endpoint URL |
|
|
87
|
+
| :---------- | :--------------------------------- |
|
|
88
|
+
| `lm-studio` | `http://127.0.0.1:1234/v1/models` |
|
|
89
|
+
| `ollama` | `http://localhost:11434/v1/models` |
|
|
90
|
+
| `llama-cpp` | `http://localhost:8080/v1/models` |
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
traicebox models import-from-openai-api --endpoint lm-studio
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
To use a custom OpenAI-compatible endpoint URL:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
traicebox models import-from-openai-api --endpoint http://your-api:port/v1/models
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
If the endpoint requires authentication, provide the API key via the `OPENAI_COMPATIBLE_API_KEY` environment variable:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
OPENAI_COMPATIBLE_API_KEY="your-api-key" traicebox models import-from-openai-api --endpoint http://your-api:port/v1/models
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 3. Configure Harness Integration (Optional)
|
|
109
|
+
|
|
110
|
+
If you use [OpenCode](https://opencode.ai/), you can synchronize its model configuration with Traicebox.
|
|
111
|
+
|
|
112
|
+
> [!WARNING]
|
|
113
|
+
> The following command will overwrite your existing OpenCode configuration file.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
traicebox generate-harness-config opencode > ~/.config/opencode/opencode.jsonc
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 4. Start the Stack
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
traicebox start
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Once running, you can access the public endpoints:
|
|
126
|
+
|
|
127
|
+
- **LiteLLM**: `http://litellm.localhost:5483`
|
|
128
|
+
- **Langfuse**: `http://langfuse.localhost:5483`
|
|
129
|
+
|
|
130
|
+
### 5. Verify and Test
|
|
131
|
+
|
|
132
|
+
If you configured OpenCode in step 3, you can test the integration:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Refresh the models cache
|
|
136
|
+
opencode models --refresh
|
|
137
|
+
|
|
138
|
+
# Run a test prompt
|
|
139
|
+
opencode run -m 'litellm/your/model' 'Just write a greeting message!'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
> [!NOTE]
|
|
143
|
+
> Replace `'litellm/your/model'` with one of the models imported in step 2.
|
|
144
|
+
|
|
145
|
+
After running a prompt, you can inspect the captured traces in Langfuse by visiting:
|
|
146
|
+
[http://langfuse.localhost:5483/project/local-project/sessions](http://langfuse.localhost:5483/project/local-project/sessions)
|
|
147
|
+
|
|
148
|
+
There, you can select the latest session to inspect system prompts, user messages, and tool interactions.
|
|
149
|
+
|
|
150
|
+
#### Application Integration (with Session Tracking)
|
|
151
|
+
|
|
152
|
+
You can integrate Traicebox into your own applications by pointing your LLM client to the LiteLLM proxy. To group related traces together, simply include an `x-litellm-session-id` header in your requests.
|
|
153
|
+
|
|
154
|
+
Here is an example using `curl` to demonstrate session grouping:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
MODEL_NAME="your-model-name"
|
|
158
|
+
|
|
159
|
+
# First request in a session
|
|
160
|
+
curl http://litellm.localhost:5483/v1/chat/completions \
|
|
161
|
+
-H "Content-Type: application/json" \
|
|
162
|
+
-H "Authorization: Bearer sk-litellm-local-client" \
|
|
163
|
+
-H "x-litellm-session-id: my-test-session" \
|
|
164
|
+
-d "{
|
|
165
|
+
\"model\": \"$MODEL_NAME\",
|
|
166
|
+
\"messages\": [{\"role\": \"user\", \"content\": \"Hello! This is a test message to start my session.\"}]
|
|
167
|
+
}"
|
|
168
|
+
|
|
169
|
+
# Second request in the same session
|
|
170
|
+
curl http://litellm.localhost:5483/v1/chat/completions \
|
|
171
|
+
-H "Content-Type: application/json" \
|
|
172
|
+
-H "Authorization: Bearer sk-litellm-local-client" \
|
|
173
|
+
-H "x-litellm-session-id: my-test-session" \
|
|
174
|
+
-d "{
|
|
175
|
+
\"model\": \"$MODEL_NAME\",
|
|
176
|
+
\"messages\": [{\"role\": \"user\", \"content\": \"Goodbye! I am done with this test session.\"}]
|
|
177
|
+
}"
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
> [!NOTE]
|
|
181
|
+
> Set `MODEL_NAME` to one of the exact model names imported in step 2. The default API key is `sk-litellm-local-client`.
|
|
182
|
+
|
|
183
|
+
Refresh the [Langfuse sessions page](http://langfuse.localhost:5483/project/local-project/sessions) to see both traces grouped under the `my-test-session` ID.
|
|
184
|
+
|
|
185
|
+
### Other Useful Commands
|
|
186
|
+
|
|
187
|
+
| Command | Description |
|
|
188
|
+
| :----------------------- | :---------------------------------------------- |
|
|
189
|
+
| `traicebox stop` | Stop the stack. |
|
|
190
|
+
| `traicebox restart` | Recreate and restart the stack. |
|
|
191
|
+
| `traicebox destroy` | Remove the stack and delete local data volumes. |
|
|
192
|
+
| `traicebox models clear` | Clear the custom model list from LiteLLM. |
|
|
193
|
+
|
|
194
|
+
For more information on available commands and options, run `traicebox --help`.
|
|
195
|
+
|
|
196
|
+
## Configuration
|
|
197
|
+
|
|
198
|
+
Traicebox stores its configuration and data in `${TRAICEBOX_HOME}`. You can override this location by setting the `TRAICEBOX_HOME` environment variable.
|
|
199
|
+
|
|
200
|
+
By default, it is located at:
|
|
201
|
+
|
|
202
|
+
| OS | Default `${TRAICEBOX_HOME}` |
|
|
203
|
+
| :----------------- | :----------------------------------------------------- |
|
|
204
|
+
| **macOS** | `~/Library/Application Support/Traicebox` |
|
|
205
|
+
| **Windows** | `%APPDATA%\Traicebox` |
|
|
206
|
+
| **Linux / Others** | `~/.config/traicebox` (or respects `$XDG_CONFIG_HOME`) |
|
|
207
|
+
|
|
208
|
+
### `traicebox.yaml`
|
|
209
|
+
|
|
210
|
+
You can customize the stack behavior by creating or editing `${TRAICEBOX_HOME}/traicebox.yaml`.
|
|
211
|
+
|
|
212
|
+
| Option | Type | Default | Description |
|
|
213
|
+
| :----- | :------- | :---------- | :----------------------------------------------------------- |
|
|
214
|
+
| `host` | `string` | `127.0.0.1` | The host address that Traicebox services will bind to. |
|
|
215
|
+
| `port` | `number` | `5483` | The port that Traicebox services will be accessible through. |
|
|
216
|
+
|
|
217
|
+
Example:
|
|
218
|
+
|
|
219
|
+
```yaml
|
|
220
|
+
host: 127.0.0.1
|
|
221
|
+
port: 5483
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## LiteLLM
|
|
225
|
+
|
|
226
|
+
LiteLLM loads its proxy config from `${TRAICEBOX_HOME}/litellm/config.yaml`. The LiteLLM UI is available at `http://litellm.localhost:5483`, where Traicebox automatically manages your admin session for immediate access.
|
|
227
|
+
|
|
228
|
+
`${TRAICEBOX_HOME}/litellm/config.yaml` is the source of truth for model routing and upstream API wiring. Imported models are written directly into `model_list`, with `api_base` inferred from the import endpoint and `api_key` read from `os.environ/OPENAI_COMPATIBLE_API_KEY`.
|
|
229
|
+
|
|
230
|
+
`OPENAI_COMPATIBLE_API_KEY` is intentionally not persisted by Tr**ai**cebox. For local backends such as LM Studio, Ollama, or llama.cpp, no secret is needed. For authenticated upstreams, run `traicebox` through your password manager CLI so it injects `OPENAI_COMPATIBLE_API_KEY` into the process; Tr**ai**cebox will materialize that into an ephemeral Docker secret for LiteLLM only.
|
|
231
|
+
|
|
232
|
+
To replace the LiteLLM `model_list` from an OpenAI-compatible `/v1/models` endpoint (or alias like `lm-studio`, `ollama`, `llama-cpp`):
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
traicebox models import-from-openai-api --endpoint lm-studio
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
If that endpoint requires auth, inject `OPENAI_COMPATIBLE_API_KEY` into the command environment first. If it does not, the command works without a key.
|
|
239
|
+
|
|
240
|
+
To clear the LiteLLM `model_list`:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
traicebox models clear
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
To print an OpenCode config snippet for the current LiteLLM models:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
traicebox generate-harness-config opencode
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Langfuse
|
|
253
|
+
|
|
254
|
+
LiteLLM sends OTEL traces to Langfuse using the built-in `langfuse_otel` callback and `LANGFUSE_OTEL_HOST=http://langfuse-web:3000`.
|
|
255
|
+
|
|
256
|
+
Langfuse is accessible at `http://langfuse.localhost:5483`. By default, Traicebox disables public signups and automatically creates a browser session for the seeded admin user so you can start tracing immediately.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "traicebox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "A zero-config local developer stack for tracing and session tracking around LLM and AI model workflows",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Fardjad Davari <public@fardjad.com>",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"scripts": {
|
|
30
30
|
"traicebox": "TRAICEBOX_DEV=1 bun ./cli/index.ts",
|
|
31
31
|
"build": "bun build ./cli/index.ts --outdir ./dist --target node",
|
|
32
|
-
"
|
|
32
|
+
"prepack": "bun run build",
|
|
33
33
|
"test": "bun test",
|
|
34
34
|
"typecheck": "tsc",
|
|
35
35
|
"check": "biome check . && dprint check",
|
|
@@ -47,4 +47,4 @@
|
|
|
47
47
|
"yaml": "^2.8.3",
|
|
48
48
|
"yargs": "^18.0.0"
|
|
49
49
|
}
|
|
50
|
-
}
|
|
50
|
+
}
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
services:
|
|
2
|
-
db:
|
|
3
|
-
image: pgautoupgrade/pgautoupgrade:18-alpine
|
|
4
|
-
environment:
|
|
5
|
-
- POSTGRES_DB=postgres
|
|
6
|
-
- POSTGRES_USER=${POSTGRES_SUPERUSER}
|
|
7
|
-
- POSTGRES_PASSWORD=${POSTGRES_SUPERPASS}
|
|
8
|
-
- PGAUTO_DEVEL=
|
|
9
|
-
- PGAUTO_ONESHOT=no
|
|
10
|
-
- LITELLM_DB_NAME=${LITELLM_DB_NAME}
|
|
11
|
-
- LITELLM_DB_USER=${LITELLM_DB_USER}
|
|
12
|
-
- LITELLM_DB_PASSWORD=${LITELLM_DB_PASSWORD}
|
|
13
|
-
- LANGFUSE_DB_NAME=${LANGFUSE_DB_NAME}
|
|
14
|
-
- LANGFUSE_DB_USER=${LANGFUSE_DB_USER}
|
|
15
|
-
- LANGFUSE_DB_PASSWORD=${LANGFUSE_DB_PASSWORD}
|
|
16
|
-
healthcheck:
|
|
17
|
-
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_SUPERUSER} -d postgres && psql -U ${POSTGRES_SUPERUSER} -d postgres -tAc \"SELECT 1 FROM pg_database WHERE datname IN ('${LITELLM_DB_NAME}','${LANGFUSE_DB_NAME}')\" | grep -q 1"]
|
|
18
|
-
interval: 3s
|
|
19
|
-
timeout: 3s
|
|
20
|
-
retries: 20
|
|
21
|
-
start_period: 5s
|
|
22
|
-
volumes:
|
|
23
|
-
- postgres_data:/var/lib/postgresql
|
|
24
|
-
- ./postgres/init/00-app-databases.sh:/docker-entrypoint-initdb.d/00-app-databases.sh:ro
|
|
25
|
-
langfuse-clickhouse:
|
|
26
|
-
image: clickhouse/clickhouse-server
|
|
27
|
-
user: "101:101"
|
|
28
|
-
environment:
|
|
29
|
-
- CLICKHOUSE_DB=${CLICKHOUSE_DB}
|
|
30
|
-
- CLICKHOUSE_USER=${CLICKHOUSE_USER}
|
|
31
|
-
- CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD}
|
|
32
|
-
- TZ=UTC
|
|
33
|
-
healthcheck:
|
|
34
|
-
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8123/ping"]
|
|
35
|
-
interval: 5s
|
|
36
|
-
timeout: 5s
|
|
37
|
-
retries: 10
|
|
38
|
-
start_period: 1s
|
|
39
|
-
volumes:
|
|
40
|
-
- langfuse_clickhouse_data:/var/lib/clickhouse
|
|
41
|
-
- langfuse_clickhouse_logs:/var/log/clickhouse-server
|
|
42
|
-
langfuse-redis:
|
|
43
|
-
image: redis:latest
|
|
44
|
-
command:
|
|
45
|
-
- --requirepass
|
|
46
|
-
- ${LANGFUSE_REDIS_PASSWORD}
|
|
47
|
-
- --maxmemory-policy
|
|
48
|
-
- noeviction
|
|
49
|
-
healthcheck:
|
|
50
|
-
test: ["CMD", "redis-cli", "-a", "${LANGFUSE_REDIS_PASSWORD}", "ping"]
|
|
51
|
-
interval: 3s
|
|
52
|
-
timeout: 10s
|
|
53
|
-
retries: 10
|
|
54
|
-
volumes:
|
|
55
|
-
- langfuse_redis_data:/data
|
|
56
|
-
langfuse-minio:
|
|
57
|
-
image: cgr.dev/chainguard/minio
|
|
58
|
-
entrypoint: sh
|
|
59
|
-
command:
|
|
60
|
-
- -c
|
|
61
|
-
- mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data
|
|
62
|
-
environment:
|
|
63
|
-
- MINIO_ROOT_USER=${MINIO_ROOT_USER}
|
|
64
|
-
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
|
|
65
|
-
healthcheck:
|
|
66
|
-
test: ["CMD", "mc", "ready", "local"]
|
|
67
|
-
interval: 1s
|
|
68
|
-
timeout: 5s
|
|
69
|
-
retries: 5
|
|
70
|
-
start_period: 1s
|
|
71
|
-
volumes:
|
|
72
|
-
- langfuse_minio_data:/data
|
|
73
|
-
langfuse-worker:
|
|
74
|
-
image: langfuse/langfuse-worker:3
|
|
75
|
-
depends_on:
|
|
76
|
-
db:
|
|
77
|
-
condition: service_healthy
|
|
78
|
-
langfuse-web:
|
|
79
|
-
condition: service_healthy
|
|
80
|
-
langfuse-minio:
|
|
81
|
-
condition: service_healthy
|
|
82
|
-
langfuse-redis:
|
|
83
|
-
condition: service_healthy
|
|
84
|
-
langfuse-clickhouse:
|
|
85
|
-
condition: service_healthy
|
|
86
|
-
environment: &langfuse-env
|
|
87
|
-
NEXTAUTH_URL: http://langfuse.localhost:${TRAICEBOX_PORT}
|
|
88
|
-
DATABASE_URL: postgresql://${LANGFUSE_DB_USER}:${LANGFUSE_DB_PASSWORD}@db:5432/${LANGFUSE_DB_NAME}
|
|
89
|
-
SALT: ${LANGFUSE_SALT}
|
|
90
|
-
ENCRYPTION_KEY: "${LANGFUSE_ENCRYPTION_KEY}"
|
|
91
|
-
TELEMETRY_ENABLED: "${LANGFUSE_TELEMETRY_ENABLED}"
|
|
92
|
-
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: "${LANGFUSE_EXPERIMENTAL_FEATURES}"
|
|
93
|
-
CLICKHOUSE_MIGRATION_URL: clickhouse://langfuse-clickhouse:9000
|
|
94
|
-
CLICKHOUSE_URL: http://langfuse-clickhouse:8123
|
|
95
|
-
CLICKHOUSE_USER: ${CLICKHOUSE_USER}
|
|
96
|
-
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
|
|
97
|
-
CLICKHOUSE_CLUSTER_ENABLED: "false"
|
|
98
|
-
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse
|
|
99
|
-
LANGFUSE_S3_EVENT_UPLOAD_REGION: auto
|
|
100
|
-
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
|
|
101
|
-
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
|
|
102
|
-
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: http://langfuse-minio:9000
|
|
103
|
-
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true"
|
|
104
|
-
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: events/
|
|
105
|
-
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: langfuse
|
|
106
|
-
LANGFUSE_S3_MEDIA_UPLOAD_REGION: auto
|
|
107
|
-
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
|
|
108
|
-
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
|
|
109
|
-
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://langfuse-minio:9000
|
|
110
|
-
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: "true"
|
|
111
|
-
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: media/
|
|
112
|
-
REDIS_HOST: langfuse-redis
|
|
113
|
-
REDIS_PORT: "6379"
|
|
114
|
-
REDIS_AUTH: ${LANGFUSE_REDIS_PASSWORD}
|
|
115
|
-
langfuse-web:
|
|
116
|
-
image: langfuse/langfuse:3
|
|
117
|
-
depends_on:
|
|
118
|
-
db:
|
|
119
|
-
condition: service_healthy
|
|
120
|
-
langfuse-minio:
|
|
121
|
-
condition: service_healthy
|
|
122
|
-
langfuse-redis:
|
|
123
|
-
condition: service_healthy
|
|
124
|
-
langfuse-clickhouse:
|
|
125
|
-
condition: service_healthy
|
|
126
|
-
environment:
|
|
127
|
-
<<: *langfuse-env
|
|
128
|
-
NEXTAUTH_SECRET: ${LANGFUSE_NEXTAUTH_SECRET}
|
|
129
|
-
AUTH_DISABLE_SIGNUP: ${LANGFUSE_AUTH_DISABLE_SIGNUP}
|
|
130
|
-
LANGFUSE_INIT_ORG_ID: ${LANGFUSE_INIT_ORG_ID}
|
|
131
|
-
LANGFUSE_INIT_ORG_NAME: ${LANGFUSE_INIT_ORG_NAME}
|
|
132
|
-
LANGFUSE_INIT_PROJECT_ID: ${LANGFUSE_INIT_PROJECT_ID}
|
|
133
|
-
LANGFUSE_INIT_PROJECT_NAME: ${LANGFUSE_INIT_PROJECT_NAME}
|
|
134
|
-
LANGFUSE_INIT_PROJECT_PUBLIC_KEY: ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY}
|
|
135
|
-
LANGFUSE_INIT_PROJECT_SECRET_KEY: ${LANGFUSE_INIT_PROJECT_SECRET_KEY}
|
|
136
|
-
LANGFUSE_INIT_USER_EMAIL: ${LANGFUSE_INIT_USER_EMAIL}
|
|
137
|
-
LANGFUSE_INIT_USER_NAME: ${LANGFUSE_INIT_USER_NAME}
|
|
138
|
-
LANGFUSE_INIT_USER_PASSWORD: ${LANGFUSE_INIT_USER_PASSWORD}
|
|
139
|
-
healthcheck:
|
|
140
|
-
test: ["CMD", "node", "-e", "const host = process.env.HOSTNAME || 'localhost'; fetch('http://' + host + ':3000/api/auth/csrf').then(async (r) => { const ok = r.ok && (await r.text()).includes('csrfToken'); process.exit(ok ? 0 : 1); }).catch(() => process.exit(1))"]
|
|
141
|
-
interval: 5s
|
|
142
|
-
timeout: 5s
|
|
143
|
-
retries: 24
|
|
144
|
-
start_period: 20s
|
|
145
|
-
langfuse-proxy:
|
|
146
|
-
build:
|
|
147
|
-
context: .
|
|
148
|
-
dockerfile: langfuse-proxy/Dockerfile
|
|
149
|
-
depends_on:
|
|
150
|
-
langfuse-web:
|
|
151
|
-
condition: service_healthy
|
|
152
|
-
environment:
|
|
153
|
-
- PORT=3000
|
|
154
|
-
- LANGFUSE_PUBLIC_ORIGIN=http://langfuse.localhost:${TRAICEBOX_PORT}
|
|
155
|
-
- LANGFUSE_UPSTREAM_ORIGIN=http://langfuse-web:3000
|
|
156
|
-
- LANGFUSE_PROXY_AUTOLOGIN=${LANGFUSE_PROXY_AUTOLOGIN}
|
|
157
|
-
- LANGFUSE_INIT_USER_EMAIL=${LANGFUSE_INIT_USER_EMAIL}
|
|
158
|
-
- LANGFUSE_INIT_USER_PASSWORD=${LANGFUSE_INIT_USER_PASSWORD}
|
|
159
|
-
healthcheck:
|
|
160
|
-
test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:3000/readyz').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
|
|
161
|
-
interval: 5s
|
|
162
|
-
timeout: 5s
|
|
163
|
-
retries: 24
|
|
164
|
-
start_period: 10s
|
|
165
|
-
litellm:
|
|
166
|
-
image: docker.litellm.ai/berriai/litellm:main-latest
|
|
167
|
-
depends_on:
|
|
168
|
-
db:
|
|
169
|
-
condition: service_healthy
|
|
170
|
-
langfuse-web:
|
|
171
|
-
condition: service_healthy
|
|
172
|
-
environment:
|
|
173
|
-
- DATABASE_URL=postgresql://${LITELLM_DB_USER}:${LITELLM_DB_PASSWORD}@db:5432/${LITELLM_DB_NAME}
|
|
174
|
-
- LANGFUSE_PUBLIC_KEY=${LANGFUSE_INIT_PROJECT_PUBLIC_KEY}
|
|
175
|
-
- LANGFUSE_SECRET_KEY=${LANGFUSE_INIT_PROJECT_SECRET_KEY}
|
|
176
|
-
- LANGFUSE_OTEL_HOST=http://langfuse-web:3000
|
|
177
|
-
- LANGFUSE_TRACING_ENVIRONMENT=${LANGFUSE_TRACING_ENVIRONMENT}
|
|
178
|
-
- LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY}
|
|
179
|
-
extra_hosts:
|
|
180
|
-
- host.docker.internal:host-gateway
|
|
181
|
-
secrets:
|
|
182
|
-
- source: openai_compatible_api_key
|
|
183
|
-
target: openai_compatible_api_key
|
|
184
|
-
volumes:
|
|
185
|
-
- ./litellm:/app/litellm-config:ro
|
|
186
|
-
entrypoint:
|
|
187
|
-
- /bin/sh
|
|
188
|
-
- /app/litellm-config/start-litellm.sh
|
|
189
|
-
command:
|
|
190
|
-
- --config
|
|
191
|
-
- /app/litellm-config/config.yaml
|
|
192
|
-
- --log_config
|
|
193
|
-
- /app/litellm-config/logging.json
|
|
194
|
-
- --port
|
|
195
|
-
- "4000"
|
|
196
|
-
healthcheck:
|
|
197
|
-
test: ["CMD", "python3", "-c", "import sys, urllib.request; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:4000/health/readiness', timeout=5).getcode() == 200 else 1)"]
|
|
198
|
-
interval: 5s
|
|
199
|
-
timeout: 5s
|
|
200
|
-
retries: 24
|
|
201
|
-
start_period: 20s
|
|
202
|
-
litellm-ui-proxy:
|
|
203
|
-
build:
|
|
204
|
-
context: .
|
|
205
|
-
dockerfile: litellm-ui-proxy/Dockerfile
|
|
206
|
-
depends_on:
|
|
207
|
-
litellm:
|
|
208
|
-
condition: service_healthy
|
|
209
|
-
environment:
|
|
210
|
-
- PORT=4000
|
|
211
|
-
- LITELLM_UPSTREAM_ORIGIN=http://litellm:4000
|
|
212
|
-
- LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY}
|
|
213
|
-
healthcheck:
|
|
214
|
-
test: ["CMD", "bun", "-e", "fetch('http://127.0.0.1:4000/readyz').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
|
|
215
|
-
interval: 5s
|
|
216
|
-
timeout: 5s
|
|
217
|
-
retries: 24
|
|
218
|
-
start_period: 10s
|
|
219
|
-
litellm-bootstrap:
|
|
220
|
-
image: curlimages/curl:8.16.0
|
|
221
|
-
depends_on:
|
|
222
|
-
litellm:
|
|
223
|
-
condition: service_healthy
|
|
224
|
-
environment:
|
|
225
|
-
- LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY}
|
|
226
|
-
- LITELLM_CLIENT_KEY=${LITELLM_CLIENT_KEY}
|
|
227
|
-
volumes:
|
|
228
|
-
- ./litellm/bootstrap-client-key.sh:/bootstrap-client-key.sh:ro
|
|
229
|
-
entrypoint:
|
|
230
|
-
- sh
|
|
231
|
-
- /bootstrap-client-key.sh
|
|
232
|
-
caddy:
|
|
233
|
-
image: caddy:2-alpine
|
|
234
|
-
depends_on:
|
|
235
|
-
langfuse-proxy:
|
|
236
|
-
condition: service_healthy
|
|
237
|
-
litellm:
|
|
238
|
-
condition: service_healthy
|
|
239
|
-
litellm-ui-proxy:
|
|
240
|
-
condition: service_healthy
|
|
241
|
-
ports:
|
|
242
|
-
- 127.0.0.1:${TRAICEBOX_PORT}:${TRAICEBOX_PORT}
|
|
243
|
-
environment:
|
|
244
|
-
- TRAICEBOX_PORT=${TRAICEBOX_PORT}
|
|
245
|
-
volumes:
|
|
246
|
-
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
|
247
|
-
healthcheck:
|
|
248
|
-
test: ["CMD-SHELL", "wget -qO- --header='Host: litellm.localhost:${TRAICEBOX_PORT}' http://127.0.0.1:${TRAICEBOX_PORT}/health/readiness >/dev/null && wget -qO- --header='Host: litellm.localhost:${TRAICEBOX_PORT}' http://127.0.0.1:${TRAICEBOX_PORT}/ui/ >/dev/null && wget -qO- --header='Host: langfuse.localhost:${TRAICEBOX_PORT}' http://127.0.0.1:${TRAICEBOX_PORT}/readyz >/dev/null"]
|
|
249
|
-
interval: 5s
|
|
250
|
-
timeout: 5s
|
|
251
|
-
retries: 24
|
|
252
|
-
start_period: 10s
|
|
253
|
-
volumes:
|
|
254
|
-
postgres_data:
|
|
255
|
-
driver: local
|
|
256
|
-
langfuse_clickhouse_data:
|
|
257
|
-
driver: local
|
|
258
|
-
langfuse_clickhouse_logs:
|
|
259
|
-
driver: local
|
|
260
|
-
langfuse_minio_data:
|
|
261
|
-
driver: local
|
|
262
|
-
langfuse_redis_data:
|
|
263
|
-
driver: local
|
|
264
|
-
secrets:
|
|
265
|
-
openai_compatible_api_key:
|
|
266
|
-
file: ${OPENAI_COMPATIBLE_API_KEY_SECRET_FILE:-/dev/null}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
model_list:
|
|
2
|
-
- model_name: bytedance/seed-oss-36b
|
|
3
|
-
litellm_params:
|
|
4
|
-
model: openai/bytedance/seed-oss-36b
|
|
5
|
-
api_base: http://host.docker.internal:1234/v1
|
|
6
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
7
|
-
- model_name: google/gemma-4-26b-a4b
|
|
8
|
-
litellm_params:
|
|
9
|
-
model: openai/google/gemma-4-26b-a4b
|
|
10
|
-
api_base: http://host.docker.internal:1234/v1
|
|
11
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
12
|
-
- model_name: google/gemma-4-31b
|
|
13
|
-
litellm_params:
|
|
14
|
-
model: openai/google/gemma-4-31b
|
|
15
|
-
api_base: http://host.docker.internal:1234/v1
|
|
16
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
17
|
-
- model_name: google/gemma-4-e2b
|
|
18
|
-
litellm_params:
|
|
19
|
-
model: openai/google/gemma-4-e2b
|
|
20
|
-
api_base: http://host.docker.internal:1234/v1
|
|
21
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
22
|
-
- model_name: google/gemma-4-e4b
|
|
23
|
-
litellm_params:
|
|
24
|
-
model: openai/google/gemma-4-e4b
|
|
25
|
-
api_base: http://host.docker.internal:1234/v1
|
|
26
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
27
|
-
- model_name: ibm/granite-4-h-tiny
|
|
28
|
-
litellm_params:
|
|
29
|
-
model: openai/ibm/granite-4-h-tiny
|
|
30
|
-
api_base: http://host.docker.internal:1234/v1
|
|
31
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
32
|
-
- model_name: liquid/lfm2-24b-a2b
|
|
33
|
-
litellm_params:
|
|
34
|
-
model: openai/liquid/lfm2-24b-a2b
|
|
35
|
-
api_base: http://host.docker.internal:1234/v1
|
|
36
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
37
|
-
- model_name: microsoft/phi-4-reasoning-plus
|
|
38
|
-
litellm_params:
|
|
39
|
-
model: openai/microsoft/phi-4-reasoning-plus
|
|
40
|
-
api_base: http://host.docker.internal:1234/v1
|
|
41
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
42
|
-
- model_name: mistralai/devstral-small-2-2512
|
|
43
|
-
litellm_params:
|
|
44
|
-
model: openai/mistralai/devstral-small-2-2512
|
|
45
|
-
api_base: http://host.docker.internal:1234/v1
|
|
46
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
47
|
-
- model_name: mistralai/devstral-small-2507
|
|
48
|
-
litellm_params:
|
|
49
|
-
model: openai/mistralai/devstral-small-2507
|
|
50
|
-
api_base: http://host.docker.internal:1234/v1
|
|
51
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
52
|
-
- model_name: mistralai/magistral-small-2509
|
|
53
|
-
litellm_params:
|
|
54
|
-
model: openai/mistralai/magistral-small-2509
|
|
55
|
-
api_base: http://host.docker.internal:1234/v1
|
|
56
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
57
|
-
- model_name: mistralai/ministral-3-14b-reasoning
|
|
58
|
-
litellm_params:
|
|
59
|
-
model: openai/mistralai/ministral-3-14b-reasoning
|
|
60
|
-
api_base: http://host.docker.internal:1234/v1
|
|
61
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
62
|
-
- model_name: nvidia/nemotron-3-nano
|
|
63
|
-
litellm_params:
|
|
64
|
-
model: openai/nvidia/nemotron-3-nano
|
|
65
|
-
api_base: http://host.docker.internal:1234/v1
|
|
66
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
67
|
-
- model_name: openai/gpt-oss-20b
|
|
68
|
-
litellm_params:
|
|
69
|
-
model: openai/openai/gpt-oss-20b
|
|
70
|
-
api_base: http://host.docker.internal:1234/v1
|
|
71
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
72
|
-
- model_name: qwen/qwen3-30b-a3b-2507
|
|
73
|
-
litellm_params:
|
|
74
|
-
model: openai/qwen/qwen3-30b-a3b-2507
|
|
75
|
-
api_base: http://host.docker.internal:1234/v1
|
|
76
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
77
|
-
- model_name: qwen/qwen3-4b-2507
|
|
78
|
-
litellm_params:
|
|
79
|
-
model: openai/qwen/qwen3-4b-2507
|
|
80
|
-
api_base: http://host.docker.internal:1234/v1
|
|
81
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
82
|
-
- model_name: qwen/qwen3.5-35b-a3b
|
|
83
|
-
litellm_params:
|
|
84
|
-
model: openai/qwen/qwen3.5-35b-a3b
|
|
85
|
-
api_base: http://host.docker.internal:1234/v1
|
|
86
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
87
|
-
- model_name: text-embedding-nomic-embed-text-v1.5
|
|
88
|
-
litellm_params:
|
|
89
|
-
model: openai/text-embedding-nomic-embed-text-v1.5
|
|
90
|
-
api_base: http://host.docker.internal:1234/v1
|
|
91
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
92
|
-
- model_name: zai-org/glm-4.7-flash
|
|
93
|
-
litellm_params:
|
|
94
|
-
model: openai/zai-org/glm-4.7-flash
|
|
95
|
-
api_base: http://host.docker.internal:1234/v1
|
|
96
|
-
api_key: os.environ/OPENAI_COMPATIBLE_API_KEY
|
|
97
|
-
litellm_settings:
|
|
98
|
-
database_url: os.environ/DATABASE_URL
|
|
99
|
-
callbacks:
|
|
100
|
-
- request_session_metadata_callback.proxy_handler_instance
|
|
101
|
-
- langfuse_otel
|
package/dist/server-d2v9t7c1.ts
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
const upstreamOrigin = requiredEnv("LANGFUSE_UPSTREAM_ORIGIN");
|
|
2
|
-
const publicOrigin = requiredEnv("LANGFUSE_PUBLIC_ORIGIN");
|
|
3
|
-
const adminEmail = requiredEnv("LANGFUSE_INIT_USER_EMAIL");
|
|
4
|
-
const adminPassword = requiredEnv("LANGFUSE_INIT_USER_PASSWORD");
|
|
5
|
-
const autoLoginEnabled = envFlag("LANGFUSE_PROXY_AUTOLOGIN", true);
|
|
6
|
-
const port = Number(Bun.env.PORT ?? "3000");
|
|
7
|
-
const retryDelayMs = Number(Bun.env.LANGFUSE_PROXY_RETRY_DELAY_MS ?? "500");
|
|
8
|
-
const maxRetries = Number(Bun.env.LANGFUSE_PROXY_MAX_RETRIES ?? "20");
|
|
9
|
-
|
|
10
|
-
const sessionCookieNames = [
|
|
11
|
-
"__Secure-authjs.session-token",
|
|
12
|
-
"authjs.session-token",
|
|
13
|
-
"__Secure-next-auth.session-token",
|
|
14
|
-
"next-auth.session-token",
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
function requiredEnv(name: string): string {
|
|
18
|
-
const value = Bun.env[name];
|
|
19
|
-
if (!value) {
|
|
20
|
-
throw new Error(`Missing required environment variable: ${name}`);
|
|
21
|
-
}
|
|
22
|
-
return value;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function envFlag(name: string, fallback: boolean): boolean {
|
|
26
|
-
const value = Bun.env[name];
|
|
27
|
-
if (value == null || value === "") {
|
|
28
|
-
return fallback;
|
|
29
|
-
}
|
|
30
|
-
return !["0", "false", "no", "off"].includes(value.toLowerCase());
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function getSetCookies(headers: Headers): string[] {
|
|
34
|
-
const bunHeaders = headers as Headers & { getSetCookie?: () => string[] };
|
|
35
|
-
if (typeof bunHeaders.getSetCookie === "function") {
|
|
36
|
-
return bunHeaders.getSetCookie();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const singleValue = headers.get("set-cookie");
|
|
40
|
-
return singleValue ? [singleValue] : [];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function serializeCookieHeader(setCookies: string[]): string {
|
|
44
|
-
return setCookies
|
|
45
|
-
.map((cookie) => cookie.split(";", 1)[0]?.trim())
|
|
46
|
-
.filter((cookie): cookie is string => Boolean(cookie))
|
|
47
|
-
.join("; ");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function hasSessionCookie(request: Request): boolean {
|
|
51
|
-
const cookieHeader = request.headers.get("cookie");
|
|
52
|
-
if (!cookieHeader) {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return sessionCookieNames.some((name) => cookieHeader.includes(`${name}=`));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function appendSetCookies(headers: Headers, cookies: string[]): void {
|
|
60
|
-
for (const cookie of cookies) {
|
|
61
|
-
headers.append("set-cookie", cookie);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function sleep(ms: number): Promise<void> {
|
|
66
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function fetchWithRetry(url: URL, init: RequestInit): Promise<Response> {
|
|
70
|
-
let lastError: unknown;
|
|
71
|
-
|
|
72
|
-
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
|
|
73
|
-
try {
|
|
74
|
-
return await fetch(url, init);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
lastError = error;
|
|
77
|
-
if (attempt === maxRetries) {
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
await sleep(retryDelayMs);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
throw lastError;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function copyRequestHeaders(request: Request): Headers {
|
|
88
|
-
const headers = new Headers(request.headers);
|
|
89
|
-
headers.delete("host");
|
|
90
|
-
headers.delete("connection");
|
|
91
|
-
headers.delete("content-length");
|
|
92
|
-
return headers;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function loginToLangfuse(): Promise<string[]> {
|
|
96
|
-
const csrfResponse = await fetchWithRetry(
|
|
97
|
-
new URL("/api/auth/csrf", upstreamOrigin),
|
|
98
|
-
{
|
|
99
|
-
redirect: "manual",
|
|
100
|
-
},
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
if (!csrfResponse.ok) {
|
|
104
|
-
throw new Error(`Langfuse CSRF request failed with ${csrfResponse.status}`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const csrfPayload = (await csrfResponse.json()) as { csrfToken?: string };
|
|
108
|
-
if (!csrfPayload.csrfToken) {
|
|
109
|
-
throw new Error("Langfuse CSRF response did not include csrfToken");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const csrfCookies = getSetCookies(csrfResponse.headers);
|
|
113
|
-
const callbackBody = new URLSearchParams({
|
|
114
|
-
email: adminEmail,
|
|
115
|
-
password: adminPassword,
|
|
116
|
-
csrfToken: csrfPayload.csrfToken,
|
|
117
|
-
callbackUrl: publicOrigin,
|
|
118
|
-
json: "true",
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const callbackResponse = await fetchWithRetry(
|
|
122
|
-
new URL("/api/auth/callback/credentials?json=true", upstreamOrigin),
|
|
123
|
-
{
|
|
124
|
-
method: "POST",
|
|
125
|
-
redirect: "manual",
|
|
126
|
-
headers: {
|
|
127
|
-
"content-type": "application/x-www-form-urlencoded",
|
|
128
|
-
cookie: serializeCookieHeader(csrfCookies),
|
|
129
|
-
},
|
|
130
|
-
body: callbackBody.toString(),
|
|
131
|
-
},
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
if (callbackResponse.status >= 400) {
|
|
135
|
-
throw new Error(
|
|
136
|
-
`Langfuse credentials callback failed with ${callbackResponse.status}`,
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const mergedCookies = [
|
|
141
|
-
...csrfCookies,
|
|
142
|
-
...getSetCookies(callbackResponse.headers),
|
|
143
|
-
];
|
|
144
|
-
if (!serializeCookieHeader(mergedCookies)) {
|
|
145
|
-
throw new Error("Langfuse login callback did not return any cookies");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return mergedCookies;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async function checkLangfuseReadiness(): Promise<void> {
|
|
152
|
-
const loginCookies = autoLoginEnabled ? await loginToLangfuse() : [];
|
|
153
|
-
const headers = new Headers();
|
|
154
|
-
const cookieHeader = serializeCookieHeader(loginCookies);
|
|
155
|
-
if (cookieHeader) {
|
|
156
|
-
headers.set("cookie", cookieHeader);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const response = await fetchWithRetry(new URL("/", upstreamOrigin), {
|
|
160
|
-
headers,
|
|
161
|
-
redirect: "manual",
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
165
|
-
if (response.status >= 400 || !contentType.includes("text/html")) {
|
|
166
|
-
throw new Error(`Langfuse readiness failed with ${response.status}`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async function proxyRequest(
|
|
171
|
-
request: Request,
|
|
172
|
-
extraCookies: string[] = [],
|
|
173
|
-
): Promise<Response> {
|
|
174
|
-
const upstreamUrl = new URL(request.url);
|
|
175
|
-
upstreamUrl.protocol = new URL(upstreamOrigin).protocol;
|
|
176
|
-
upstreamUrl.host = new URL(upstreamOrigin).host;
|
|
177
|
-
|
|
178
|
-
const headers = copyRequestHeaders(request);
|
|
179
|
-
if (extraCookies.length > 0) {
|
|
180
|
-
const existingCookies = request.headers.get("cookie");
|
|
181
|
-
const loginCookies = serializeCookieHeader(extraCookies);
|
|
182
|
-
const combinedCookies = [existingCookies, loginCookies]
|
|
183
|
-
.filter(Boolean)
|
|
184
|
-
.join("; ");
|
|
185
|
-
if (combinedCookies) {
|
|
186
|
-
headers.set("cookie", combinedCookies);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const response = await fetchWithRetry(upstreamUrl, {
|
|
191
|
-
method: request.method,
|
|
192
|
-
headers,
|
|
193
|
-
body:
|
|
194
|
-
request.method === "GET" || request.method === "HEAD"
|
|
195
|
-
? undefined
|
|
196
|
-
: request.body,
|
|
197
|
-
redirect: "manual",
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
const responseHeaders = new Headers(response.headers);
|
|
201
|
-
responseHeaders.delete("content-encoding");
|
|
202
|
-
responseHeaders.delete("content-length");
|
|
203
|
-
|
|
204
|
-
return new Response(response.body, {
|
|
205
|
-
status: response.status,
|
|
206
|
-
statusText: response.statusText,
|
|
207
|
-
headers: responseHeaders,
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const server = Bun.serve({
|
|
212
|
-
port,
|
|
213
|
-
async fetch(request: Request): Promise<Response> {
|
|
214
|
-
const url = new URL(request.url);
|
|
215
|
-
|
|
216
|
-
if (url.pathname === "/healthz") {
|
|
217
|
-
return new Response("ok");
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (url.pathname === "/readyz") {
|
|
221
|
-
try {
|
|
222
|
-
await checkLangfuseReadiness();
|
|
223
|
-
return new Response("ok");
|
|
224
|
-
} catch (error) {
|
|
225
|
-
console.error("[langfuse-proxy] readiness check failed", error);
|
|
226
|
-
return new Response("not ready", { status: 503 });
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (!autoLoginEnabled || url.pathname.startsWith("/api/auth/")) {
|
|
231
|
-
return proxyRequest(request);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (hasSessionCookie(request)) {
|
|
235
|
-
return proxyRequest(request);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
const loginCookies = await loginToLangfuse();
|
|
240
|
-
const response = await proxyRequest(request, loginCookies);
|
|
241
|
-
const headers = new Headers(response.headers);
|
|
242
|
-
appendSetCookies(headers, loginCookies);
|
|
243
|
-
headers.set("cache-control", "no-store");
|
|
244
|
-
|
|
245
|
-
return new Response(response.body, {
|
|
246
|
-
status: response.status,
|
|
247
|
-
statusText: response.statusText,
|
|
248
|
-
headers,
|
|
249
|
-
});
|
|
250
|
-
} catch (error) {
|
|
251
|
-
console.error("[langfuse-proxy] auto-login failed", error);
|
|
252
|
-
return new Response("Langfuse auto-login failed", { status: 502 });
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
console.log(`[langfuse-proxy] listening on :${server.port}`);
|
package/dist/server-k3xr84w3.ts
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
const upstreamOrigin = requiredEnv("LITELLM_UPSTREAM_ORIGIN");
|
|
2
|
-
const masterKey = requiredEnv("LITELLM_MASTER_KEY");
|
|
3
|
-
const uiUsername = Bun.env.LITELLM_UI_USERNAME ?? "admin";
|
|
4
|
-
const port = Number(Bun.env.PORT ?? "4000");
|
|
5
|
-
const retryDelayMs = Number(Bun.env.LITELLM_UI_PROXY_RETRY_DELAY_MS ?? "500");
|
|
6
|
-
const maxRetries = Number(Bun.env.LITELLM_UI_PROXY_MAX_RETRIES ?? "20");
|
|
7
|
-
const uiDefaultFlags = [
|
|
8
|
-
"disableShowNewBadge",
|
|
9
|
-
"disableShowPrompts",
|
|
10
|
-
"disableUsageIndicator",
|
|
11
|
-
"disableBlogPosts",
|
|
12
|
-
"disableBouncingIcon",
|
|
13
|
-
] as const;
|
|
14
|
-
|
|
15
|
-
function requiredEnv(name: string): string {
|
|
16
|
-
const value = Bun.env[name];
|
|
17
|
-
if (!value) {
|
|
18
|
-
throw new Error(`Missing required environment variable: ${name}`);
|
|
19
|
-
}
|
|
20
|
-
return value;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function sleep(ms: number): Promise<void> {
|
|
24
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function fetchWithRetry(url: URL, init: RequestInit): Promise<Response> {
|
|
28
|
-
let lastError: unknown;
|
|
29
|
-
|
|
30
|
-
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
|
|
31
|
-
try {
|
|
32
|
-
return await fetch(url, init);
|
|
33
|
-
} catch (error) {
|
|
34
|
-
lastError = error;
|
|
35
|
-
if (attempt === maxRetries) {
|
|
36
|
-
break;
|
|
37
|
-
}
|
|
38
|
-
await sleep(retryDelayMs);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
throw lastError;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function copyRequestHeaders(request: Request): Headers {
|
|
46
|
-
const headers = new Headers(request.headers);
|
|
47
|
-
headers.delete("host");
|
|
48
|
-
headers.delete("connection");
|
|
49
|
-
headers.delete("content-length");
|
|
50
|
-
headers.set("authorization", `Bearer ${masterKey}`);
|
|
51
|
-
|
|
52
|
-
const existingCookies = request.headers.get("cookie");
|
|
53
|
-
const mergedCookies = [existingCookies, `token=${masterKey}`]
|
|
54
|
-
.filter(Boolean)
|
|
55
|
-
.join("; ");
|
|
56
|
-
headers.set("cookie", mergedCookies);
|
|
57
|
-
return headers;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function extractTokenCookie(cookieHeader: string | null): string | null {
|
|
61
|
-
if (!cookieHeader) {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const match = cookieHeader.match(/(?:^|;\s*)token=([^;]+)/);
|
|
66
|
-
return match?.[1] ?? null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function mergeCookieHeader(
|
|
70
|
-
existingCookieHeader: string | null,
|
|
71
|
-
token: string,
|
|
72
|
-
): string {
|
|
73
|
-
const cookies = [existingCookieHeader, `token=${token}`].filter(Boolean);
|
|
74
|
-
return cookies.join("; ");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function loginForUi(request: Request): Promise<string> {
|
|
78
|
-
const requestUrl = new URL(request.url);
|
|
79
|
-
const redirectTarget = `${requestUrl.origin}/ui/`;
|
|
80
|
-
const body = new URLSearchParams({
|
|
81
|
-
username: uiUsername,
|
|
82
|
-
password: masterKey,
|
|
83
|
-
redirect_to: redirectTarget,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const loginResponse = await fetchWithRetry(
|
|
87
|
-
new URL("/login", upstreamOrigin),
|
|
88
|
-
{
|
|
89
|
-
method: "POST",
|
|
90
|
-
headers: {
|
|
91
|
-
"content-type": "application/x-www-form-urlencoded",
|
|
92
|
-
authorization: `Bearer ${masterKey}`,
|
|
93
|
-
},
|
|
94
|
-
body,
|
|
95
|
-
redirect: "manual",
|
|
96
|
-
},
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
const token = extractTokenCookie(loginResponse.headers.get("set-cookie"));
|
|
100
|
-
if (!token) {
|
|
101
|
-
throw new Error("LiteLLM UI login did not return a token cookie");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return token;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function checkUiReadiness(): Promise<void> {
|
|
108
|
-
const token = await loginForUi(new Request("http://localhost/ui/"));
|
|
109
|
-
const response = await fetchWithRetry(new URL("/ui/", upstreamOrigin), {
|
|
110
|
-
headers: {
|
|
111
|
-
authorization: `Bearer ${masterKey}`,
|
|
112
|
-
cookie: `token=${token}`,
|
|
113
|
-
},
|
|
114
|
-
redirect: "manual",
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
118
|
-
if (!response.ok || !contentType.includes("text/html")) {
|
|
119
|
-
throw new Error(`LiteLLM UI readiness failed with ${response.status}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function rewriteLocationHeader(
|
|
124
|
-
request: Request,
|
|
125
|
-
responseHeaders: Headers,
|
|
126
|
-
): void {
|
|
127
|
-
const location = responseHeaders.get("location");
|
|
128
|
-
if (!location) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const requestOrigin = new URL(request.url).origin;
|
|
133
|
-
const upstream = new URL(upstreamOrigin);
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
const resolved = new URL(location, upstreamOrigin);
|
|
137
|
-
if (resolved.origin === upstream.origin) {
|
|
138
|
-
responseHeaders.set(
|
|
139
|
-
"location",
|
|
140
|
-
`${requestOrigin}${resolved.pathname}${resolved.search}${resolved.hash}`,
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
} catch {
|
|
144
|
-
// Leave malformed or non-URL locations untouched.
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function injectUiDefaults(html: string): string {
|
|
149
|
-
const script = [
|
|
150
|
-
"<script>",
|
|
151
|
-
"try {",
|
|
152
|
-
...uiDefaultFlags.map(
|
|
153
|
-
(flag) =>
|
|
154
|
-
`if (window.localStorage.getItem(${JSON.stringify(flag)}) === null) window.localStorage.setItem(${JSON.stringify(flag)}, "true");`,
|
|
155
|
-
),
|
|
156
|
-
"} catch {}",
|
|
157
|
-
"</script>",
|
|
158
|
-
].join("");
|
|
159
|
-
|
|
160
|
-
if (html.includes("</head>")) {
|
|
161
|
-
return html.replace("</head>", `${script}</head>`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return `${script}${html}`;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async function proxyRequest(request: Request): Promise<Response> {
|
|
168
|
-
const upstreamUrl = new URL(request.url);
|
|
169
|
-
upstreamUrl.protocol = new URL(upstreamOrigin).protocol;
|
|
170
|
-
upstreamUrl.host = new URL(upstreamOrigin).host;
|
|
171
|
-
const requestPath = new URL(request.url).pathname;
|
|
172
|
-
|
|
173
|
-
let token = extractTokenCookie(request.headers.get("cookie"));
|
|
174
|
-
if (requestPath.startsWith("/ui")) {
|
|
175
|
-
token ??= await loginForUi(request);
|
|
176
|
-
|
|
177
|
-
if (requestPath === "/ui/login" || requestPath === "/ui/login/") {
|
|
178
|
-
upstreamUrl.pathname = "/ui/";
|
|
179
|
-
upstreamUrl.search = "";
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const response = await fetchWithRetry(upstreamUrl, {
|
|
184
|
-
method: request.method,
|
|
185
|
-
headers: (() => {
|
|
186
|
-
const headers = copyRequestHeaders(request);
|
|
187
|
-
if (token) {
|
|
188
|
-
headers.set(
|
|
189
|
-
"cookie",
|
|
190
|
-
mergeCookieHeader(request.headers.get("cookie"), token),
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
return headers;
|
|
194
|
-
})(),
|
|
195
|
-
body:
|
|
196
|
-
request.method === "GET" || request.method === "HEAD"
|
|
197
|
-
? undefined
|
|
198
|
-
: request.body,
|
|
199
|
-
redirect: "manual",
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
const responseHeaders = new Headers(response.headers);
|
|
203
|
-
responseHeaders.delete("content-encoding");
|
|
204
|
-
responseHeaders.delete("content-length");
|
|
205
|
-
rewriteLocationHeader(request, responseHeaders);
|
|
206
|
-
if (token) {
|
|
207
|
-
responseHeaders.append(
|
|
208
|
-
"set-cookie",
|
|
209
|
-
`token=${token}; Path=/; SameSite=Lax`,
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const contentType = responseHeaders.get("content-type") ?? "";
|
|
214
|
-
if (requestPath.startsWith("/ui") && contentType.includes("text/html")) {
|
|
215
|
-
const html = await response.text();
|
|
216
|
-
return new Response(injectUiDefaults(html), {
|
|
217
|
-
status: response.status,
|
|
218
|
-
statusText: response.statusText,
|
|
219
|
-
headers: responseHeaders,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return new Response(response.body, {
|
|
224
|
-
status: response.status,
|
|
225
|
-
statusText: response.statusText,
|
|
226
|
-
headers: responseHeaders,
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const server = Bun.serve({
|
|
231
|
-
port,
|
|
232
|
-
async fetch(request: Request): Promise<Response> {
|
|
233
|
-
const pathname = new URL(request.url).pathname;
|
|
234
|
-
if (pathname === "/healthz") {
|
|
235
|
-
return new Response("ok");
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (pathname === "/readyz") {
|
|
239
|
-
try {
|
|
240
|
-
await checkUiReadiness();
|
|
241
|
-
return new Response("ok");
|
|
242
|
-
} catch (error) {
|
|
243
|
-
console.error("[litellm-ui-proxy] readiness check failed", error);
|
|
244
|
-
return new Response("not ready", { status: 503 });
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
return await proxyRequest(request);
|
|
250
|
-
} catch (error) {
|
|
251
|
-
console.error("[litellm-ui-proxy] upstream request failed", error);
|
|
252
|
-
return new Response("LiteLLM UI proxy upstream request failed", {
|
|
253
|
-
status: 502,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
console.log(`[litellm-ui-proxy] listening on :${server.port}`);
|