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.
- package/LICENSE +21 -0
- package/dist/00-app-databases-mx6j59m0.sh +28 -0
- package/dist/Caddyfile-at2nzhxs. +18 -0
- package/dist/Dockerfile-bzexf8bh. +7 -0
- package/dist/Dockerfile-tvb2c6ma. +7 -0
- package/dist/bootstrap-client-key-v79zzaxg.sh +39 -0
- package/dist/compose-1b8sxndd.yml +266 -0
- package/dist/compose-vz8yebk4.yml +266 -0
- package/dist/config-tt3vhpk0.yaml +101 -0
- package/dist/config-ymzwdk89.yaml +7 -0
- package/dist/index.js +16487 -0
- package/dist/logging-jyk2svr4.json +35 -0
- package/dist/request_session_metadata_callback-wfkph4zx.py +109 -0
- package/dist/server-bm43enwc.ts +257 -0
- package/dist/server-d2v9t7c1.ts +257 -0
- package/dist/server-dchc55xz.ts +259 -0
- package/dist/server-k3xr84w3.ts +259 -0
- package/dist/start-litellm-1rtmgp2c.sh +16 -0
- package/dist/traicebox-7pysd22b.yaml +2 -0
- package/dist/traicebox-d8k281x5.yaml +2 -0
- package/package.json +50 -0
|
@@ -0,0 +1,259 @@
|
|
|
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}`);
|
|
@@ -0,0 +1,259 @@
|
|
|
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}`);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
SECRET_PATH="/run/secrets/openai_compatible_api_key"
|
|
5
|
+
|
|
6
|
+
if [ -z "${OPENAI_COMPATIBLE_API_KEY:-}" ] && [ -s "$SECRET_PATH" ]; then
|
|
7
|
+
OPENAI_COMPATIBLE_API_KEY="$(cat "$SECRET_PATH")"
|
|
8
|
+
export OPENAI_COMPATIBLE_API_KEY
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
if [ -z "${OPENAI_COMPATIBLE_API_KEY:-}" ]; then
|
|
12
|
+
OPENAI_COMPATIBLE_API_KEY="sk-no-auth-required"
|
|
13
|
+
export OPENAI_COMPATIBLE_API_KEY
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
exec litellm "$@"
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "traicebox",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A zero-config local developer stack for tracing and session tracking around LLM and AI model workflows",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Fardjad Davari <public@fardjad.com>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/fardjad/traicebox"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"ai",
|
|
13
|
+
"llm",
|
|
14
|
+
"tracing",
|
|
15
|
+
"observability",
|
|
16
|
+
"debugging",
|
|
17
|
+
"developer-tools",
|
|
18
|
+
"cli",
|
|
19
|
+
"zero-config",
|
|
20
|
+
"session-tracking"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"traicebox": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"traicebox": "TRAICEBOX_DEV=1 bun ./cli/index.ts",
|
|
31
|
+
"build": "bun build ./cli/index.ts --outdir ./dist --target node",
|
|
32
|
+
"prepublishOnly": "bun run build",
|
|
33
|
+
"test": "bun test",
|
|
34
|
+
"typecheck": "tsc",
|
|
35
|
+
"check": "biome check . && dprint check",
|
|
36
|
+
"fix": "biome check --write . && dprint fmt"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@biomejs/biome": "^2.4.12",
|
|
40
|
+
"@types/bun": "^1.3.12",
|
|
41
|
+
"@types/yargs": "^17.0.35",
|
|
42
|
+
"dprint": "^0.54.0",
|
|
43
|
+
"typescript": "^6.0.2"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"ora": "^9.3.0",
|
|
47
|
+
"yaml": "^2.8.3",
|
|
48
|
+
"yargs": "^18.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|