traicebox 0.1.0

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.
@@ -0,0 +1,35 @@
1
+ {
2
+ "version": 1,
3
+ "disable_existing_loggers": false,
4
+ "formatters": {
5
+ "default": {
6
+ "()": "uvicorn.logging.DefaultFormatter",
7
+ "fmt": "%(levelprefix)s %(message)s",
8
+ "use_colors": null
9
+ }
10
+ },
11
+ "handlers": {
12
+ "default": {
13
+ "class": "logging.StreamHandler",
14
+ "formatter": "default",
15
+ "stream": "ext://sys.stderr"
16
+ }
17
+ },
18
+ "loggers": {
19
+ "uvicorn": {
20
+ "handlers": ["default"],
21
+ "level": "WARNING",
22
+ "propagate": false
23
+ },
24
+ "uvicorn.error": {
25
+ "handlers": ["default"],
26
+ "level": "WARNING",
27
+ "propagate": false
28
+ },
29
+ "uvicorn.access": {
30
+ "handlers": [],
31
+ "level": "WARNING",
32
+ "propagate": false
33
+ }
34
+ }
35
+ }
@@ -0,0 +1,109 @@
1
+ from typing import Optional
2
+
3
+ from litellm.integrations.custom_logger import CustomLogger
4
+ from litellm.litellm_core_utils.llm_request_utils import (
5
+ get_proxy_server_request_headers,
6
+ )
7
+ from litellm.proxy.proxy_server import DualCache, UserAPIKeyAuth
8
+ from litellm.types.utils import CallTypesLiteral
9
+
10
+
11
+ SESSION_HEADER_CANDIDATES = (
12
+ "x-litellm-trace-id",
13
+ "x-litellm-session-id",
14
+ "x-session-affinity",
15
+ "x-opencode-session",
16
+ )
17
+
18
+
19
+ def _normalize_headers(headers: dict) -> dict[str, str]:
20
+ normalized_headers: dict[str, str] = {}
21
+ for key, value in headers.items():
22
+ if key is None or value is None:
23
+ continue
24
+ normalized_headers[str(key).lower()] = str(value).strip()
25
+ return normalized_headers
26
+
27
+
28
+ def _extract_session_id(headers: dict[str, str]) -> Optional[str]:
29
+ for header_name in SESSION_HEADER_CANDIDATES:
30
+ value = headers.get(header_name)
31
+ if value:
32
+ return value
33
+ return None
34
+
35
+
36
+ def _get_incoming_headers(data: dict) -> dict[str, str]:
37
+ secret_fields = data.get("secret_fields")
38
+ if isinstance(secret_fields, dict):
39
+ raw_headers = secret_fields.get("raw_headers")
40
+ if isinstance(raw_headers, dict) and raw_headers:
41
+ return _normalize_headers(raw_headers)
42
+
43
+ litellm_params = data.get("litellm_params")
44
+ if isinstance(litellm_params, dict):
45
+ headers = get_proxy_server_request_headers(litellm_params)
46
+ if headers:
47
+ return _normalize_headers(headers)
48
+
49
+ proxy_server_request = data.get("proxy_server_request")
50
+ if isinstance(proxy_server_request, dict):
51
+ headers = proxy_server_request.get("headers")
52
+ if isinstance(headers, dict) and headers:
53
+ return _normalize_headers(headers)
54
+
55
+ return {}
56
+
57
+
58
+ def _ensure_mapping(data: dict, key: str) -> dict:
59
+ current_value = data.get(key)
60
+ if isinstance(current_value, dict):
61
+ return current_value
62
+
63
+ replacement: dict = {}
64
+ data[key] = replacement
65
+ return replacement
66
+
67
+
68
+ class RequestSessionMetadataCallback(CustomLogger):
69
+ async def async_pre_call_hook(
70
+ self,
71
+ user_api_key_dict: UserAPIKeyAuth,
72
+ cache: DualCache,
73
+ data: dict,
74
+ call_type: CallTypesLiteral,
75
+ ):
76
+ del user_api_key_dict, cache, call_type
77
+
78
+ if data.get("litellm_session_id"):
79
+ return data
80
+
81
+ metadata = _ensure_mapping(data, "metadata")
82
+ if metadata.get("session_id"):
83
+ return data
84
+
85
+ litellm_metadata = _ensure_mapping(data, "litellm_metadata")
86
+ if litellm_metadata.get("session_id"):
87
+ return data
88
+
89
+ headers = _get_incoming_headers(data)
90
+ session_id = _extract_session_id(headers)
91
+ if not session_id:
92
+ return data
93
+
94
+ parent_session_id = headers.get("x-parent-session-id")
95
+ data["litellm_session_id"] = session_id
96
+ data["litellm_trace_id"] = session_id
97
+ metadata.setdefault("session_id", session_id)
98
+ metadata.setdefault("trace_id", session_id)
99
+ litellm_metadata.setdefault("session_id", session_id)
100
+ litellm_metadata.setdefault("trace_id", session_id)
101
+
102
+ if parent_session_id:
103
+ metadata.setdefault("parent_session_id", parent_session_id)
104
+ litellm_metadata.setdefault("parent_session_id", parent_session_id)
105
+
106
+ return data
107
+
108
+
109
+ proxy_handler_instance = RequestSessionMetadataCallback()
@@ -0,0 +1,257 @@
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}`);
@@ -0,0 +1,257 @@
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}`);