@burtson-labs/bandit-engine 2.0.52 → 2.0.54

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.
Files changed (51) hide show
  1. package/README.md +23 -12
  2. package/dist/{aiProviderStore-3N3VE6D4.mjs → aiProviderStore-337QNQB3.mjs} +2 -2
  3. package/dist/{chat-YWYLVKXX.mjs → chat-IDE6Z4Q4.mjs} +6 -6
  4. package/dist/chat-provider.js +17 -9
  5. package/dist/chat-provider.js.map +1 -1
  6. package/dist/chat-provider.mjs +4 -4
  7. package/dist/{chunk-QX6CO7TJ.mjs → chunk-557E5VZ2.mjs} +3 -3
  8. package/dist/{chunk-MH7WFWCP.mjs → chunk-AVV7HDGR.mjs} +3 -3
  9. package/dist/{chunk-BENL3EF2.mjs → chunk-H3BYFEIE.mjs} +18 -10
  10. package/dist/chunk-H3BYFEIE.mjs.map +1 -0
  11. package/dist/{chunk-YZ2HJFPQ.mjs → chunk-KM7FUWCM.mjs} +4 -4
  12. package/dist/chunk-KM7FUWCM.mjs.map +1 -0
  13. package/dist/{chunk-M3BEAMCC.mjs → chunk-KNBWR4DS.mjs} +775 -488
  14. package/dist/chunk-KNBWR4DS.mjs.map +1 -0
  15. package/dist/{chunk-Y5N3NSTU.mjs → chunk-NP2OHTTX.mjs} +116 -152
  16. package/dist/chunk-NP2OHTTX.mjs.map +1 -0
  17. package/dist/{chunk-TSQCNHOX.mjs → chunk-TLY6A6XL.mjs} +123 -147
  18. package/dist/chunk-TLY6A6XL.mjs.map +1 -0
  19. package/dist/{chunk-37PEP5JK.mjs → chunk-UFSEYVRS.mjs} +3 -3
  20. package/dist/{chunk-RSSJADDD.mjs → chunk-WL7NV4WJ.mjs} +28 -32
  21. package/dist/chunk-WL7NV4WJ.mjs.map +1 -0
  22. package/dist/index.d.mts +5 -3
  23. package/dist/index.d.ts +5 -3
  24. package/dist/index.js +4097 -4045
  25. package/dist/index.js.map +1 -1
  26. package/dist/index.mjs +11 -15
  27. package/dist/index.mjs.map +1 -1
  28. package/dist/management/management.js +4068 -4016
  29. package/dist/management/management.js.map +1 -1
  30. package/dist/management/management.mjs +7 -7
  31. package/dist/modals/chat-modal/chat-modal.js +1401 -1279
  32. package/dist/modals/chat-modal/chat-modal.js.map +1 -1
  33. package/dist/modals/chat-modal/chat-modal.mjs +4 -4
  34. package/dist/{gateway-oScD5tvE.d.mts → public-BzsEWB08.d.mts} +11 -122
  35. package/dist/{gateway-oScD5tvE.d.ts → public-BzsEWB08.d.ts} +11 -122
  36. package/dist/public-types.d.mts +2 -32
  37. package/dist/public-types.d.ts +2 -32
  38. package/package.json +3 -11
  39. package/dist/chunk-BENL3EF2.mjs.map +0 -1
  40. package/dist/chunk-M3BEAMCC.mjs.map +0 -1
  41. package/dist/chunk-RSSJADDD.mjs.map +0 -1
  42. package/dist/chunk-TSQCNHOX.mjs.map +0 -1
  43. package/dist/chunk-Y5N3NSTU.mjs.map +0 -1
  44. package/dist/chunk-YZ2HJFPQ.mjs.map +0 -1
  45. package/dist/cli.js +0 -4084
  46. package/dist/cli.js.map +0 -1
  47. /package/dist/{aiProviderStore-3N3VE6D4.mjs.map → aiProviderStore-337QNQB3.mjs.map} +0 -0
  48. /package/dist/{chat-YWYLVKXX.mjs.map → chat-IDE6Z4Q4.mjs.map} +0 -0
  49. /package/dist/{chunk-QX6CO7TJ.mjs.map → chunk-557E5VZ2.mjs.map} +0 -0
  50. /package/dist/{chunk-MH7WFWCP.mjs.map → chunk-AVV7HDGR.mjs.map} +0 -0
  51. /package/dist/{chunk-37PEP5JK.mjs.map → chunk-UFSEYVRS.mjs.map} +0 -0
package/dist/cli.js DELETED
@@ -1,4084 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- var __create = Object.create;
4
- var __defProp = Object.defineProperty;
5
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __getProtoOf = Object.getPrototypeOf;
8
- var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __copyProps = (to, from, except, desc) => {
10
- if (from && typeof from === "object" || typeof from === "function") {
11
- for (let key of __getOwnPropNames(from))
12
- if (!__hasOwnProp.call(to, key) && key !== except)
13
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
- }
15
- return to;
16
- };
17
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
- // If the importer is in node compatibility mode or this is not an ESM
19
- // file that has been converted to a CommonJS file using a Babel-
20
- // compatible transform (i.e. "__esModule" has not been set), then set
21
- // "default" to the CommonJS "module.exports" for node compatibility.
22
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
- mod
24
- ));
25
-
26
- // src/cli/index.ts
27
- var import_node_path3 = __toESM(require("path"));
28
- var import_commander = require("commander");
29
-
30
- // package.json
31
- var package_default = {
32
- name: "@burtson-labs/bandit-engine",
33
- version: "2.0.52",
34
- license: "BUSL-1.1",
35
- main: "dist/index.js",
36
- module: "dist/index.mjs",
37
- types: "dist/index.d.ts",
38
- bin: {
39
- bandit: "dist/cli.js"
40
- },
41
- files: [
42
- "dist",
43
- "dist/cli",
44
- "docs",
45
- "LICENSE",
46
- "README.md"
47
- ],
48
- repository: {
49
- type: "git",
50
- url: "https://github.com/Burtson-Labs/bandit-engine.git"
51
- },
52
- homepage: "https://banditailabs.com/npm-package",
53
- author: "Burtson Labs LLC <team@banditai.ai>",
54
- maintainers: [
55
- {
56
- name: "Burtson Labs LLC",
57
- email: "team@banditai.ai"
58
- }
59
- ],
60
- keywords: [
61
- "ai",
62
- "chat",
63
- "react",
64
- "mui",
65
- "llm",
66
- "frontend",
67
- "openai",
68
- "ollama",
69
- "chatbot",
70
- "sdk"
71
- ],
72
- scripts: {
73
- build: "tsup",
74
- dev: "tsup --watch",
75
- docs: 'typedoc src --out docs/api_reference --skipErrorChecking --sourceLinkTemplate "https://github.com/Burtson-Labs/bandit-engine/blob/main/{path}#L{line}" && node ./scripts/post-typedoc.mjs',
76
- lint: 'eslint "src/**/*.{ts,tsx}"',
77
- test: "vitest",
78
- protect: "node scripts/add-license-headers.js",
79
- "validate-protection": "node scripts/validate-protection.js"
80
- },
81
- overrides: {
82
- "sha.js": "^2.4.12"
83
- },
84
- dependencies: {
85
- "@emotion/react": "^11.14.0",
86
- "@emotion/styled": "^11.14.0",
87
- "@mui/icons-material": "^7.0.1",
88
- "@mui/material": "^7.1.0",
89
- "@tanstack/react-query": "^5.66.3",
90
- axios: "^1.7.9",
91
- commander: "^12.1.0",
92
- "fs-extra": "^11.3.0",
93
- "highlight.js": "^11.10.0",
94
- idb: "latest",
95
- lowlight: "^3.1.0",
96
- mammoth: "^1.9.0",
97
- "pdfjs-dist": "^5.2.133",
98
- prompts: "^2.4.2",
99
- react: "^19.0.0",
100
- "react-dom": "^19.0.0",
101
- "react-markdown": "^10.1.0",
102
- "react-router-dom": "^7.5.0",
103
- "rehype-raw": "^7.0.0",
104
- "rehype-sanitize": "^6.0.0",
105
- "remark-gfm": "^4.0.1",
106
- rxjs: "^7.8.2",
107
- uuid: "^11.1.0",
108
- zustand: "^4.5.6"
109
- },
110
- devDependencies: {
111
- "@testing-library/jest-dom": "^6.6.3",
112
- "@testing-library/react": "^16.3.0",
113
- "@types/fs-extra": "^11.0.4",
114
- "@types/node": "^24.0.3",
115
- "@types/prompts": "^2.4.9",
116
- "@types/react": "^18.3.22",
117
- "@types/react-dom": "^18.2.17",
118
- "@types/uuid": "^10.0.0",
119
- "@vitejs/plugin-react": "^4.6.0",
120
- eslint: "^8.57.0",
121
- "eslint-plugin-react": "^7.34.1",
122
- jsdom: "^26.1.0",
123
- tsup: "^8.5.0",
124
- typedoc: "^0.26.11",
125
- typescript: "5.5.4",
126
- vitest: "^3.2.4"
127
- },
128
- peerDependencies: {
129
- "@mui/material": ">=5",
130
- react: ">=18",
131
- zustand: ">=4"
132
- },
133
- exports: {
134
- ".": {
135
- import: "./dist/index.mjs",
136
- require: "./dist/index.js"
137
- },
138
- "./types": {
139
- types: "./dist/public-types.d.ts",
140
- default: "./dist/public-types.d.ts"
141
- }
142
- }
143
- };
144
-
145
- // src/cli/createQuickstart.ts
146
- var import_node_path2 = __toESM(require("path"));
147
- var import_fs_extra = __toESM(require("fs-extra"));
148
- var import_prompts = __toESM(require("prompts"));
149
-
150
- // src/cli/utils.ts
151
- var import_node_path = __toESM(require("path"));
152
- var toKebabCase = (value) => {
153
- return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[\s_./]+/g, "-").replace(/[^a-zA-Z0-9-]+/g, "").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
154
- };
155
- var toTitleCase = (value) => {
156
- const cleaned = value.replace(/[-_/]+/g, " ").replace(/\s+/g, " ").trim();
157
- if (!cleaned) {
158
- return "";
159
- }
160
- return cleaned.replace(/\b\w/g, (char) => char.toUpperCase());
161
- };
162
- var formatJson = (value) => `${JSON.stringify(value, null, 2)}
163
- `;
164
- var KNOWN_PROVIDERS = /* @__PURE__ */ new Set(["openai", "azure", "azure-openai", "azureopenai", "anthropic", "xai", "ollama"]);
165
- var sanitizeModelIdentifier = (value) => {
166
- const trimmed = value.trim();
167
- if (!trimmed.includes(":")) {
168
- return trimmed.toLowerCase();
169
- }
170
- const segments = trimmed.split(/:(.+)/).filter(Boolean);
171
- if (segments.length < 2) {
172
- return trimmed.toLowerCase();
173
- }
174
- const [candidateProvider, rest] = segments;
175
- const provider = candidateProvider.toLowerCase();
176
- const cleanRest = rest.trim().replace(/[^a-zA-Z0-9_.:-]/g, "-").replace(/-+/g, "-").toLowerCase();
177
- if (KNOWN_PROVIDERS.has(provider)) {
178
- if (provider === "azure-openai" || provider === "azureopenai") {
179
- return `azure:${cleanRest}`;
180
- }
181
- if (provider === "ollama") {
182
- return cleanRest;
183
- }
184
- return `${provider}:${cleanRest}`;
185
- }
186
- return [candidateProvider, rest].filter(Boolean).join(":").replace(/[^a-zA-Z0-9_.:-]/g, "-").replace(/-+/g, "-").toLowerCase();
187
- };
188
- var normalizeLineEndings = (content) => content.replace(/\r\n/g, "\n");
189
- var ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}
190
- `;
191
-
192
- // src/cli/templates.ts
193
- var QUOTE = '"';
194
- var buildPackageJson = (ctx) => formatJson({
195
- name: ctx.packageName,
196
- private: true,
197
- version: "0.1.0",
198
- type: "module",
199
- scripts: {
200
- dev: 'concurrently -k "npm run dev:gateway" "npm run dev:web"',
201
- "dev:web": "vite",
202
- "dev:gateway": "node server/gateway.js",
203
- build: "vite build",
204
- preview: "vite preview"
205
- },
206
- dependencies: {
207
- "@burtson-labs/bandit-engine": `^${ctx.engineVersion}`,
208
- "@emotion/react": "^11.14.0",
209
- "@emotion/styled": "^11.14.0",
210
- "@mui/material": "^7.1.0",
211
- "@tanstack/react-query": "^5.59.20",
212
- "cors": "^2.8.5",
213
- "dotenv": "^16.4.5",
214
- "express": "^4.19.2",
215
- "react": "^19.0.0",
216
- "react-dom": "^19.0.0",
217
- "react-router-dom": "^7.5.0",
218
- "zustand": "^4.5.6"
219
- },
220
- devDependencies: {
221
- "@types/express": "^4.17.21",
222
- "@types/node": "^20.17.7",
223
- "@types/react": "^18.3.22",
224
- "@types/react-dom": "^18.2.18",
225
- "@vitejs/plugin-react": "^5.0.0",
226
- "concurrently": "^8.2.2",
227
- "typescript": "^5.5.4",
228
- "vite": "^7.1.9"
229
- }
230
- });
231
- var buildEnvExample = (ctx) => {
232
- const lines = [
233
- "# Frontend configuration",
234
- `VITE_DEV_PORT=${ctx.frontendPort}`,
235
- `VITE_GATEWAY_URL=${ctx.defaultGatewayUrl}`,
236
- `VITE_DEFAULT_MODEL=${ctx.defaultModelId}`,
237
- `VITE_FALLBACK_MODEL=${ctx.fallbackModelId ?? ""}`,
238
- `VITE_GATEWAY_PROVIDER=${ctx.defaultProvider}`,
239
- `VITE_BRANDING_TEXT=${ctx.brandingText}`,
240
- "",
241
- "# Gateway configuration",
242
- "# These values power server/gateway.js \u2014 update them before running in production."
243
- ];
244
- switch (ctx.defaultProvider) {
245
- case "openai":
246
- lines.push("OPENAI_API_KEY=");
247
- break;
248
- case "azure":
249
- lines.push("AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com");
250
- lines.push("AZURE_OPENAI_API_KEY=");
251
- lines.push("AZURE_OPENAI_API_VERSION=2024-08-01-preview");
252
- lines.push("AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-4o");
253
- lines.push("AZURE_OPENAI_COMPLETIONS_DEPLOYMENT=gpt-35-turbo-instruct");
254
- lines.push("AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT=text-embedding-3-large");
255
- break;
256
- case "anthropic":
257
- lines.push("ANTHROPIC_API_KEY=");
258
- lines.push("ANTHROPIC_BASE_URL=https://api.anthropic.com");
259
- lines.push("ANTHROPIC_API_VERSION=2023-06-01");
260
- lines.push("ANTHROPIC_MAX_TOKENS=1024");
261
- break;
262
- case "xai":
263
- lines.push("XAI_API_KEY=");
264
- lines.push("XAI_BASE_URL=https://api.x.ai/v1");
265
- break;
266
- case "bandit":
267
- lines.push("BANDIT_API_KEY=");
268
- lines.push("BANDIT_BASE_URL=https://api.burtson.ai");
269
- break;
270
- case "ollama":
271
- default:
272
- lines.push("OLLAMA_URL=http://localhost:11434");
273
- break;
274
- }
275
- lines.push(`PORT=${ctx.gatewayPort}`);
276
- lines.push(
277
- "# If you switch providers later, copy the relevant block above and update the credentials."
278
- );
279
- return ensureTrailingNewline(normalizeLineEndings(lines.join("\n")));
280
- };
281
- var buildTsConfig = () => formatJson({
282
- compilerOptions: {
283
- target: "ESNext",
284
- useDefineForClassFields: true,
285
- lib: ["DOM", "DOM.Iterable", "ESNext"],
286
- allowJs: false,
287
- skipLibCheck: true,
288
- esModuleInterop: true,
289
- allowSyntheticDefaultImports: true,
290
- strict: true,
291
- forceConsistentCasingInFileNames: true,
292
- module: "ESNext",
293
- moduleResolution: "Node",
294
- resolveJsonModule: true,
295
- isolatedModules: true,
296
- noEmit: true,
297
- jsx: "react-jsx"
298
- },
299
- include: ["src"]
300
- });
301
- var buildEnvDts = () => ensureTrailingNewline(
302
- `/// <reference types="vite/client" />
303
-
304
- interface ImportMetaEnv {
305
- readonly VITE_GATEWAY_URL?: string;
306
- readonly VITE_GATEWAY_PROVIDER?: string;
307
- readonly VITE_DEFAULT_MODEL?: string;
308
- readonly VITE_FALLBACK_MODEL?: string;
309
- readonly VITE_FEEDBACK_EMAIL?: string;
310
- readonly VITE_BRANDING_TEXT?: string;
311
- }
312
-
313
- interface ImportMeta {
314
- readonly env: ImportMetaEnv;
315
- }
316
- `
317
- );
318
- var buildViteConfig = (ctx) => ensureTrailingNewline(
319
- normalizeLineEndings(
320
- `import { defineConfig, loadEnv } from "vite";
321
- import react from "@vitejs/plugin-react";
322
-
323
- export default defineConfig(({ mode }) => {
324
- const env = loadEnv(mode, process.cwd(), "");
325
- const parsedPort = Number(env.VITE_DEV_PORT || env.PORT || ${ctx.frontendPort});
326
- const port = Number.isFinite(parsedPort) ? parsedPort : ${ctx.frontendPort};
327
-
328
- return {
329
- plugins: [react()],
330
- resolve: {
331
- dedupe: [
332
- "react",
333
- "react-dom",
334
- "@mui/material",
335
- "@mui/system",
336
- "@emotion/react",
337
- "@emotion/styled",
338
- "react-router-dom"
339
- ],
340
- },
341
- optimizeDeps: {
342
- include: ["@burtson-labs/bandit-engine"],
343
- },
344
- server: {
345
- port,
346
- },
347
- };
348
- });
349
- `
350
- )
351
- );
352
- var buildMainTsx = () => ensureTrailingNewline(
353
- normalizeLineEndings(
354
- `import React from "react";
355
- import ReactDOM from "react-dom/client";
356
- import { BrowserRouter } from "react-router-dom";
357
- import App from "./App";
358
- import "./index.css";
359
-
360
- ReactDOM.createRoot(document.getElementById("root")!).render(
361
- <React.StrictMode>
362
- <BrowserRouter>
363
- <App />
364
- </BrowserRouter>
365
- </React.StrictMode>
366
- );
367
- `
368
- )
369
- );
370
- var buildIndexCss = () => ensureTrailingNewline(
371
- normalizeLineEndings(
372
- `:root {
373
- font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
374
- background: radial-gradient(circle at top left, rgba(120, 119, 255, 0.12), transparent 45%),
375
- radial-gradient(circle at bottom right, rgba(244, 114, 182, 0.1), transparent 55%),
376
- #05070f;
377
- color: #f8fafc;
378
- min-height: 100vh;
379
- }
380
-
381
- body {
382
- margin: 0;
383
- }
384
-
385
- * {
386
- box-sizing: border-box;
387
- }
388
- `
389
- )
390
- );
391
- var buildIndexHtml = () => ensureTrailingNewline(
392
- normalizeLineEndings(
393
- `<!doctype html>
394
- <html lang="en">
395
- <head>
396
- <meta charset="UTF-8" />
397
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
398
- <link rel="icon" href="https://cdn.burtson.ai/images/bandit-head.png" />
399
- <title>Bandit Quickstart</title>
400
- </head>
401
- <body>
402
- <div id="root"></div>
403
- <script type="module" src="/src/main.tsx"></script>
404
- </body>
405
- </html>
406
- `
407
- )
408
- );
409
- var buildThemeTs = () => ensureTrailingNewline(
410
- normalizeLineEndings(
411
- `import { createTheme } from "@mui/material/styles";
412
-
413
- export const banditQuickstartTheme = createTheme({
414
- palette: {
415
- mode: "dark",
416
- primary: {
417
- main: "#f97316",
418
- },
419
- secondary: {
420
- main: "#6366f1",
421
- },
422
- background: {
423
- default: "#05070f",
424
- paper: "rgba(15, 23, 42, 0.78)",
425
- },
426
- },
427
- typography: {
428
- fontFamily: '"Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
429
- h1: {
430
- fontWeight: 700,
431
- },
432
- h2: {
433
- fontWeight: 600,
434
- },
435
- },
436
- components: {
437
- MuiPaper: {
438
- styleOverrides: {
439
- root: {
440
- backdropFilter: "blur(18px)",
441
- backgroundImage: "linear-gradient(145deg, rgba(15, 23, 42, 0.92), rgba(2, 6, 23, 0.92))",
442
- },
443
- },
444
- },
445
- },
446
- });
447
- `
448
- )
449
- );
450
- var buildAppTsx = (ctx) => {
451
- const responseStatusExpr = "${response.status}";
452
- const gatewayErrorExpr = '${gatewayError ?? "Unknown"}';
453
- const template = `
454
- import { useEffect, useState, useMemo, useCallback, type ReactNode } from "react";
455
- import {
456
- CssBaseline,
457
- AppBar,
458
- Toolbar,
459
- Typography,
460
- Container,
461
- Box,
462
- Button,
463
- Chip,
464
- Tooltip,
465
- Stack,
466
- Card,
467
- CardContent
468
- } from "@mui/material";
469
- import { ThemeProvider } from "@mui/material/styles";
470
- import { Routes, Route, Navigate, Link as RouterLink, useLocation } from "react-router-dom";
471
- import { ChatProvider, Chat, ChatModal, Management } from "@burtson-labs/bandit-engine";
472
- import * as BanditEngine from "@burtson-labs/bandit-engine";
473
- import { banditQuickstartTheme } from "./theme";
474
-
475
- const gatewayBaseUrl = (import.meta.env.VITE_GATEWAY_URL ?? "${ctx.defaultGatewayUrl}").replace(/\\/$/, "");
476
- const defaultModelId = import.meta.env.VITE_DEFAULT_MODEL ?? "${ctx.defaultModelId}";
477
- const fallbackModelId = import.meta.env.VITE_FALLBACK_MODEL ?? ${ctx.fallbackModelId ? `${QUOTE}${ctx.fallbackModelId}${QUOTE}` : "undefined"};
478
- const brandingText = import.meta.env.VITE_BRANDING_TEXT ?? "${ctx.brandingText}";
479
- const provider = (import.meta.env.VITE_GATEWAY_PROVIDER ?? "${ctx.defaultProvider}") as "openai" | "ollama" | "azure" | "anthropic" | "xai" | "bandit";
480
-
481
- const gatewayApiUrl = gatewayBaseUrl.endsWith("/api") ? gatewayBaseUrl : gatewayBaseUrl + "/api";
482
- const banditHeadLogoUrl = "https://cdn.burtson.ai/images/bandit-head.png";
483
- const burtsonLabsLogoUrl = "https://cdn.burtson.ai/logos/burtson-labs-logo-alt.png";
484
- const healthEndpoint = gatewayApiUrl + "/health";
485
-
486
- // Move packageSettings outside the component to prevent recreation on every render
487
- const packageSettings = {
488
- defaultModel: defaultModelId,
489
- fallbackModel: fallbackModelId,
490
- gatewayApiUrl: gatewayApiUrl,
491
- brandingConfigUrl: "/config.json",
492
- aiProvider: {
493
- type: "gateway" as const,
494
- gatewayUrl: gatewayApiUrl,
495
- provider,
496
- tokenFactory: () => {
497
- return localStorage.getItem("authToken");
498
- }
499
- },
500
- feedbackEmail: import.meta.env.VITE_FEEDBACK_EMAIL,
501
- featureFlags: {
502
- subscriptionType: "premium" as const,
503
- rolesClaimKey: "roles",
504
- subscriptionTypeClaimKey: "subscriptionType",
505
- isSubscribedClaimKey: "isSubscribed",
506
- jwtStorageKey: "authToken",
507
- adminRole: "admin",
508
- debug: true,
509
- featureMatrix: {
510
- tts: false,
511
- stt: false,
512
- semanticSearchSimple: false,
513
- semanticSearchPremium: false,
514
- advancedSearch: false,
515
- advancedMemories: false,
516
- },
517
- },
518
- };
519
-
520
- const seedQuickstartAuth = () => {
521
- if (typeof window === "undefined" || typeof localStorage === "undefined") {
522
- return;
523
- }
524
-
525
- const applyAuthToken = (token: string) => {
526
- localStorage.setItem("authToken", token);
527
- const maybeService = (BanditEngine as { authenticationService?: { setToken: (token: string) => void } }).authenticationService;
528
- try {
529
- maybeService?.setToken(token);
530
- } catch (error) {
531
- console.warn("Bandit quickstart: failed to seed authentication service token", error);
532
- }
533
- };
534
-
535
- const existing = localStorage.getItem("authToken");
536
- if (existing) {
537
- applyAuthToken(existing);
538
- return;
539
- }
540
-
541
- const header = {
542
- alg: "HS256",
543
- typ: "JWT",
544
- };
545
- const payload = {
546
- exp: Math.floor(Date.now() / 1000) + 60 * 60 * 8,
547
- roles: ["admin"],
548
- iat: Math.floor(Date.now() / 1000),
549
- email: "quickstart@burtson.ai",
550
- sub: "123456789012345678901",
551
- };
552
- const encodeSegment = (value: unknown) =>
553
- btoa(JSON.stringify(value))
554
- .replace(/=+$/g, "")
555
- .replace(/\\+/g, "-")
556
- .replace(/\\//g, "_");
557
- const mockToken = \`${"${"}encodeSegment(header)}.${"${"}encodeSegment(payload)}.quickstart\`;
558
- applyAuthToken(mockToken);
559
- };
560
-
561
- seedQuickstartAuth();
562
-
563
- function App() {
564
- const location = useLocation();
565
- const [isModalOpen, setIsModalOpen] = useState(false);
566
- const [gatewayStatus, setGatewayStatus] = useState<"checking" | "healthy" | "error">("checking");
567
- const [gatewayError, setGatewayError] = useState<string | null>(null);
568
-
569
- // Separate effect for health checking to avoid re-renders
570
- useEffect(() => {
571
- let cancelled = false;
572
-
573
- const checkHealth = async () => {
574
- try {
575
- const response = await fetch(healthEndpoint, { headers: { "Content-Type": "application/json" } });
576
- if (!response.ok) {
577
- throw new Error(\`HTTP __RESPONSE_STATUS__\`);
578
- }
579
- await response.json();
580
- if (!cancelled) {
581
- setGatewayStatus(prevStatus => prevStatus !== "healthy" ? "healthy" : prevStatus);
582
- setGatewayError(prevError => prevError !== null ? null : prevError);
583
- }
584
- } catch (error) {
585
- if (!cancelled) {
586
- const errorMessage = error instanceof Error ? error.message : String(error);
587
- setGatewayStatus(prevStatus => prevStatus !== "error" ? "error" : prevStatus);
588
- setGatewayError(prevError => prevError !== errorMessage ? errorMessage : prevError);
589
- }
590
- }
591
- };
592
-
593
- // Initial check
594
- checkHealth();
595
-
596
- // Set up interval for periodic checks
597
- const interval = setInterval(checkHealth, 15000);
598
-
599
- return () => {
600
- cancelled = true;
601
- clearInterval(interval);
602
- };
603
- }, []);
604
-
605
- const gatewayChip = useMemo(() => (
606
- <Tooltip
607
- title={
608
- gatewayStatus === "error"
609
- ? \`Gateway health check failed. Last error: __GATEWAY_ERROR__\`
610
- : gatewayStatus === "healthy"
611
- ? "Gateway reachable"
612
- : "Checking gateway health..."
613
- }
614
- >
615
- <Chip
616
- size="small"
617
- color={gatewayStatus === "healthy" ? "success" : gatewayStatus === "error" ? "error" : "default"}
618
- variant={gatewayStatus === "healthy" ? "filled" : "outlined"}
619
- label={
620
- gatewayStatus === "healthy"
621
- ? "Gateway: Healthy"
622
- : gatewayStatus === "error"
623
- ? "Gateway: Unreachable"
624
- : "Gateway: Checking..."
625
- }
626
- sx={{ fontWeight: 600 }}
627
- />
628
- </Tooltip>
629
- ), [gatewayStatus, gatewayError]);
630
-
631
- const handleOpenModal = useCallback(() => setIsModalOpen(true), []);
632
- const handleCloseModal = useCallback(() => setIsModalOpen(false), []);
633
-
634
- const HomePage = ({ onOpenModal }: { onOpenModal: () => void }) => (
635
- <Container maxWidth="lg" sx={{ py: { xs: 4, md: 6 } }}>
636
- <Stack spacing={{ xs: 5, md: 7 }}>
637
- <Box
638
- sx={{
639
- display: "flex",
640
- flexDirection: { xs: "column-reverse", md: "row" },
641
- alignItems: "center",
642
- gap: { xs: 4, md: 8 },
643
- }}
644
- >
645
- <Stack spacing={3} sx={{ flex: 1, width: "100%" }}>
646
- <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
647
- <Box
648
- component="img"
649
- src={banditHeadLogoUrl}
650
- alt="Bandit AI logo"
651
- sx={{ height: 48, width: 48, borderRadius: 3, boxShadow: "0 18px 50px rgba(99, 102, 241, 0.35)" }}
652
- />
653
- <Typography variant="overline" color="primary.light" sx={{ letterSpacing: 2 }}>
654
- Powered by Bandit Engine
655
- </Typography>
656
- </Box>
657
- <Typography variant="h3" fontWeight={700}>
658
- {brandingText}
659
- </Typography>
660
- <Typography variant="body1" color="text.secondary">
661
- Build, brand, and launch your assistant with a drop-in chat surface plus a secure gateway for Bandit AI, OpenAI, Azure OpenAI, Anthropic, XAI, or Ollama.
662
- </Typography>
663
- <Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
664
- <Button component={RouterLink} to="/chat" variant="contained" color="primary">
665
- Go to chat demo
666
- </Button>
667
- <Button variant="outlined" color="secondary" onClick={onOpenModal}>
668
- Open modal assistant
669
- </Button>
670
- </Stack>
671
- </Stack>
672
- <Box
673
- component="img"
674
- src={burtsonLabsLogoUrl}
675
- alt="Burtson Labs logo"
676
- sx={{
677
- width: "100%",
678
- maxWidth: 320,
679
- mx: { xs: "auto", md: 0 },
680
- display: "block",
681
- filter: "drop-shadow(0 25px 45px rgba(15, 23, 42, 0.45))",
682
- }}
683
- />
684
- </Box>
685
- <Box
686
- sx={{
687
- display: "grid",
688
- gap: 3,
689
- gridTemplateColumns: { xs: "1fr", md: "repeat(3, minmax(0, 1fr))" },
690
- }}
691
- >
692
- <Card sx={{ height: "100%", backdropFilter: "blur(12px)", backgroundColor: "rgba(15, 23, 42, 0.64)" }}>
693
- <CardContent>
694
- <Typography variant="h6" gutterBottom>
695
- Configure in minutes
696
- </Typography>
697
- <Typography variant="body2" color="text.secondary">
698
- Edit <code>public/config.json</code> and <code>.env</code> to tailor models, personas, and branding for your product.
699
- </Typography>
700
- </CardContent>
701
- </Card>
702
- <Card sx={{ height: "100%", backdropFilter: "blur(12px)", backgroundColor: "rgba(15, 23, 42, 0.64)" }}>
703
- <CardContent>
704
- <Typography variant="h6" gutterBottom>
705
- Ship secure gateways
706
- </Typography>
707
- <Typography variant="body2" color="text.secondary">
708
- Keep API keys server-side while proxying requests to Bandit AI, OpenAI, Azure OpenAI, Anthropic, XAI, or Ollama through the included Express gateway.
709
- </Typography>
710
- </CardContent>
711
- </Card>
712
- <Card sx={{ height: "100%", backdropFilter: "blur(12px)", backgroundColor: "rgba(15, 23, 42, 0.64)" }}>
713
- <CardContent>
714
- <Typography variant="h6" gutterBottom>
715
- Manage knowledge freely
716
- </Typography>
717
- <Typography variant="body2" color="text.secondary">
718
- Add memories, files, and tools directly in the management console that ships with this quickstart.
719
- </Typography>
720
- <Button component={RouterLink} to="/management" variant="text" color="secondary" sx={{ mt: 2, px: 0 }}>
721
- Explore management console
722
- </Button>
723
- </CardContent>
724
- </Card>
725
- </Box>
726
- </Stack>
727
- </Container>
728
- );
729
-
730
- const ChatPage = ({ onOpenModal }: { onOpenModal: () => void }) => (
731
- <Container maxWidth="lg" sx={{ py: 4, display: "flex", flexDirection: "column", gap: 3 }}>
732
- <Stack
733
- direction={{ xs: "column", md: "row" }}
734
- spacing={2}
735
- alignItems={{ xs: "stretch", md: "center" }}
736
- justifyContent="space-between"
737
- >
738
- <Box>
739
- <Typography variant="h4" fontWeight={700} gutterBottom>
740
- Chat demo
741
- </Typography>
742
- <Typography variant="body2" color="text.secondary">
743
- This route renders the full <code>{\`<Chat />\`}</code> surface powered by your quickstart gateway.
744
- </Typography>
745
- </Box>
746
- <Stack direction={{ xs: "column", sm: "row" }} spacing={1.5}>
747
- <Button component={RouterLink} to="/management" variant="outlined" color="secondary">
748
- Management console
749
- </Button>
750
- <Button variant="contained" color="primary" onClick={onOpenModal}>
751
- Open modal assistant
752
- </Button>
753
- </Stack>
754
- </Stack>
755
- <Box
756
- sx={{
757
- flexGrow: 1,
758
- minHeight: 540,
759
- borderRadius: 3,
760
- overflow: "hidden",
761
- boxShadow: "0 35px 90px rgba(15, 23, 42, 0.55)",
762
- }}
763
- >
764
- <Chat />
765
- </Box>
766
- </Container>
767
- );
768
-
769
- const Header = ({ gatewayChip }: { gatewayChip: ReactNode }) => (
770
- <AppBar
771
- position="sticky"
772
- color="transparent"
773
- elevation={0}
774
- sx={{ borderBottom: "1px solid rgba(148, 163, 184, 0.16)", backdropFilter: "blur(18px)" }}
775
- >
776
- <Toolbar sx={{ gap: 2, flexWrap: "wrap" }}>
777
- <Button
778
- component={RouterLink}
779
- to="/"
780
- color="inherit"
781
- sx={{
782
- display: "flex",
783
- alignItems: "center",
784
- gap: 1.5,
785
- py: 1,
786
- px: 1.5,
787
- borderRadius: 2,
788
- textTransform: "none",
789
- bgcolor: "transparent",
790
- "&:hover": { bgcolor: "rgba(99, 102, 241, 0.12)" },
791
- }}
792
- >
793
- <Typography variant="h6" sx={{ fontWeight: 600 }}>
794
- {brandingText}
795
- </Typography>
796
- </Button>
797
- <Box sx={{ flexGrow: 1 }} />
798
- {gatewayChip}
799
- <Stack direction="row" spacing={1} flexWrap="wrap" justifyContent="flex-end">
800
- <Button component={RouterLink} to="/" color="inherit">
801
- Home
802
- </Button>
803
- <Button component={RouterLink} to="/management" color="inherit">
804
- Management
805
- </Button>
806
- <Button component={RouterLink} to="/chat" variant="contained" color="primary">
807
- Go to chat
808
- </Button>
809
- </Stack>
810
- </Toolbar>
811
- </AppBar>
812
- );
813
-
814
- return (
815
- <Routes>
816
- <Route path="/management" element={
817
- <ChatProvider packageSettings={packageSettings}>
818
- <Management />
819
- </ChatProvider>
820
- } />
821
- <Route path="/chat" element={
822
- <ThemeProvider theme={banditQuickstartTheme}>
823
- <CssBaseline />
824
- <ChatProvider packageSettings={packageSettings}>
825
- <Box display="flex" flexDirection="column" minHeight="100vh">
826
- <Box component="main" sx={{ flexGrow: 1, display: "flex" }}>
827
- <ChatPage onOpenModal={handleOpenModal} />
828
- </Box>
829
- <ChatModal open={isModalOpen} onClose={handleCloseModal} />
830
- </Box>
831
- </ChatProvider>
832
- </ThemeProvider>
833
- } />
834
- <Route path="/*" element={
835
- <ThemeProvider theme={banditQuickstartTheme}>
836
- <CssBaseline />
837
- <ChatProvider packageSettings={packageSettings}>
838
- <Box display="flex" flexDirection="column" minHeight="100vh">
839
- <Header gatewayChip={gatewayChip} />
840
- <Box component="main" sx={{ flexGrow: 1, display: "flex" }}>
841
- <Routes>
842
- <Route path="/" element={<HomePage onOpenModal={handleOpenModal} />} />
843
- <Route path="*" element={<Navigate to="/" replace />} />
844
- </Routes>
845
- </Box>
846
- <ChatModal open={isModalOpen} onClose={handleCloseModal} />
847
- </Box>
848
- </ChatProvider>
849
- </ThemeProvider>
850
- } />
851
- </Routes>
852
- );
853
- }
854
-
855
- export default App;
856
- `;
857
- const withResponse = template.replace(/__RESPONSE_STATUS__/g, responseStatusExpr);
858
- const withGatewayError = withResponse.replace(/__GATEWAY_ERROR__/g, gatewayErrorExpr);
859
- return ensureTrailingNewline(normalizeLineEndings(withGatewayError));
860
- };
861
- var buildBrandingConfig = (ctx) => formatJson({
862
- branding: {
863
- logoBase64: ctx.isDefaultLogo ? null : ctx.logoBase64,
864
- brandingText: ctx.brandingText,
865
- theme: "bandit-dark",
866
- hasTransparentLogo: ctx.isDefaultLogo ? true : ctx.hasTransparentLogo
867
- },
868
- knowledgeDocs: []
869
- });
870
- var NEXT_CHAT_ROUTE_TEMPLATE = `import { NextRequest, NextResponse } from "next/server";
871
-
872
- export const dynamic = "force-dynamic";
873
-
874
- const DEFAULT_PROVIDER = "__DEFAULT_PROVIDER__";
875
- const DEFAULT_MODEL = "__DEFAULT_MODEL__";
876
- const FALLBACK_MODEL = __FALLBACK_MODEL__;
877
-
878
- const OLLAMA_URL = (process.env.OLLAMA_URL ?? "http://localhost:11434").replace(/\\/$/, "");
879
- const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
880
- const AZURE_OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT ? process.env.AZURE_OPENAI_ENDPOINT.replace(/\\/$/, "") : undefined;
881
- const AZURE_OPENAI_API_KEY = process.env.AZURE_OPENAI_API_KEY;
882
- const AZURE_OPENAI_API_VERSION = process.env.AZURE_OPENAI_API_VERSION ?? "2024-08-01-preview";
883
- const AZURE_OPENAI_CHAT_DEPLOYMENT = process.env.AZURE_OPENAI_CHAT_DEPLOYMENT;
884
- const AZURE_OPENAI_COMPLETIONS_DEPLOYMENT = process.env.AZURE_OPENAI_COMPLETIONS_DEPLOYMENT ?? AZURE_OPENAI_CHAT_DEPLOYMENT;
885
- const AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT = process.env.AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT;
886
- const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
887
- const ANTHROPIC_BASE_URL = (process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com").replace(/\\/$/, "");
888
- const ANTHROPIC_API_VERSION = process.env.ANTHROPIC_API_VERSION ?? "2023-06-01";
889
- const ANTHROPIC_MAX_TOKENS = Number.isFinite(Number(process.env.ANTHROPIC_MAX_TOKENS))
890
- ? Number(process.env.ANTHROPIC_MAX_TOKENS)
891
- : 1024;
892
- const XAI_API_KEY = process.env.XAI_API_KEY;
893
- const XAI_BASE_URL = (process.env.XAI_BASE_URL ?? "https://api.x.ai/v1").replace(/\\/$/, "");
894
- const BANDIT_API_KEY = process.env.BANDIT_API_KEY;
895
- const BANDIT_BASE_URL = (process.env.BANDIT_BASE_URL ?? "https://api.burtson.ai").replace(/\\/$/, "");
896
-
897
- const normalizeGatewayImageUrl = (value: unknown): string => {
898
- if (!value) {
899
- return "";
900
- }
901
- if (typeof value === "string") {
902
- const trimmed = value.trim();
903
- if (!trimmed) {
904
- return "";
905
- }
906
- if (/^data:/i.test(trimmed) || /^https?:/i.test(trimmed)) {
907
- return trimmed;
908
- }
909
- return 'data:image/jpeg;base64,' + trimmed;
910
- }
911
- if (typeof value === "object") {
912
- const possibleUrl =
913
- typeof (value as { url?: string }).url === "string"
914
- ? (value as { url: string }).url
915
- : (value as { image_url?: { url?: string } }).image_url && typeof (value as { image_url?: { url?: string } }).image_url?.url === "string"
916
- ? ((value as { image_url: { url: string } }).image_url.url)
917
- : "";
918
- return normalizeGatewayImageUrl(possibleUrl);
919
- }
920
- return "";
921
- };
922
-
923
- const extractGatewayImageDetail = (value: unknown): string | undefined => {
924
- if (value && typeof value === "object") {
925
- const record = value as Record<string, unknown>;
926
- if (typeof record.detail === "string" && record.detail.trim()) {
927
- return record.detail;
928
- }
929
- const nested = record.image_url;
930
- if (nested && typeof (nested as Record<string, unknown>).detail === "string" && (nested as Record<string, unknown>).detail.trim()) {
931
- return (nested as Record<string, unknown>).detail as string;
932
- }
933
- }
934
- return undefined;
935
- };
936
-
937
- interface GatewayChatBody {
938
- provider?: string;
939
- model?: string;
940
- messages?: Array<{ role: string; content: unknown }>;
941
- prompt?: string;
942
- stream?: boolean;
943
- temperature?: number;
944
- max_tokens?: number;
945
- top_p?: number;
946
- stop?: string | string[];
947
- stop_sequences?: string | string[];
948
- tools?: unknown;
949
- tool_choice?: unknown;
950
- metadata?: unknown;
951
- thinking?: unknown;
952
- images?: string[];
953
- [key: string]: unknown;
954
- }
955
-
956
- const normalizeProvider = (input: string): "openai" | "azure" | "anthropic" | "ollama" | "xai" | "bandit" => {
957
- const value = input.toLowerCase();
958
- if (value === "azure-openai" || value === "azureopenai" || value === "azure") return "azure";
959
- if (value === "anthropic" || value === "claude") return "anthropic";
960
- if (value === "ollama") return "ollama";
961
- if (value === "xai" || value === "grok") return "xai";
962
- if (value === "bandit" || value === "banditai" || value === "bandit-ai") return "bandit";
963
- return "openai";
964
- };
965
-
966
- const stripPrefix = (model: unknown, prefix: string, fallback: string): string => {
967
- if (typeof model === "string") {
968
- return model.replace(new RegExp(\`^\${prefix}:\`), "");
969
- }
970
- return fallback;
971
- };
972
-
973
- const requireOpenAIKey = () => {
974
- if (!OPENAI_API_KEY) {
975
- throw new Error("Missing OPENAI_API_KEY. Add it to your .env file to route requests to OpenAI.");
976
- }
977
- return OPENAI_API_KEY;
978
- };
979
-
980
- const requireBanditKey = () => {
981
- if (!BANDIT_API_KEY) {
982
- throw new Error("Missing BANDIT_API_KEY. Add it to your .env file to route requests to Bandit AI.");
983
- }
984
- return BANDIT_API_KEY;
985
- };
986
-
987
- const requireXAIKey = () => {
988
- if (!XAI_API_KEY) {
989
- throw new Error("Missing XAI_API_KEY. Add it to your .env file to route requests to xAI.");
990
- }
991
- return XAI_API_KEY;
992
- };
993
-
994
- const requireAnthropicKey = () => {
995
- if (!ANTHROPIC_API_KEY) {
996
- throw new Error("Missing ANTHROPIC_API_KEY. Add it to your .env file to route requests to Anthropic.");
997
- }
998
- return ANTHROPIC_API_KEY;
999
- };
1000
-
1001
- const isAzureConfigured = () => Boolean(AZURE_OPENAI_ENDPOINT && AZURE_OPENAI_API_KEY);
1002
-
1003
- const requireAzureBaseConfig = () => {
1004
- if (!AZURE_OPENAI_ENDPOINT) {
1005
- throw new Error("Missing AZURE_OPENAI_ENDPOINT. Add it to your .env file to route requests to Azure OpenAI.");
1006
- }
1007
- if (!AZURE_OPENAI_API_KEY) {
1008
- throw new Error("Missing AZURE_OPENAI_API_KEY. Add it to your .env file to route requests to Azure OpenAI.");
1009
- }
1010
- return {
1011
- endpoint: AZURE_OPENAI_ENDPOINT,
1012
- apiKey: AZURE_OPENAI_API_KEY,
1013
- };
1014
- };
1015
-
1016
- const buildAzureDeploymentUrl = (deployment: string | undefined, suffix: string) => {
1017
- if (!deployment) {
1018
- throw new Error(\`Missing Azure OpenAI \${suffix.split("/")[0]} deployment name.\`);
1019
- }
1020
- const { endpoint } = requireAzureBaseConfig();
1021
- const normalized = suffix.replace(/^\\/+/, "");
1022
- return \`\${endpoint}/openai/deployments/\${deployment}/\${normalized}?api-version=\${AZURE_OPENAI_API_VERSION}\`;
1023
- };
1024
-
1025
- const resolveAzureDeployment = (model: unknown, fallback: string | undefined, kind: "chat" | "completions" | "embeddings") => {
1026
- const explicit = typeof model === "string" ? model.replace(/^azure:/, "") : undefined;
1027
- if (explicit) return explicit;
1028
- if (kind === "embeddings") return AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT ?? fallback;
1029
- if (kind === "completions") return AZURE_OPENAI_COMPLETIONS_DEPLOYMENT ?? fallback;
1030
- return AZURE_OPENAI_CHAT_DEPLOYMENT ?? fallback;
1031
- };
1032
-
1033
- const flattenGatewayContent = (content: unknown): string => {
1034
- if (typeof content === "string") return content;
1035
- if (Array.isArray(content)) {
1036
- return content
1037
- .map((part) => {
1038
- if (typeof part === "string") return part;
1039
- if (part && typeof part === "object" && "type" in part) {
1040
- const typed = part as { type?: string; text?: string; image_url?: { url?: string } };
1041
- if (typed.type === "text" && typeof typed.text === "string") return typed.text;
1042
- if (typed.type === "image_url" && typed.image_url?.url) return \`[Image: \${typed.image_url.url}]\`;
1043
- }
1044
- return JSON.stringify(part ?? {});
1045
- })
1046
- .join("\\n");
1047
- }
1048
- if (content && typeof content === "object") return JSON.stringify(content);
1049
- return "";
1050
- };
1051
-
1052
- const toAnthropicMessages = (messages: Array<{ role: string; content: unknown }> = []) => {
1053
- const anthropicMessages: Array<{ role: "user" | "assistant"; content: Array<{ type: "text"; text: string }> }> = [];
1054
- let systemPrompt = "";
1055
-
1056
- for (const message of messages) {
1057
- if (!message) continue;
1058
- const text = flattenGatewayContent(message.content);
1059
- if (message.role === "system") {
1060
- systemPrompt = systemPrompt ? \`\${systemPrompt}\\n\\n\${text}\` : text;
1061
- continue;
1062
- }
1063
- const role = message.role === "assistant" ? "assistant" : "user";
1064
- anthropicMessages.push({
1065
- role,
1066
- content: [{ type: "text", text }],
1067
- });
1068
- }
1069
-
1070
- return { messages: anthropicMessages, system: systemPrompt || undefined };
1071
- };
1072
-
1073
- const convertAnthropicResponseToGateway = (responseBody: any, modelName: string) => {
1074
- if (!responseBody) {
1075
- return {
1076
- id: \`anthropic-\${Date.now()}\`,
1077
- object: "chat.completion",
1078
- created: Math.floor(Date.now() / 1000),
1079
- model: modelName.startsWith("anthropic:") ? modelName : \`anthropic:\${modelName}\`,
1080
- choices: [],
1081
- };
1082
- }
1083
-
1084
- const textContent = Array.isArray(responseBody.content)
1085
- ? responseBody.content
1086
- .filter((item: any) => item && item.type === "text" && typeof item.text === "string")
1087
- .map((item: any) => item.text)
1088
- .join("\\n")
1089
- : typeof responseBody.content === "string"
1090
- ? responseBody.content
1091
- : "";
1092
-
1093
- const promptTokens = responseBody.usage?.input_tokens ?? 0;
1094
- const completionTokens = responseBody.usage?.output_tokens ?? 0;
1095
-
1096
- return {
1097
- id: responseBody.id ?? \`anthropic-\${Date.now()}\`,
1098
- object: "chat.completion",
1099
- created: Math.floor(Date.now() / 1000),
1100
- model: modelName.startsWith("anthropic:") ? modelName : \`anthropic:\${modelName}\`,
1101
- choices: [
1102
- {
1103
- index: 0,
1104
- message: {
1105
- role: responseBody.role ?? "assistant",
1106
- content: textContent,
1107
- },
1108
- finish_reason: responseBody.stop_reason ?? responseBody.stop_sequence ?? null,
1109
- },
1110
- ],
1111
- usage: responseBody.usage
1112
- ? {
1113
- prompt_tokens: promptTokens,
1114
- completion_tokens: completionTokens,
1115
- total_tokens: promptTokens + completionTokens,
1116
- }
1117
- : undefined,
1118
- };
1119
- };
1120
-
1121
- const passthroughResponse = (upstream: Response) => {
1122
- const headers = new Headers(upstream.headers);
1123
- return new Response(upstream.body, {
1124
- status: upstream.status,
1125
- statusText: upstream.statusText,
1126
- headers,
1127
- });
1128
- };
1129
-
1130
- const jsonResponse = async (upstream: Response) => {
1131
- const data = await upstream.json().catch(async () => ({ raw: await upstream.text() }));
1132
- return NextResponse.json(data, { status: upstream.status });
1133
- };
1134
-
1135
- const errorResponse = (status: number, error: unknown) =>
1136
- NextResponse.json(
1137
- {
1138
- error: error instanceof Error ? error.message : String(error ?? "Unknown error"),
1139
- },
1140
- { status }
1141
- );
1142
-
1143
- export async function POST(request: NextRequest) {
1144
- const body = (await request.json()) as GatewayChatBody;
1145
- const provider = normalizeProvider(body.provider ?? DEFAULT_PROVIDER);
1146
- const stream = body.stream !== false;
1147
-
1148
- try {
1149
- switch (provider) {
1150
- case "openai": {
1151
- const openaiKey = requireOpenAIKey();
1152
- const { provider: _provider, ...cleanBody } = body;
1153
- const requestBody = {
1154
- ...cleanBody,
1155
- stream,
1156
- model: stripPrefix(body.model ?? DEFAULT_MODEL, "openai", "gpt-4o"),
1157
- };
1158
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
1159
- method: "POST",
1160
- headers: {
1161
- "Content-Type": "application/json",
1162
- Authorization: \`Bearer \${openaiKey}\`,
1163
- },
1164
- body: JSON.stringify(requestBody),
1165
- });
1166
- if (!response.ok) {
1167
- const details = await response.text();
1168
- return NextResponse.json({ error: \`OpenAI chat failed: \${response.status}\`, details }, { status: response.status });
1169
- }
1170
- return stream ? passthroughResponse(response) : jsonResponse(response);
1171
- }
1172
-
1173
- case "bandit": {
1174
- const banditKey = requireBanditKey();
1175
- const { provider: _provider, ...cleanBody } = body;
1176
- const providerName = typeof body.provider === "string" ? body.provider : "bandit";
1177
- const requestBody = {
1178
- ...cleanBody,
1179
- stream,
1180
- model: stripPrefix(body.model ?? DEFAULT_MODEL, "bandit", "bandit-core-1"),
1181
- };
1182
-
1183
- if (
1184
- providerName !== "ollama" &&
1185
- Array.isArray(requestBody.images) &&
1186
- requestBody.images.length > 0 &&
1187
- Array.isArray(requestBody.messages)
1188
- ) {
1189
- const lastUserIndex = requestBody.messages.map((message) => message?.role).lastIndexOf("user");
1190
- if (lastUserIndex !== -1) {
1191
- const targetMessage = requestBody.messages[lastUserIndex] ?? {};
1192
- const baseContent = Array.isArray(targetMessage.content)
1193
- ? targetMessage.content.filter(Boolean)
1194
- : typeof targetMessage.content === "string" && targetMessage.content.trim().length > 0
1195
- ? [{ type: "text", text: targetMessage.content }]
1196
- : [];
1197
-
1198
- const imageContent = requestBody.images
1199
- .map((entry) => {
1200
- const url = normalizeGatewayImageUrl(entry);
1201
- if (!url) {
1202
- return null;
1203
- }
1204
- return {
1205
- type: "image_url",
1206
- image_url: {
1207
- url,
1208
- detail: extractGatewayImageDetail(entry) ?? "auto"
1209
- }
1210
- };
1211
- })
1212
- .filter(Boolean);
1213
-
1214
- if (imageContent.length > 0) {
1215
- requestBody.messages[lastUserIndex] = {
1216
- ...targetMessage,
1217
- content: [...baseContent, ...imageContent]
1218
- };
1219
- }
1220
- }
1221
- delete requestBody.images;
1222
- }
1223
-
1224
- const response = await fetch(BANDIT_BASE_URL + "/completions", {
1225
- method: "POST",
1226
- headers: {
1227
- "Content-Type": "application/json",
1228
- Authorization: \`Bearer \${banditKey}\`,
1229
- },
1230
- body: JSON.stringify(requestBody),
1231
- });
1232
- if (!response.ok) {
1233
- const details = await response.text();
1234
- return NextResponse.json({ error: \`Bandit chat failed: \${response.status}\`, details }, { status: response.status });
1235
- }
1236
- return stream ? passthroughResponse(response) : jsonResponse(response);
1237
- }
1238
-
1239
- case "xai": {
1240
- const xaiKey = requireXAIKey();
1241
- const { provider: _provider, ...cleanBody } = body;
1242
- const requestBody = {
1243
- ...cleanBody,
1244
- stream,
1245
- model: stripPrefix(body.model ?? DEFAULT_MODEL, "xai", "grok-2-latest"),
1246
- };
1247
- const response = await fetch(XAI_BASE_URL + "/chat/completions", {
1248
- method: "POST",
1249
- headers: {
1250
- "Content-Type": "application/json",
1251
- Authorization: "Bearer " + xaiKey,
1252
- },
1253
- body: JSON.stringify(requestBody),
1254
- });
1255
- if (!response.ok) {
1256
- const details = await response.text();
1257
- return NextResponse.json({ error: "xAI chat failed: " + response.status, details }, { status: response.status });
1258
- }
1259
- return stream ? passthroughResponse(response) : jsonResponse(response);
1260
- }
1261
-
1262
- case "anthropic": {
1263
- const anthropicKey = requireAnthropicKey();
1264
- const requestedModel = stripPrefix(body.model ?? DEFAULT_MODEL, "anthropic", "claude-3-5-haiku-latest");
1265
- const stopSequences = Array.isArray(body.stop)
1266
- ? body.stop
1267
- : Array.isArray(body.stop_sequences)
1268
- ? body.stop_sequences
1269
- : body.stop
1270
- ? [body.stop]
1271
- : undefined;
1272
- const { messages, system } = toAnthropicMessages(Array.isArray(body.messages) ? body.messages : []);
1273
- const fallbackText = typeof body.prompt === "string" && body.prompt.trim().length > 0
1274
- ? body.prompt
1275
- : "Hello from Bandit quickstart gateway";
1276
-
1277
- const requestBody: Record<string, unknown> = {
1278
- model: requestedModel,
1279
- messages: messages.length > 0
1280
- ? messages
1281
- : [
1282
- {
1283
- role: "user",
1284
- content: [{ type: "text", text: fallbackText }],
1285
- },
1286
- ],
1287
- stream,
1288
- max_tokens: typeof body.max_tokens === "number" && body.max_tokens > 0 ? body.max_tokens : ANTHROPIC_MAX_TOKENS,
1289
- };
1290
-
1291
- if (system) requestBody.system = system;
1292
- if (typeof body.temperature === "number") requestBody.temperature = body.temperature;
1293
- if (typeof body.top_p === "number") requestBody.top_p = body.top_p;
1294
- if (typeof body.top_k === "number") requestBody.top_k = body.top_k;
1295
- if (stopSequences) requestBody.stop_sequences = stopSequences;
1296
- if (body.metadata) requestBody.metadata = body.metadata;
1297
- if (body.tools) requestBody.tools = body.tools;
1298
- if (body.tool_choice) requestBody.tool_choice = body.tool_choice;
1299
- if (body.thinking) requestBody.thinking = body.thinking;
1300
-
1301
- const response = await fetch(\`\${ANTHROPIC_BASE_URL}/v1/messages\`, {
1302
- method: "POST",
1303
- headers: {
1304
- "Content-Type": "application/json",
1305
- "x-api-key": anthropicKey,
1306
- "anthropic-version": ANTHROPIC_API_VERSION,
1307
- },
1308
- body: JSON.stringify(requestBody),
1309
- });
1310
-
1311
- if (!response.ok) {
1312
- const details = await response.text();
1313
- return NextResponse.json({ error: \`Anthropic chat failed: \${response.status}\`, details }, { status: response.status });
1314
- }
1315
-
1316
- if (stream) {
1317
- return passthroughResponse(response);
1318
- }
1319
-
1320
- const data = await response.json();
1321
- const normalized = convertAnthropicResponseToGateway(data, requestedModel);
1322
- return NextResponse.json(normalized);
1323
- }
1324
-
1325
- case "azure": {
1326
- const { apiKey } = requireAzureBaseConfig();
1327
- const deployment = resolveAzureDeployment(body.model, AZURE_OPENAI_CHAT_DEPLOYMENT, "chat");
1328
- const { provider: _provider, model: _model, ...cleanBody } = body;
1329
- const requestBody = {
1330
- ...cleanBody,
1331
- stream,
1332
- };
1333
-
1334
- const response = await fetch(buildAzureDeploymentUrl(deployment, "chat/completions"), {
1335
- method: "POST",
1336
- headers: {
1337
- "Content-Type": "application/json",
1338
- "api-key": apiKey,
1339
- },
1340
- body: JSON.stringify(requestBody),
1341
- });
1342
-
1343
- if (!response.ok) {
1344
- const details = await response.text();
1345
- return NextResponse.json({ error: \`Azure OpenAI chat failed: \${response.status}\`, details }, { status: response.status });
1346
- }
1347
-
1348
- return stream ? passthroughResponse(response) : jsonResponse(response);
1349
- }
1350
-
1351
- case "ollama": {
1352
- const { provider: _provider, ...cleanBody } = body;
1353
- const requestBody = {
1354
- ...cleanBody,
1355
- stream,
1356
- model: stripPrefix(body.model ?? DEFAULT_MODEL, "ollama", "llama3.1"),
1357
- };
1358
-
1359
- const response = await fetch(\`\${OLLAMA_URL}/api/chat\`, {
1360
- method: "POST",
1361
- headers: {
1362
- "Content-Type": "application/json",
1363
- },
1364
- body: JSON.stringify(requestBody),
1365
- });
1366
-
1367
- if (!response.ok) {
1368
- const details = await response.text();
1369
- return NextResponse.json({ error: \`Ollama chat failed: \${response.status}\`, details }, { status: response.status });
1370
- }
1371
-
1372
- return stream ? passthroughResponse(response) : jsonResponse(response);
1373
- }
1374
-
1375
- default:
1376
- return errorResponse(400, \`Unsupported provider: \${provider}\`);
1377
- }
1378
- } catch (error) {
1379
- const message = error instanceof Error ? error.message : String(error);
1380
- const status = message.startsWith("Missing") ? 400 : 500;
1381
- return errorResponse(status, error);
1382
- }
1383
- }
1384
-
1385
- `;
1386
- var NEXT_HEALTH_ROUTE_TEMPLATE = `import { NextResponse } from "next/server";
1387
-
1388
- export const dynamic = "force-dynamic";
1389
-
1390
- const QUICKSTART_VERSION = "0.1.0";
1391
- const OLLAMA_URL = (process.env.OLLAMA_URL ?? "http://localhost:11434").replace(/\\/$/, "");
1392
- const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
1393
- const AZURE_OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT ? process.env.AZURE_OPENAI_ENDPOINT.replace(/\\/$/, "") : undefined;
1394
- const AZURE_OPENAI_API_KEY = process.env.AZURE_OPENAI_API_KEY;
1395
- const AZURE_OPENAI_API_VERSION = process.env.AZURE_OPENAI_API_VERSION ?? "2024-08-01-preview";
1396
- const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
1397
- const ANTHROPIC_BASE_URL = (process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com").replace(/\\/$/, "");
1398
- const ANTHROPIC_API_VERSION = process.env.ANTHROPIC_API_VERSION ?? "2023-06-01";
1399
- const XAI_API_KEY = process.env.XAI_API_KEY;
1400
- const XAI_BASE_URL = (process.env.XAI_BASE_URL ?? "https://api.x.ai/v1").replace(/\\/$/, "");
1401
-
1402
- const isAzureConfigured = () => Boolean(AZURE_OPENAI_ENDPOINT && AZURE_OPENAI_API_KEY);
1403
-
1404
- const buildAzurePath = (path: string) => {
1405
- const normalized = path.replace(/^\\/+/, "");
1406
- if (!AZURE_OPENAI_ENDPOINT) {
1407
- throw new Error("Missing AZURE_OPENAI_ENDPOINT. Add it to your .env file to route requests to Azure OpenAI.");
1408
- }
1409
- return \`\${AZURE_OPENAI_ENDPOINT}/openai/\${normalized}?api-version=\${AZURE_OPENAI_API_VERSION}\`;
1410
- };
1411
-
1412
- export async function GET() {
1413
- const providers: Array<Record<string, unknown>> = [];
1414
-
1415
- // OpenAI
1416
- try {
1417
- if (OPENAI_API_KEY) {
1418
- const response = await fetch("https://api.openai.com/v1/models", {
1419
- headers: { Authorization: \`Bearer \${OPENAI_API_KEY}\` },
1420
- });
1421
- providers.push({
1422
- name: "openai",
1423
- status: response.ok ? "healthy" : "unhealthy",
1424
- provider: "openai",
1425
- });
1426
- } else {
1427
- providers.push({
1428
- name: "openai",
1429
- status: "unconfigured",
1430
- provider: "openai",
1431
- error: "API key not configured",
1432
- });
1433
- }
1434
- } catch (error) {
1435
- providers.push({
1436
- name: "openai",
1437
- status: "unhealthy",
1438
- provider: "openai",
1439
- error: error instanceof Error ? error.message : String(error),
1440
- });
1441
- }
1442
-
1443
- // Azure
1444
- if (AZURE_OPENAI_ENDPOINT || AZURE_OPENAI_API_KEY) {
1445
- if (!isAzureConfigured()) {
1446
- providers.push({
1447
- name: "azure",
1448
- status: "unconfigured",
1449
- provider: "azure",
1450
- error: "Endpoint or API key not configured",
1451
- endpoint: AZURE_OPENAI_ENDPOINT,
1452
- });
1453
- } else {
1454
- try {
1455
- const response = await fetch(buildAzurePath("deployments"), {
1456
- headers: { "api-key": AZURE_OPENAI_API_KEY ?? "" },
1457
- });
1458
- providers.push({
1459
- name: "azure",
1460
- status: response.ok ? "healthy" : "unhealthy",
1461
- provider: "azure",
1462
- endpoint: AZURE_OPENAI_ENDPOINT,
1463
- });
1464
- } catch (error) {
1465
- providers.push({
1466
- name: "azure",
1467
- status: "unhealthy",
1468
- provider: "azure",
1469
- endpoint: AZURE_OPENAI_ENDPOINT,
1470
- error: error instanceof Error ? error.message : String(error),
1471
- });
1472
- }
1473
- }
1474
- } else {
1475
- providers.push({
1476
- name: "azure",
1477
- status: "unconfigured",
1478
- provider: "azure",
1479
- error: "Endpoint or API key not configured",
1480
- });
1481
- }
1482
-
1483
- // Anthropic
1484
- if (ANTHROPIC_API_KEY) {
1485
- try {
1486
- const response = await fetch(\`\${ANTHROPIC_BASE_URL}/v1/models\`, {
1487
- headers: {
1488
- "x-api-key": ANTHROPIC_API_KEY,
1489
- "anthropic-version": ANTHROPIC_API_VERSION,
1490
- },
1491
- });
1492
- providers.push({
1493
- name: "anthropic",
1494
- status: response.ok ? "healthy" : "unhealthy",
1495
- provider: "anthropic",
1496
- endpoint: ANTHROPIC_BASE_URL,
1497
- });
1498
- } catch (error) {
1499
- providers.push({
1500
- name: "anthropic",
1501
- status: "unhealthy",
1502
- provider: "anthropic",
1503
- endpoint: ANTHROPIC_BASE_URL,
1504
- error: error instanceof Error ? error.message : String(error),
1505
- });
1506
- }
1507
- } else {
1508
- providers.push({
1509
- name: "anthropic",
1510
- status: "unconfigured",
1511
- provider: "anthropic",
1512
- error: "API key not configured",
1513
- });
1514
- }
1515
-
1516
- // xAI
1517
- if (XAI_API_KEY) {
1518
- try {
1519
- const response = await fetch(XAI_BASE_URL + "/models", {
1520
- headers: { Authorization: "Bearer " + XAI_API_KEY },
1521
- });
1522
- providers.push({
1523
- name: "xai",
1524
- status: response.ok ? "healthy" : "unhealthy",
1525
- provider: "xai",
1526
- endpoint: XAI_BASE_URL,
1527
- });
1528
- } catch (error) {
1529
- providers.push({
1530
- name: "xai",
1531
- status: "unhealthy",
1532
- provider: "xai",
1533
- endpoint: XAI_BASE_URL,
1534
- error: error instanceof Error ? error.message : String(error),
1535
- });
1536
- }
1537
- } else {
1538
- providers.push({
1539
- name: "xai",
1540
- status: "unconfigured",
1541
- provider: "xai",
1542
- error: "API key not configured",
1543
- endpoint: XAI_BASE_URL,
1544
- });
1545
- }
1546
-
1547
- // Ollama
1548
- try {
1549
- const response = await fetch(\`\${OLLAMA_URL}/api/tags\`);
1550
- providers.push({
1551
- name: "ollama",
1552
- status: response.ok ? "healthy" : "unhealthy",
1553
- provider: "ollama",
1554
- url: OLLAMA_URL,
1555
- });
1556
- } catch (error) {
1557
- providers.push({
1558
- name: "ollama",
1559
- status: "offline",
1560
- provider: "ollama",
1561
- url: OLLAMA_URL,
1562
- error: error instanceof Error ? error.message : String(error),
1563
- });
1564
- }
1565
-
1566
- const overallHealthy = providers.some((provider) => provider.status === "healthy");
1567
-
1568
- return NextResponse.json({
1569
- status: overallHealthy ? "healthy" : "unhealthy",
1570
- version: QUICKSTART_VERSION,
1571
- uptime: Math.round(process.uptime()),
1572
- providers,
1573
- });
1574
- }
1575
-
1576
- `;
1577
- var NEXT_MODELS_ROUTE_TEMPLATE = `import { NextResponse } from "next/server";
1578
-
1579
- export const dynamic = "force-dynamic";
1580
-
1581
- const BASE_GATEWAY_MODELS = __GATEWAY_MODELS__;
1582
-
1583
- export function toGatewayModels() {
1584
- return BASE_GATEWAY_MODELS.map((model) => ({
1585
- ...model,
1586
- created: Date.now(),
1587
- modified_at: new Date().toISOString(),
1588
- size: 0,
1589
- digest: "",
1590
- details: {
1591
- format: "chat",
1592
- family: model.provider,
1593
- families: [model.provider],
1594
- parameter_size: "",
1595
- quantization_level: "",
1596
- },
1597
- }));
1598
- }
1599
-
1600
- export async function GET() {
1601
- return NextResponse.json({ models: toGatewayModels() });
1602
- }
1603
-
1604
- `;
1605
- var NEXT_GATEWAY_README_TEMPLATE = `# Next.js Gateway API
1606
-
1607
- This directory contains a minimal Next.js App Router implementation of the Bandit gateway API. It mirrors the Express gateway in
1608
- \`server/gateway.js\` but is ready to drop into a Next.js project.
1609
-
1610
- ## Routes
1611
-
1612
- - \`app/api/health/route.ts\` \u2013 provider health and availability checks
1613
- - \`app/api/chat/completions/route.ts\` \u2013 provider-aware chat completions endpoint (Bandit AI, OpenAI, Azure OpenAI, Anthropic, xAI, Ollama)
1614
- - \`app/api/models/route.ts\` \u2013 exposes the scaffolded gateway model metadata used by the frontend
1615
-
1616
- ## Usage
1617
-
1618
- 1. Copy the contents of \`server/next-app/\` into the \`app/\` directory of a Next.js project.
1619
- 2. Ensure the environment variables listed in \`.env.example\` are available to the Next.js runtime. At minimum you will want the
1620
- provider API keys you plan to use (Bandit AI, OpenAI, Azure OpenAI, Anthropic, xAI, or Ollama).
1621
- 3. Start Next.js with \`npm run dev\` (or your project\u2019s equivalent). The routes are server-only (\`export const dynamic = "force-dynamic"\`)
1622
- and can coexist with any frontend pages.
1623
-
1624
- The generated routes favour clarity over cleverness so you can extend them with custom auth, logging, and provider routing logic.
1625
- `;
1626
- var buildNextChatRoute = (ctx) => {
1627
- const fallbackModel = ctx.fallbackModelId ? `"${ctx.fallbackModelId}"` : "undefined";
1628
- return ensureTrailingNewline(
1629
- normalizeLineEndings(
1630
- NEXT_CHAT_ROUTE_TEMPLATE.replace(/__DEFAULT_PROVIDER__/g, ctx.defaultProvider).replace(/__DEFAULT_MODEL__/g, ctx.defaultModelId).replace(/__FALLBACK_MODEL__/g, fallbackModel)
1631
- )
1632
- );
1633
- };
1634
- var buildNextHealthRoute = () => ensureTrailingNewline(normalizeLineEndings(NEXT_HEALTH_ROUTE_TEMPLATE));
1635
- var buildNextModelsRoute = (ctx) => {
1636
- const modelsDefinition = JSON.stringify(ctx.gatewayModels, null, 2);
1637
- return ensureTrailingNewline(
1638
- normalizeLineEndings(
1639
- NEXT_MODELS_ROUTE_TEMPLATE.replace("__GATEWAY_MODELS__", modelsDefinition)
1640
- )
1641
- );
1642
- };
1643
- var buildNextGatewayReadme = () => ensureTrailingNewline(normalizeLineEndings(NEXT_GATEWAY_README_TEMPLATE));
1644
- var buildGatewayServer = (ctx) => {
1645
- const modelsDefinition = JSON.stringify(ctx.gatewayModels, null, 2);
1646
- const gatewaySource = `import express from "express";
1647
- import cors from "cors";
1648
- import dotenv from "dotenv";
1649
-
1650
- dotenv.config();
1651
-
1652
- const app = express();
1653
- app.use(cors());
1654
- app.use(express.json({ limit: '50mb' }));
1655
- app.use(express.urlencoded({ limit: '50mb', extended: true }));
1656
-
1657
- const QUICKSTART_VERSION = "0.1.0";
1658
- const DEFAULT_PROVIDER = "${ctx.defaultProvider}";
1659
- const BASE_GATEWAY_MODELS = ${modelsDefinition};
1660
- const BANDIT_API_KEY = process.env.BANDIT_API_KEY;
1661
- const BANDIT_BASE_URL = (process.env.BANDIT_BASE_URL ?? "https://api.burtson.ai").replace(/\\/$/, "");
1662
- const OLLAMA_BASE_URL = (process.env.OLLAMA_URL ?? "http://localhost:11434").replace(/\\/$/, "");
1663
- const AZURE_OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT ? process.env.AZURE_OPENAI_ENDPOINT.replace(/\\/$/, "") : undefined;
1664
- const AZURE_OPENAI_API_KEY = process.env.AZURE_OPENAI_API_KEY;
1665
- const AZURE_OPENAI_API_VERSION = process.env.AZURE_OPENAI_API_VERSION ?? "2024-08-01-preview";
1666
- const AZURE_OPENAI_CHAT_DEPLOYMENT = process.env.AZURE_OPENAI_CHAT_DEPLOYMENT;
1667
- const AZURE_OPENAI_COMPLETIONS_DEPLOYMENT = process.env.AZURE_OPENAI_COMPLETIONS_DEPLOYMENT ?? AZURE_OPENAI_CHAT_DEPLOYMENT;
1668
- const AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT = process.env.AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT;
1669
- const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
1670
- const ANTHROPIC_BASE_URL = (process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com").replace(/\\/$/, "");
1671
- const ANTHROPIC_API_VERSION = process.env.ANTHROPIC_API_VERSION ?? "2023-06-01";
1672
- const ANTHROPIC_MAX_TOKENS = Number.isFinite(Number(process.env.ANTHROPIC_MAX_TOKENS))
1673
- ? Number(process.env.ANTHROPIC_MAX_TOKENS)
1674
- : 1024;
1675
- const XAI_API_KEY = process.env.XAI_API_KEY;
1676
- const XAI_BASE_URL = (process.env.XAI_BASE_URL ?? "https://api.x.ai/v1").replace(/\\/$/, "");
1677
-
1678
- const toGatewayModels = () =>
1679
- BASE_GATEWAY_MODELS.map((model) => ({
1680
- ...model,
1681
- created: Date.now(),
1682
- modified_at: new Date().toISOString(),
1683
- size: 0,
1684
- digest: "",
1685
- details: {
1686
- format: "chat",
1687
- family: model.provider,
1688
- families: [model.provider],
1689
- parameter_size: "",
1690
- quantization_level: "",
1691
- },
1692
- }));
1693
-
1694
- const stripAzureModelPrefix = (value) =>
1695
- typeof value === "string" ? value.replace(/^azure:/, "") : undefined;
1696
-
1697
- const requireBanditKey = () => {
1698
- if (!BANDIT_API_KEY) {
1699
- throw new Error("Missing BANDIT_API_KEY. Add it to your .env file to route requests to Bandit AI.");
1700
- }
1701
- return BANDIT_API_KEY;
1702
- };
1703
-
1704
- const isAzureConfigured = () => Boolean(AZURE_OPENAI_ENDPOINT && AZURE_OPENAI_API_KEY);
1705
-
1706
- const requireAzureBaseConfig = () => {
1707
- if (!AZURE_OPENAI_ENDPOINT) {
1708
- throw new Error("Missing AZURE_OPENAI_ENDPOINT. Add it to your .env file to route requests to Azure OpenAI.");
1709
- }
1710
- if (!AZURE_OPENAI_API_KEY) {
1711
- throw new Error("Missing AZURE_OPENAI_API_KEY. Add it to your .env file to route requests to Azure OpenAI.");
1712
- }
1713
- return {
1714
- endpoint: AZURE_OPENAI_ENDPOINT,
1715
- apiKey: AZURE_OPENAI_API_KEY,
1716
- apiVersion: AZURE_OPENAI_API_VERSION,
1717
- };
1718
- };
1719
-
1720
- const resolveAzureDeployment = (explicitValue, fallbackValue, kind) => {
1721
- const fromRequest = stripAzureModelPrefix(explicitValue);
1722
- if (fromRequest) {
1723
- return fromRequest;
1724
- }
1725
- if (fallbackValue) {
1726
- return fallbackValue;
1727
- }
1728
- throw new Error(\`Missing Azure OpenAI \${kind} deployment name. Set AZURE_OPENAI_\${kind.toUpperCase()}_DEPLOYMENT in your .env file.\`);
1729
- };
1730
-
1731
- const buildAzureDeploymentUrl = (deployment, suffix) => {
1732
- const { endpoint } = requireAzureBaseConfig();
1733
- const normalizedSuffix = suffix.replace(/^\\//, "");
1734
- return \`\${endpoint}/openai/deployments/\${deployment}/\${normalizedSuffix}?api-version=\${AZURE_OPENAI_API_VERSION}\`;
1735
- };
1736
-
1737
- const buildAzurePath = (suffix) => {
1738
- const { endpoint } = requireAzureBaseConfig();
1739
- const normalizedSuffix = suffix.replace(/^\\//, "");
1740
- const hasQuery = normalizedSuffix.includes("?");
1741
- const separator = hasQuery ? "&" : "?";
1742
- return \`\${endpoint}/openai/\${normalizedSuffix}\${separator}api-version=\${AZURE_OPENAI_API_VERSION}\`;
1743
- };
1744
-
1745
- const stripAnthropicModelPrefix = (value) =>
1746
- typeof value === "string" ? value.replace(/^anthropic:/, "") : undefined;
1747
-
1748
- const isAnthropicConfigured = () => Boolean(ANTHROPIC_API_KEY);
1749
-
1750
- const requireAnthropicKey = () => {
1751
- if (!ANTHROPIC_API_KEY) {
1752
- throw new Error("Missing ANTHROPIC_API_KEY. Add it to your .env file to route requests to Anthropic.");
1753
- }
1754
- return ANTHROPIC_API_KEY;
1755
- };
1756
-
1757
- const buildAnthropicUrl = (path) => {
1758
- const normalized = path.replace(/^\\//, "");
1759
- return \`\${ANTHROPIC_BASE_URL}/v1/\${normalized}\`;
1760
- };
1761
-
1762
- const buildAnthropicHeaders = () => ({
1763
- "Content-Type": "application/json",
1764
- "x-api-key": requireAnthropicKey(),
1765
- "anthropic-version": ANTHROPIC_API_VERSION,
1766
- });
1767
-
1768
- const flattenGatewayContent = (content) => {
1769
- if (typeof content === "string") {
1770
- return content;
1771
- }
1772
- if (Array.isArray(content)) {
1773
- return content
1774
- .map((part) => {
1775
- if (typeof part === "string") {
1776
- return part;
1777
- }
1778
- if (part?.type === "text" && typeof part.text === "string") {
1779
- return part.text;
1780
- }
1781
- if (part?.type === "image_url" && part.image_url?.url) {
1782
- return \`[Image: \${part.image_url.url}]\`;
1783
- }
1784
- return JSON.stringify(part ?? {});
1785
- })
1786
- .join("\\n");
1787
- }
1788
- if (content && typeof content === "object") {
1789
- return JSON.stringify(content);
1790
- }
1791
- return "";
1792
- };
1793
-
1794
- const normalizeGatewayImageUrl = (value) => {
1795
- if (!value) {
1796
- return "";
1797
- }
1798
- if (typeof value === "string") {
1799
- const trimmed = value.trim();
1800
- if (!trimmed) {
1801
- return "";
1802
- }
1803
- if (/^data:/i.test(trimmed) || /^https?:/i.test(trimmed)) {
1804
- return trimmed;
1805
- }
1806
- return 'data:image/jpeg;base64,' + trimmed;
1807
- }
1808
- if (typeof value === "object") {
1809
- const possibleUrl =
1810
- typeof value.url === "string"
1811
- ? value.url
1812
- : value.image_url && typeof value.image_url.url === "string"
1813
- ? value.image_url.url
1814
- : "";
1815
- return normalizeGatewayImageUrl(possibleUrl);
1816
- }
1817
- return "";
1818
- };
1819
-
1820
- const extractGatewayImageDetail = (value) => {
1821
- if (value && typeof value === "object") {
1822
- const record = value;
1823
- if (typeof record.detail === "string" && record.detail.trim()) {
1824
- return record.detail;
1825
- }
1826
- if (record.image_url && typeof record.image_url.detail === "string" && record.image_url.detail.trim()) {
1827
- return record.image_url.detail;
1828
- }
1829
- }
1830
- return undefined;
1831
- };
1832
-
1833
- const toAnthropicMessages = (messages = []) => {
1834
- const anthropicMessages = [];
1835
- let systemPrompt = "";
1836
-
1837
- for (const message of messages) {
1838
- if (!message) continue;
1839
- const text = flattenGatewayContent(message.content);
1840
-
1841
- if (message.role === "system") {
1842
- systemPrompt = systemPrompt ? \`\${systemPrompt}\\n\\n\${text}\` : text;
1843
- continue;
1844
- }
1845
-
1846
- const role = message.role === "assistant" ? "assistant" : "user";
1847
- anthropicMessages.push({
1848
- role,
1849
- content: [{ type: "text", text }],
1850
- });
1851
- }
1852
-
1853
- return { messages: anthropicMessages, system: systemPrompt || undefined };
1854
- };
1855
-
1856
- const convertAnthropicResponseToGateway = (responseBody, modelName) => {
1857
- if (!responseBody) {
1858
- return {
1859
- id: \`anthropic-\${Date.now()}\`,
1860
- object: "chat.completion",
1861
- created: Math.floor(Date.now() / 1000),
1862
- model: modelName.startsWith("anthropic:") ? modelName : \`anthropic:\${modelName}\`,
1863
- choices: [],
1864
- };
1865
- }
1866
-
1867
- const textContent = Array.isArray(responseBody.content)
1868
- ? responseBody.content
1869
- .filter((item) => item && item.type === "text" && typeof item.text === "string")
1870
- .map((item) => item.text)
1871
- .join("\\n")
1872
- : typeof responseBody.content === "string"
1873
- ? responseBody.content
1874
- : "";
1875
-
1876
- const promptTokens = responseBody.usage?.input_tokens ?? 0;
1877
- const completionTokens = responseBody.usage?.output_tokens ?? 0;
1878
-
1879
- return {
1880
- id: responseBody.id ?? \`anthropic-\${Date.now()}\`,
1881
- object: "chat.completion",
1882
- created: Math.floor(Date.now() / 1000),
1883
- model: modelName.startsWith("anthropic:") ? modelName : \`anthropic:\${modelName}\`,
1884
- choices: [
1885
- {
1886
- index: 0,
1887
- message: {
1888
- role: responseBody.role ?? "assistant",
1889
- content: textContent,
1890
- },
1891
- finish_reason: responseBody.stop_reason ?? responseBody.stop_sequence ?? null,
1892
- },
1893
- ],
1894
- usage: responseBody.usage
1895
- ? {
1896
- prompt_tokens: promptTokens,
1897
- completion_tokens: completionTokens,
1898
- total_tokens: promptTokens + completionTokens,
1899
- }
1900
- : undefined,
1901
- };
1902
- };
1903
-
1904
- const convertAnthropicResponseToGenerate = (responseBody, modelName) => {
1905
- const gatewayResponse = convertAnthropicResponseToGateway(responseBody, modelName);
1906
- const content = gatewayResponse.choices?.[0]?.message?.content ?? "";
1907
- return {
1908
- model: gatewayResponse.model,
1909
- created_at: new Date().toISOString(),
1910
- response: content,
1911
- done: true,
1912
- total_duration: 0,
1913
- load_duration: 0,
1914
- prompt_eval_count: gatewayResponse.usage?.prompt_tokens ?? 0,
1915
- prompt_eval_duration: 0,
1916
- eval_count: gatewayResponse.usage?.completion_tokens ?? 0,
1917
- eval_duration: 0,
1918
- };
1919
- };
1920
-
1921
- const requireOpenAIKey = () => {
1922
- const key = process.env.OPENAI_API_KEY;
1923
- if (!key) {
1924
- throw new Error("Missing OPENAI_API_KEY. Add it to your .env file to route requests to OpenAI.");
1925
- }
1926
- return key;
1927
- };
1928
-
1929
- const requireXAIKey = () => {
1930
- const key = XAI_API_KEY;
1931
- if (!key) {
1932
- throw new Error("Missing XAI_API_KEY. Add it to your .env file to route requests to xAI.");
1933
- }
1934
- return key;
1935
- };
1936
-
1937
- // Utility function to handle streaming responses
1938
- const handleStreamingResponse = async (upstreamResponse, res) => {
1939
- res.setHeader('Content-Type', 'text/event-stream');
1940
- res.setHeader('Cache-Control', 'no-cache');
1941
- res.setHeader('Connection', 'keep-alive');
1942
- res.setHeader('Access-Control-Allow-Origin', '*');
1943
-
1944
- try {
1945
- // Get the readable stream from the response
1946
- const reader = upstreamResponse.body.getReader();
1947
-
1948
- while (true) {
1949
- const { done, value } = await reader.read();
1950
- if (done) break;
1951
-
1952
- // Write the chunk to the response
1953
- res.write(value);
1954
- }
1955
-
1956
- res.end();
1957
- } catch (error) {
1958
- console.error('Streaming error:', error);
1959
- // Fallback to non-streaming
1960
- const text = await upstreamResponse.text();
1961
- res.send(text);
1962
- }
1963
- };
1964
-
1965
- const relayAnthropicStream = async (upstreamResponse, res) => {
1966
- res.setHeader('Content-Type', 'text/event-stream');
1967
- res.setHeader('Cache-Control', 'no-cache');
1968
- res.setHeader('Connection', 'keep-alive');
1969
- res.setHeader('Access-Control-Allow-Origin', '*');
1970
-
1971
- const reader = upstreamResponse.body?.getReader();
1972
- if (!reader) {
1973
- const fallback = await upstreamResponse.text();
1974
- res.write("data: " + JSON.stringify({ choices: [{ delta: { content: fallback } }] }) + "\\n\\n");
1975
- res.write("data: [DONE]\\n\\n");
1976
- return res.end();
1977
- }
1978
-
1979
- const decoder = new TextDecoder();
1980
- let buffer = '';
1981
-
1982
- const sendChunk = (payload) => {
1983
- res.write("data: " + JSON.stringify(payload) + "\\n\\n");
1984
- };
1985
-
1986
- try {
1987
- while (true) {
1988
- const { value, done } = await reader.read();
1989
- if (done) break;
1990
- buffer += decoder.decode(value, { stream: true });
1991
-
1992
- let delimiterIndex;
1993
- while ((delimiterIndex = buffer.indexOf('\\n\\n')) >= 0) {
1994
- const rawEvent = buffer.slice(0, delimiterIndex).trim();
1995
- buffer = buffer.slice(delimiterIndex + 2);
1996
- if (!rawEvent) continue;
1997
-
1998
- const lines = rawEvent.split('\\n');
1999
- const eventLine = lines.find((line) => line.startsWith('event:')) ?? '';
2000
- const dataLine = lines.find((line) => line.startsWith('data:')) ?? '';
2001
- const event = eventLine.replace('event:', '').trim();
2002
- const trimmedData = dataLine.replace('data:', '').trim();
2003
-
2004
- if (!trimmedData) {
2005
- continue;
2006
- }
2007
-
2008
- let parsed;
2009
- try {
2010
- parsed = JSON.parse(trimmedData);
2011
- } catch (error) {
2012
- console.error('Anthropic stream parse error', error, { rawEvent });
2013
- continue;
2014
- }
2015
-
2016
- if (event === 'content_block_delta') {
2017
- const textChunk = parsed?.delta?.text ?? '';
2018
- if (textChunk) {
2019
- sendChunk({
2020
- choices: [
2021
- {
2022
- delta: {
2023
- content: textChunk,
2024
- },
2025
- },
2026
- ],
2027
- });
2028
- }
2029
- } else if (event === 'message_stop') {
2030
- sendChunk({
2031
- choices: [
2032
- {
2033
- delta: {},
2034
- finish_reason: 'stop',
2035
- },
2036
- ],
2037
- });
2038
- }
2039
- }
2040
- }
2041
- } catch (error) {
2042
- console.error('Anthropic streaming relay error', error);
2043
- sendChunk({
2044
- error: error instanceof Error ? error.message : String(error),
2045
- });
2046
- } finally {
2047
- res.write("data: [DONE]\\n\\n");
2048
- res.end();
2049
- }
2050
- };
2051
-
2052
- // ============================================================================
2053
- // GENERAL HEALTH & MODELS
2054
- // ============================================================================
2055
-
2056
- app.get("/api/health", async (_req, res) => {
2057
- const providers = [];
2058
-
2059
- // Check OpenAI
2060
- try {
2061
- const openaiKey = process.env.OPENAI_API_KEY;
2062
- if (openaiKey) {
2063
- const response = await fetch("https://api.openai.com/v1/models", {
2064
- headers: { "Authorization": \`Bearer \${openaiKey}\` }
2065
- });
2066
- providers.push({
2067
- name: "openai",
2068
- status: response.ok ? "healthy" : "unhealthy",
2069
- provider: "openai"
2070
- });
2071
- } else {
2072
- providers.push({
2073
- name: "openai",
2074
- status: "unconfigured",
2075
- provider: "openai",
2076
- error: "API key not configured"
2077
- });
2078
- }
2079
- } catch (error) {
2080
- providers.push({
2081
- name: "openai",
2082
- status: "unhealthy",
2083
- provider: "openai",
2084
- error: error.message
2085
- });
2086
- }
2087
-
2088
- // Check Azure OpenAI
2089
- if (AZURE_OPENAI_ENDPOINT || AZURE_OPENAI_API_KEY) {
2090
- if (!isAzureConfigured()) {
2091
- providers.push({
2092
- name: "azure",
2093
- status: "unconfigured",
2094
- provider: "azure",
2095
- error: "Endpoint or API key not configured",
2096
- endpoint: AZURE_OPENAI_ENDPOINT
2097
- });
2098
- } else {
2099
- try {
2100
- const { endpoint } = requireAzureBaseConfig();
2101
- const deploymentsUrl = buildAzurePath("deployments");
2102
- const response = await fetch(deploymentsUrl, {
2103
- headers: { "api-key": AZURE_OPENAI_API_KEY }
2104
- });
2105
- providers.push({
2106
- name: "azure",
2107
- status: response.ok ? "healthy" : "unhealthy",
2108
- provider: "azure",
2109
- endpoint
2110
- });
2111
- } catch (error) {
2112
- providers.push({
2113
- name: "azure",
2114
- status: "unhealthy",
2115
- provider: "azure",
2116
- error: error instanceof Error ? error.message : String(error),
2117
- endpoint: AZURE_OPENAI_ENDPOINT
2118
- });
2119
- }
2120
- }
2121
- } else {
2122
- providers.push({
2123
- name: "azure",
2124
- status: "unconfigured",
2125
- provider: "azure",
2126
- error: "Endpoint or API key not configured"
2127
- });
2128
- }
2129
-
2130
- // Check Anthropic
2131
- if (ANTHROPIC_API_KEY) {
2132
- try {
2133
- const response = await fetch(buildAnthropicUrl("models"), {
2134
- headers: buildAnthropicHeaders(),
2135
- method: "GET"
2136
- });
2137
- providers.push({
2138
- name: "anthropic",
2139
- status: response.ok ? "healthy" : "unhealthy",
2140
- provider: "anthropic",
2141
- endpoint: ANTHROPIC_BASE_URL
2142
- });
2143
- } catch (error) {
2144
- providers.push({
2145
- name: "anthropic",
2146
- status: "unhealthy",
2147
- provider: "anthropic",
2148
- error: error instanceof Error ? error.message : String(error),
2149
- endpoint: ANTHROPIC_BASE_URL
2150
- });
2151
- }
2152
- } else {
2153
- providers.push({
2154
- name: "anthropic",
2155
- status: "unconfigured",
2156
- provider: "anthropic",
2157
- error: "API key not configured"
2158
- });
2159
- }
2160
-
2161
- // Check xAI
2162
- if (XAI_API_KEY) {
2163
- try {
2164
- const response = await fetch(XAI_BASE_URL + "/models", {
2165
- headers: { "Authorization": "Bearer " + XAI_API_KEY }
2166
- });
2167
- providers.push({
2168
- name: "xai",
2169
- status: response.ok ? "healthy" : "unhealthy",
2170
- provider: "xai",
2171
- endpoint: XAI_BASE_URL
2172
- });
2173
- } catch (error) {
2174
- providers.push({
2175
- name: "xai",
2176
- status: "unhealthy",
2177
- provider: "xai",
2178
- error: error instanceof Error ? error.message : String(error),
2179
- endpoint: XAI_BASE_URL
2180
- });
2181
- }
2182
- } else {
2183
- providers.push({
2184
- name: "xai",
2185
- status: "unconfigured",
2186
- provider: "xai",
2187
- error: "API key not configured",
2188
- endpoint: XAI_BASE_URL
2189
- });
2190
- }
2191
-
2192
- // Check Ollama
2193
- try {
2194
- console.log(\`Checking Ollama health at: \${OLLAMA_BASE_URL}/api/tags\`);
2195
- const response = await fetch(\`\${OLLAMA_BASE_URL}/api/tags\`);
2196
- const status = response.ok ? "healthy" : "unhealthy";
2197
- console.log(\`Ollama health check result: \${status}\`);
2198
- providers.push({
2199
- name: "ollama",
2200
- status: status,
2201
- provider: "ollama",
2202
- url: OLLAMA_BASE_URL
2203
- });
2204
- } catch (error) {
2205
- console.log(\`Ollama health check error: \${error.message}\`);
2206
- providers.push({
2207
- name: "ollama",
2208
- status: "offline",
2209
- provider: "ollama",
2210
- error: error.message,
2211
- url: OLLAMA_BASE_URL
2212
- });
2213
- }
2214
-
2215
- const overallHealthy = providers.some(p => p.status === "healthy");
2216
-
2217
- res.json({
2218
- status: overallHealthy ? "healthy" : "unhealthy",
2219
- version: QUICKSTART_VERSION,
2220
- uptime: Math.round(process.uptime()),
2221
- providers
2222
- });
2223
- });
2224
-
2225
- app.get("/api/models", (_req, res) => {
2226
- res.json({ models: toGatewayModels() });
2227
- });
2228
-
2229
- // ============================================================================
2230
- // ANTHROPIC ROUTES
2231
- // ============================================================================
2232
-
2233
- app.get("/api/anthropic/health", async (_req, res) => {
2234
- try {
2235
- requireAnthropicKey();
2236
- const response = await fetch(buildAnthropicUrl("models"), {
2237
- method: "GET",
2238
- headers: buildAnthropicHeaders()
2239
- });
2240
- const isHealthy = response.ok;
2241
- res.json({
2242
- status: isHealthy ? "healthy" : "unhealthy",
2243
- anthropic_status: isHealthy,
2244
- provider: "anthropic",
2245
- endpoint: ANTHROPIC_BASE_URL
2246
- });
2247
- } catch (error) {
2248
- const message = error instanceof Error ? error.message : String(error);
2249
- res.status(503).json({
2250
- status: "unhealthy",
2251
- anthropic_status: false,
2252
- provider: "anthropic",
2253
- error: message,
2254
- endpoint: ANTHROPIC_BASE_URL
2255
- });
2256
- }
2257
- });
2258
-
2259
- app.post("/api/anthropic/chat/completions", async (req, res) => {
2260
- try {
2261
- requireAnthropicKey();
2262
- const rawBody = req.body ?? {};
2263
- const isStreaming = rawBody.stream === true;
2264
- const requestedModel =
2265
- stripAnthropicModelPrefix(rawBody.model) ??
2266
- stripAnthropicModelPrefix("${ctx.defaultModelId}") ??
2267
- "claude-3-5-haiku-latest";
2268
-
2269
- const stopSequences = Array.isArray(rawBody.stop)
2270
- ? rawBody.stop
2271
- : Array.isArray(rawBody.stop_sequences)
2272
- ? rawBody.stop_sequences
2273
- : rawBody.stop
2274
- ? [rawBody.stop]
2275
- : undefined;
2276
-
2277
- const { messages: anthropicMessages, system } = toAnthropicMessages(
2278
- Array.isArray(rawBody.messages) ? rawBody.messages : []
2279
- );
2280
-
2281
- const fallbackText =
2282
- typeof rawBody.prompt === "string" && rawBody.prompt.trim().length > 0
2283
- ? rawBody.prompt
2284
- : "Hello from Bandit quickstart gateway";
2285
-
2286
- const requestBody = {
2287
- model: requestedModel,
2288
- messages:
2289
- anthropicMessages.length > 0
2290
- ? anthropicMessages
2291
- : [
2292
- {
2293
- role: "user",
2294
- content: [{ type: "text", text: fallbackText }],
2295
- },
2296
- ],
2297
- stream: isStreaming,
2298
- max_tokens:
2299
- typeof rawBody.max_tokens === "number" && rawBody.max_tokens > 0
2300
- ? rawBody.max_tokens
2301
- : ANTHROPIC_MAX_TOKENS,
2302
- };
2303
-
2304
- if (system) {
2305
- requestBody.system = system;
2306
- }
2307
- if (typeof rawBody.temperature === "number") {
2308
- requestBody.temperature = rawBody.temperature;
2309
- }
2310
- if (typeof rawBody.top_p === "number") {
2311
- requestBody.top_p = rawBody.top_p;
2312
- }
2313
- if (typeof rawBody.top_k === "number") {
2314
- requestBody.top_k = rawBody.top_k;
2315
- }
2316
- if (stopSequences) {
2317
- requestBody.stop_sequences = stopSequences;
2318
- }
2319
- if (rawBody.metadata) {
2320
- requestBody.metadata = rawBody.metadata;
2321
- }
2322
- if (rawBody.tools) {
2323
- requestBody.tools = rawBody.tools;
2324
- }
2325
- if (rawBody.tool_choice) {
2326
- requestBody.tool_choice = rawBody.tool_choice;
2327
- }
2328
- if (rawBody.thinking) {
2329
- requestBody.thinking = rawBody.thinking;
2330
- }
2331
- if (rawBody.extra_headers) {
2332
- requestBody.extra_headers = rawBody.extra_headers;
2333
- }
2334
-
2335
- const response = await fetch(buildAnthropicUrl("messages"), {
2336
- method: "POST",
2337
- headers: buildAnthropicHeaders(),
2338
- body: JSON.stringify(requestBody),
2339
- });
2340
-
2341
- if (!response.ok) {
2342
- const errorText = await response.text();
2343
- return res.status(response.status).json({
2344
- error: \`Anthropic chat failed: \${response.status}\`,
2345
- details: errorText,
2346
- });
2347
- }
2348
-
2349
- if (isStreaming) {
2350
- await relayAnthropicStream(response, res);
2351
- } else {
2352
- const data = await response.json();
2353
- const normalized = convertAnthropicResponseToGateway(data, requestedModel);
2354
- res.json(normalized);
2355
- }
2356
- } catch (error) {
2357
- const message = error instanceof Error ? error.message : String(error);
2358
- const status = message.startsWith("Missing ANTHROPIC_API_KEY") ? 400 : 500;
2359
- res.status(status).json({ error: message });
2360
- }
2361
- });
2362
-
2363
- app.post("/api/anthropic/chat", async (req, res) => {
2364
- req.url = "/api/anthropic/chat/completions";
2365
- return app._router.handle(req, res);
2366
- });
2367
-
2368
- app.post("/api/anthropic/completions", async (req, res) => {
2369
- try {
2370
- requireAnthropicKey();
2371
- const rawBody = req.body ?? {};
2372
- const isStreaming = rawBody.stream === true;
2373
- const requestedModel =
2374
- stripAnthropicModelPrefix(rawBody.model) ??
2375
- stripAnthropicModelPrefix("${ctx.defaultModelId}") ??
2376
- "claude-3-5-sonnet-latest";
2377
-
2378
- const stopSequences = Array.isArray(rawBody.stop)
2379
- ? rawBody.stop
2380
- : Array.isArray(rawBody.stop_sequences)
2381
- ? rawBody.stop_sequences
2382
- : rawBody.stop
2383
- ? [rawBody.stop]
2384
- : undefined;
2385
-
2386
- const prompt =
2387
- typeof rawBody.prompt === "string" && rawBody.prompt.trim().length > 0
2388
- ? rawBody.prompt
2389
- : "Hello from Bandit quickstart gateway";
2390
-
2391
- const { messages, system } = toAnthropicMessages([
2392
- { role: "user", content: prompt },
2393
- ]);
2394
-
2395
- const requestBody = {
2396
- model: requestedModel,
2397
- messages,
2398
- stream: isStreaming,
2399
- max_tokens:
2400
- typeof rawBody.max_tokens === "number" && rawBody.max_tokens > 0
2401
- ? rawBody.max_tokens
2402
- : ANTHROPIC_MAX_TOKENS,
2403
- };
2404
-
2405
- if (system) {
2406
- requestBody.system = system;
2407
- }
2408
- if (typeof rawBody.temperature === "number") {
2409
- requestBody.temperature = rawBody.temperature;
2410
- }
2411
- if (typeof rawBody.top_p === "number") {
2412
- requestBody.top_p = rawBody.top_p;
2413
- }
2414
- if (typeof rawBody.top_k === "number") {
2415
- requestBody.top_k = rawBody.top_k;
2416
- }
2417
- if (stopSequences) {
2418
- requestBody.stop_sequences = stopSequences;
2419
- }
2420
- if (rawBody.metadata) {
2421
- requestBody.metadata = rawBody.metadata;
2422
- }
2423
- if (rawBody.tools) {
2424
- requestBody.tools = rawBody.tools;
2425
- }
2426
- if (rawBody.tool_choice) {
2427
- requestBody.tool_choice = rawBody.tool_choice;
2428
- }
2429
-
2430
- const response = await fetch(buildAnthropicUrl("messages"), {
2431
- method: "POST",
2432
- headers: buildAnthropicHeaders(),
2433
- body: JSON.stringify(requestBody),
2434
- });
2435
-
2436
- if (!response.ok) {
2437
- const errorText = await response.text();
2438
- return res.status(response.status).json({
2439
- error: \`Anthropic completions failed: \${response.status}\`,
2440
- details: errorText,
2441
- });
2442
- }
2443
-
2444
- if (isStreaming) {
2445
- await relayAnthropicStream(response, res);
2446
- } else {
2447
- const data = await response.json();
2448
- const formatted = convertAnthropicResponseToGenerate(data, requestedModel);
2449
- res.json(formatted);
2450
- }
2451
- } catch (error) {
2452
- const message = error instanceof Error ? error.message : String(error);
2453
- const status = message.startsWith("Missing ANTHROPIC_API_KEY") ? 400 : 500;
2454
- res.status(status).json({ error: message });
2455
- }
2456
- });
2457
-
2458
- app.post("/api/anthropic/generate", async (req, res) => {
2459
- req.url = "/api/anthropic/completions";
2460
- return app._router.handle(req, res);
2461
- });
2462
-
2463
- app.get("/api/anthropic/models", async (_req, res) => {
2464
- try {
2465
- requireAnthropicKey();
2466
- const response = await fetch(buildAnthropicUrl("models"), {
2467
- method: "GET",
2468
- headers: buildAnthropicHeaders(),
2469
- });
2470
-
2471
- if (!response.ok) {
2472
- const errorText = await response.text();
2473
- return res.status(response.status).json({
2474
- error: \`Anthropic models failed: \${response.status}\`,
2475
- details: errorText,
2476
- });
2477
- }
2478
-
2479
- const text = await response.text();
2480
- res.setHeader('Content-Type', 'application/json');
2481
- res.send(text);
2482
- } catch (error) {
2483
- const message = error instanceof Error ? error.message : String(error);
2484
- const status = message.startsWith("Missing ANTHROPIC_API_KEY") ? 400 : 500;
2485
- res.status(status).json({ error: message });
2486
- }
2487
- });
2488
-
2489
- app.post("/api/anthropic/embed", async (_req, res) => {
2490
- res.status(501).json({
2491
- error: "Anthropic embeddings not implemented",
2492
- message: "Add support for the Anthropic embeddings endpoint if your use case requires it."
2493
- });
2494
- });
2495
-
2496
- // ============================================================================
2497
- // AZURE OPENAI ROUTES
2498
- // ============================================================================
2499
-
2500
- app.get("/api/azure/health", async (_req, res) => {
2501
- try {
2502
- const { endpoint } = requireAzureBaseConfig();
2503
- const deploymentsUrl = buildAzurePath("deployments");
2504
- const response = await fetch(deploymentsUrl, {
2505
- headers: { "api-key": AZURE_OPENAI_API_KEY }
2506
- });
2507
- const isHealthy = response.ok;
2508
- res.json({
2509
- status: isHealthy ? "healthy" : "unhealthy",
2510
- azure_status: isHealthy,
2511
- provider: "azure",
2512
- endpoint
2513
- });
2514
- } catch (error) {
2515
- res.status(503).json({
2516
- status: "unhealthy",
2517
- azure_status: false,
2518
- provider: "azure",
2519
- error: error instanceof Error ? error.message : String(error),
2520
- endpoint: AZURE_OPENAI_ENDPOINT
2521
- });
2522
- }
2523
- });
2524
-
2525
- app.post("/api/azure/chat/completions", async (req, res) => {
2526
- try {
2527
- const { apiKey } = requireAzureBaseConfig();
2528
- const deployment = resolveAzureDeployment(req.body?.model, AZURE_OPENAI_CHAT_DEPLOYMENT, "chat");
2529
- const isStreaming = req.body?.stream === true;
2530
- const { provider, model, ...cleanBody } = req.body ?? {};
2531
- const requestBody = { ...cleanBody };
2532
-
2533
- const response = await fetch(buildAzureDeploymentUrl(deployment, "chat/completions"), {
2534
- method: "POST",
2535
- headers: {
2536
- "Content-Type": "application/json",
2537
- "api-key": apiKey
2538
- },
2539
- body: JSON.stringify(requestBody)
2540
- });
2541
-
2542
- if (!response.ok) {
2543
- const errorText = await response.text();
2544
- return res.status(response.status).json({
2545
- error: \`Azure OpenAI chat failed: \${response.status}\`,
2546
- details: errorText
2547
- });
2548
- }
2549
-
2550
- if (isStreaming) {
2551
- await handleStreamingResponse(response, res);
2552
- } else {
2553
- const text = await response.text();
2554
- res.setHeader('Content-Type', 'application/json');
2555
- res.send(text);
2556
- }
2557
- } catch (error) {
2558
- const message = error instanceof Error ? error.message : String(error);
2559
- const status = message.startsWith("Missing Azure OpenAI") ? 400 : 500;
2560
- res.status(status).json({ error: message });
2561
- }
2562
- });
2563
-
2564
- app.post("/api/azure/chat", async (req, res) => {
2565
- req.url = "/api/azure/chat/completions";
2566
- return app._router.handle(req, res);
2567
- });
2568
-
2569
- app.post("/api/azure/completions", async (req, res) => {
2570
- try {
2571
- const { apiKey } = requireAzureBaseConfig();
2572
- const deployment = resolveAzureDeployment(req.body?.model, AZURE_OPENAI_COMPLETIONS_DEPLOYMENT, "completions");
2573
- const isStreaming = req.body?.stream === true;
2574
- const { provider, model, ...cleanBody } = req.body ?? {};
2575
- const requestBody = { ...cleanBody };
2576
-
2577
- const response = await fetch(buildAzureDeploymentUrl(deployment, "completions"), {
2578
- method: "POST",
2579
- headers: {
2580
- "Content-Type": "application/json",
2581
- "api-key": apiKey
2582
- },
2583
- body: JSON.stringify(requestBody)
2584
- });
2585
-
2586
- if (!response.ok) {
2587
- const errorText = await response.text();
2588
- return res.status(response.status).json({
2589
- error: \`Azure OpenAI completions failed: \${response.status}\`,
2590
- details: errorText
2591
- });
2592
- }
2593
-
2594
- if (isStreaming) {
2595
- await handleStreamingResponse(response, res);
2596
- } else {
2597
- const text = await response.text();
2598
- res.setHeader('Content-Type', 'application/json');
2599
- res.send(text);
2600
- }
2601
- } catch (error) {
2602
- const message = error instanceof Error ? error.message : String(error);
2603
- const status = message.startsWith("Missing Azure OpenAI") ? 400 : 500;
2604
- res.status(status).json({ error: message });
2605
- }
2606
- });
2607
-
2608
- app.post("/api/azure/generate", async (req, res) => {
2609
- try {
2610
- const { apiKey } = requireAzureBaseConfig();
2611
- const deployment = resolveAzureDeployment(req.body?.model, AZURE_OPENAI_CHAT_DEPLOYMENT, "chat");
2612
- const prompt = req.body?.prompt || "";
2613
- const isStreaming = req.body?.stream === true;
2614
-
2615
- const chatBody = {
2616
- messages: [
2617
- {
2618
- role: "user",
2619
- content: prompt
2620
- }
2621
- ],
2622
- stream: isStreaming,
2623
- max_tokens: req.body?.max_tokens ?? 150,
2624
- temperature: req.body?.temperature ?? 0.7
2625
- };
2626
-
2627
- const response = await fetch(buildAzureDeploymentUrl(deployment, "chat/completions"), {
2628
- method: "POST",
2629
- headers: {
2630
- "Content-Type": "application/json",
2631
- "api-key": apiKey
2632
- },
2633
- body: JSON.stringify(chatBody)
2634
- });
2635
-
2636
- if (!response.ok) {
2637
- const errorText = await response.text();
2638
- return res.status(response.status).json({
2639
- error: \`Azure OpenAI generate failed: \${response.status}\`,
2640
- details: errorText
2641
- });
2642
- }
2643
-
2644
- if (isStreaming) {
2645
- await handleStreamingResponse(response, res);
2646
- } else {
2647
- const text = await response.text();
2648
- res.setHeader('Content-Type', 'application/json');
2649
- res.send(text);
2650
- }
2651
- } catch (error) {
2652
- const message = error instanceof Error ? error.message : String(error);
2653
- const status = message.startsWith("Missing Azure OpenAI") ? 400 : 500;
2654
- res.status(status).json({ error: message });
2655
- }
2656
- });
2657
-
2658
- app.get("/api/azure/models", async (_req, res) => {
2659
- try {
2660
- requireAzureBaseConfig();
2661
-
2662
- const response = await fetch(buildAzurePath("deployments"), {
2663
- headers: { "api-key": AZURE_OPENAI_API_KEY }
2664
- });
2665
-
2666
- if (!response.ok) {
2667
- const errorText = await response.text();
2668
- return res.status(response.status).json({
2669
- error: \`Azure OpenAI models failed: \${response.status}\`,
2670
- details: errorText
2671
- });
2672
- }
2673
-
2674
- const text = await response.text();
2675
- res.setHeader('Content-Type', 'application/json');
2676
- res.send(text);
2677
- } catch (error) {
2678
- const message = error instanceof Error ? error.message : String(error);
2679
- const status = message.startsWith("Missing Azure OpenAI") ? 400 : 500;
2680
- res.status(status).json({ error: message });
2681
- }
2682
- });
2683
-
2684
- app.post("/api/azure/embed", async (req, res) => {
2685
- try {
2686
- const { apiKey } = requireAzureBaseConfig();
2687
- const deployment = resolveAzureDeployment(req.body?.model, AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT, "embeddings");
2688
- const { provider, model, ...cleanBody } = req.body ?? {};
2689
- const requestBody = { ...cleanBody };
2690
-
2691
- const response = await fetch(buildAzureDeploymentUrl(deployment, "embeddings"), {
2692
- method: "POST",
2693
- headers: {
2694
- "Content-Type": "application/json",
2695
- "api-key": apiKey
2696
- },
2697
- body: JSON.stringify(requestBody)
2698
- });
2699
-
2700
- if (!response.ok) {
2701
- const errorText = await response.text();
2702
- return res.status(response.status).json({
2703
- error: \`Azure OpenAI embed failed: \${response.status}\`,
2704
- details: errorText
2705
- });
2706
- }
2707
-
2708
- const text = await response.text();
2709
- res.setHeader('Content-Type', 'application/json');
2710
- res.send(text);
2711
- } catch (error) {
2712
- const message = error instanceof Error ? error.message : String(error);
2713
- const status = message.startsWith("Missing Azure OpenAI") ? 400 : 500;
2714
- res.status(status).json({ error: message });
2715
- }
2716
- });
2717
-
2718
- // ============================================================================
2719
- // XAI ROUTES
2720
- // ============================================================================
2721
-
2722
- // xAI Health Check
2723
- app.get("/api/xai/health", async (_req, res) => {
2724
- try {
2725
- const xaiKey = requireXAIKey();
2726
- const response = await fetch(XAI_BASE_URL + "/models", {
2727
- headers: { "Authorization": "Bearer " + xaiKey }
2728
- });
2729
- const isHealthy = response.ok;
2730
- res.json({
2731
- status: isHealthy ? "healthy" : "unhealthy",
2732
- xai_status: isHealthy,
2733
- provider: "xai"
2734
- });
2735
- } catch (error) {
2736
- res.status(503).json({
2737
- status: "unhealthy",
2738
- xai_status: false,
2739
- error: error instanceof Error ? error.message : String(error),
2740
- provider: "xai"
2741
- });
2742
- }
2743
- });
2744
-
2745
- // xAI Chat Completions
2746
- app.post("/api/xai/chat/completions", async (req, res) => {
2747
- try {
2748
- const xaiKey = requireXAIKey();
2749
- const isStreaming = req.body?.stream === true;
2750
- const { provider, ...cleanBody } = req.body ?? {};
2751
- const requestBody = {
2752
- ...cleanBody,
2753
- model: req.body?.model?.replace(/^xai:/, "") || "grok-2-latest"
2754
- };
2755
-
2756
- const response = await fetch(XAI_BASE_URL + "/chat/completions", {
2757
- method: "POST",
2758
- headers: {
2759
- "Content-Type": "application/json",
2760
- "Authorization": "Bearer " + xaiKey
2761
- },
2762
- body: JSON.stringify(requestBody)
2763
- });
2764
-
2765
- if (!response.ok) {
2766
- const errorText = await response.text();
2767
- return res.status(response.status).json({
2768
- error: "xAI chat failed: " + response.status,
2769
- details: errorText
2770
- });
2771
- }
2772
-
2773
- if (isStreaming) {
2774
- await handleStreamingResponse(response, res);
2775
- } else {
2776
- const text = await response.text();
2777
- res.setHeader('Content-Type', 'application/json');
2778
- res.send(text);
2779
- }
2780
- } catch (error) {
2781
- res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
2782
- }
2783
- });
2784
-
2785
- app.post("/api/xai/chat", async (req, res) => {
2786
- req.url = "/api/xai/chat/completions";
2787
- return app._router.handle(req, res);
2788
- });
2789
-
2790
- // xAI Completions
2791
- app.post("/api/xai/completions", async (req, res) => {
2792
- try {
2793
- const xaiKey = requireXAIKey();
2794
- const isStreaming = req.body?.stream === true;
2795
- const { provider, ...cleanBody } = req.body ?? {};
2796
- const requestBody = {
2797
- ...cleanBody,
2798
- model: req.body?.model?.replace(/^xai:/, "") || "grok-2-mini"
2799
- };
2800
-
2801
- const response = await fetch(XAI_BASE_URL + "/completions", {
2802
- method: "POST",
2803
- headers: {
2804
- "Content-Type": "application/json",
2805
- "Authorization": "Bearer " + xaiKey
2806
- },
2807
- body: JSON.stringify(requestBody)
2808
- });
2809
-
2810
- if (!response.ok) {
2811
- const errorText = await response.text();
2812
- return res.status(response.status).json({
2813
- error: "xAI completions failed: " + response.status,
2814
- details: errorText
2815
- });
2816
- }
2817
-
2818
- if (isStreaming) {
2819
- await handleStreamingResponse(response, res);
2820
- } else {
2821
- const text = await response.text();
2822
- res.setHeader('Content-Type', 'application/json');
2823
- res.send(text);
2824
- }
2825
- } catch (error) {
2826
- res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
2827
- }
2828
- });
2829
-
2830
- // xAI Generate
2831
- app.post("/api/xai/generate", async (req, res) => {
2832
- try {
2833
- const xaiKey = requireXAIKey();
2834
- const prompt = req.body?.prompt || "";
2835
- const model = req.body?.model?.replace(/^xai:/, "") || "grok-2-latest";
2836
- const isStreaming = req.body?.stream === true;
2837
-
2838
- const chatBody = {
2839
- model,
2840
- messages: [
2841
- { role: "user", content: prompt }
2842
- ],
2843
- stream: isStreaming,
2844
- max_tokens: req.body?.max_tokens || 150,
2845
- temperature: req.body?.temperature ?? 0.7
2846
- };
2847
-
2848
- const response = await fetch(XAI_BASE_URL + "/chat/completions", {
2849
- method: "POST",
2850
- headers: {
2851
- "Content-Type": "application/json",
2852
- "Authorization": "Bearer " + xaiKey
2853
- },
2854
- body: JSON.stringify(chatBody)
2855
- });
2856
-
2857
- if (!response.ok) {
2858
- const errorText = await response.text();
2859
- return res.status(response.status).json({
2860
- error: "xAI generate failed: " + response.status,
2861
- details: errorText
2862
- });
2863
- }
2864
-
2865
- if (isStreaming) {
2866
- await handleStreamingResponse(response, res);
2867
- } else {
2868
- const data = await response.json();
2869
- const generateResponse = {
2870
- model,
2871
- created_at: new Date().toISOString(),
2872
- response: data.choices?.[0]?.message?.content || "",
2873
- done: true,
2874
- context: [],
2875
- total_duration: 0,
2876
- load_duration: 0,
2877
- prompt_eval_count: data.usage?.prompt_tokens || 0,
2878
- prompt_eval_duration: 0,
2879
- eval_count: data.usage?.completion_tokens || 0,
2880
- eval_duration: 0
2881
- };
2882
- res.json(generateResponse);
2883
- }
2884
- } catch (error) {
2885
- res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
2886
- }
2887
- });
2888
-
2889
- // xAI Models
2890
- app.get("/api/xai/models", async (_req, res) => {
2891
- try {
2892
- const xaiKey = requireXAIKey();
2893
- const response = await fetch(XAI_BASE_URL + "/models", {
2894
- headers: { "Authorization": "Bearer " + xaiKey }
2895
- });
2896
-
2897
- if (!response.ok) {
2898
- const errorText = await response.text();
2899
- return res.status(response.status).json({
2900
- error: "xAI models failed: " + response.status,
2901
- details: errorText
2902
- });
2903
- }
2904
-
2905
- const text = await response.text();
2906
- res.setHeader('Content-Type', 'application/json');
2907
- res.send(text);
2908
- } catch (error) {
2909
- res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
2910
- }
2911
- });
2912
-
2913
- // ============================================================================
2914
- // BANDIT AI ROUTES
2915
- // ============================================================================
2916
-
2917
- app.get("/api/bandit/health", async (_req, res) => {
2918
- try {
2919
- const banditKey = requireBanditKey();
2920
- const response = await fetch(BANDIT_BASE_URL + "/models", {
2921
- headers: { "Authorization": \`Bearer \${banditKey}\` }
2922
- });
2923
- const isHealthy = response.ok;
2924
- res.json({
2925
- status: isHealthy ? "healthy" : "unhealthy",
2926
- bandit_status: isHealthy,
2927
- provider: "bandit",
2928
- endpoint: BANDIT_BASE_URL
2929
- });
2930
- } catch (error) {
2931
- res.status(503).json({
2932
- status: "unhealthy",
2933
- bandit_status: false,
2934
- error: error instanceof Error ? error.message : String(error),
2935
- provider: "bandit",
2936
- endpoint: BANDIT_BASE_URL
2937
- });
2938
- }
2939
- });
2940
-
2941
- app.post("/api/bandit/chat/completions", async (req, res) => {
2942
- try {
2943
- const banditKey = requireBanditKey();
2944
- const isStreaming = req.body?.stream === true;
2945
- const { provider, ...cleanBody } = req.body ?? {};
2946
- const providerName = typeof provider === "string" ? provider : "bandit";
2947
- const requestBody = {
2948
- ...cleanBody,
2949
- model: req.body?.model?.replace(/^bandit:/, "") || "bandit-core-1"
2950
- };
2951
-
2952
- if (
2953
- providerName !== "ollama" &&
2954
- Array.isArray(requestBody.images) &&
2955
- requestBody.images.length > 0 &&
2956
- Array.isArray(requestBody.messages)
2957
- ) {
2958
- const lastUserIndex = requestBody.messages.map((message) => message?.role).lastIndexOf("user");
2959
- if (lastUserIndex !== -1) {
2960
- const targetMessage = requestBody.messages[lastUserIndex] ?? {};
2961
- const baseContent = Array.isArray(targetMessage.content)
2962
- ? targetMessage.content.filter(Boolean)
2963
- : typeof targetMessage.content === "string" && targetMessage.content.trim().length > 0
2964
- ? [{ type: "text", text: targetMessage.content }]
2965
- : [];
2966
-
2967
- const imageContent = requestBody.images
2968
- .map((entry) => {
2969
- const url = normalizeGatewayImageUrl(entry);
2970
- if (!url) {
2971
- return null;
2972
- }
2973
- return {
2974
- type: "image_url",
2975
- image_url: {
2976
- url,
2977
- detail: extractGatewayImageDetail(entry) ?? "auto"
2978
- }
2979
- };
2980
- })
2981
- .filter(Boolean);
2982
-
2983
- if (imageContent.length > 0) {
2984
- requestBody.messages[lastUserIndex] = {
2985
- ...targetMessage,
2986
- content: [...baseContent, ...imageContent]
2987
- };
2988
- }
2989
- }
2990
- delete requestBody.images;
2991
- }
2992
-
2993
- const response = await fetch(BANDIT_BASE_URL + "/completions", {
2994
- method: "POST",
2995
- headers: {
2996
- "Content-Type": "application/json",
2997
- "Authorization": \`Bearer \${banditKey}\`
2998
- },
2999
- body: JSON.stringify(requestBody)
3000
- });
3001
-
3002
- if (!response.ok) {
3003
- const errorText = await response.text();
3004
- return res.status(response.status).json({
3005
- error: \`Bandit chat failed: \${response.status}\`,
3006
- details: errorText
3007
- });
3008
- }
3009
-
3010
- if (isStreaming) {
3011
- await handleStreamingResponse(response, res);
3012
- } else {
3013
- const text = await response.text();
3014
- res.setHeader('Content-Type', 'application/json');
3015
- res.send(text);
3016
- }
3017
- } catch (error) {
3018
- res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
3019
- }
3020
- });
3021
-
3022
- app.post("/api/bandit/chat", async (req, res) => {
3023
- req.url = "/api/bandit/chat/completions";
3024
- return app._router.handle(req, res);
3025
- });
3026
-
3027
- app.post("/api/bandit/completions", async (req, res) => {
3028
- try {
3029
- const banditKey = requireBanditKey();
3030
- const isStreaming = req.body?.stream === true;
3031
- const { provider, ...cleanBody } = req.body ?? {};
3032
- const requestBody = {
3033
- ...cleanBody,
3034
- model: req.body?.model?.replace(/^bandit:/, "") || "bandit-core-1"
3035
- };
3036
-
3037
- const response = await fetch(BANDIT_BASE_URL + "/completions", {
3038
- method: "POST",
3039
- headers: {
3040
- "Content-Type": "application/json",
3041
- "Authorization": \`Bearer \${banditKey}\`
3042
- },
3043
- body: JSON.stringify(requestBody)
3044
- });
3045
-
3046
- if (!response.ok) {
3047
- const errorText = await response.text();
3048
- return res.status(response.status).json({
3049
- error: \`Bandit completions failed: \${response.status}\`,
3050
- details: errorText
3051
- });
3052
- }
3053
-
3054
- if (isStreaming) {
3055
- await handleStreamingResponse(response, res);
3056
- } else {
3057
- const text = await response.text();
3058
- res.setHeader('Content-Type', 'application/json');
3059
- res.send(text);
3060
- }
3061
- } catch (error) {
3062
- res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
3063
- }
3064
- });
3065
-
3066
- app.post("/api/bandit/generate", async (req, res) => {
3067
- try {
3068
- const banditKey = requireBanditKey();
3069
- const prompt = req.body?.prompt || "";
3070
- const model = req.body?.model?.replace(/^bandit:/, "") || "bandit-core-1";
3071
- const isStreaming = req.body?.stream === true;
3072
-
3073
- const chatBody = {
3074
- model,
3075
- messages: [
3076
- { role: "user", content: prompt }
3077
- ],
3078
- stream: isStreaming,
3079
- max_tokens: req.body?.max_tokens || 256,
3080
- temperature: req.body?.temperature ?? 0.7
3081
- };
3082
-
3083
- const response = await fetch(BANDIT_BASE_URL + "/completions", {
3084
- method: "POST",
3085
- headers: {
3086
- "Content-Type": "application/json",
3087
- "Authorization": \`Bearer \${banditKey}\`
3088
- },
3089
- body: JSON.stringify(chatBody)
3090
- });
3091
-
3092
- if (!response.ok) {
3093
- const errorText = await response.text();
3094
- return res.status(response.status).json({
3095
- error: \`Bandit generate failed: \${response.status}\`,
3096
- details: errorText
3097
- });
3098
- }
3099
-
3100
- if (isStreaming) {
3101
- await handleStreamingResponse(response, res);
3102
- } else {
3103
- const data = await response.json();
3104
- const generateResponse = {
3105
- model,
3106
- created_at: new Date().toISOString(),
3107
- response: data.choices?.[0]?.message?.content || "",
3108
- done: true,
3109
- context: [],
3110
- total_duration: 0,
3111
- load_duration: 0,
3112
- prompt_eval_count: data.usage?.prompt_tokens || 0,
3113
- prompt_eval_duration: 0,
3114
- eval_count: data.usage?.completion_tokens || 0,
3115
- eval_duration: 0
3116
- };
3117
- res.json(generateResponse);
3118
- }
3119
- } catch (error) {
3120
- res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
3121
- }
3122
- });
3123
-
3124
- app.get("/api/bandit/models", async (_req, res) => {
3125
- try {
3126
- const banditKey = requireBanditKey();
3127
- const response = await fetch(BANDIT_BASE_URL + "/models", {
3128
- headers: { "Authorization": \`Bearer \${banditKey}\` }
3129
- });
3130
-
3131
- if (!response.ok) {
3132
- const errorText = await response.text();
3133
- return res.status(response.status).json({
3134
- error: \`Bandit models failed: \${response.status}\`,
3135
- details: errorText
3136
- });
3137
- }
3138
-
3139
- const text = await response.text();
3140
- res.setHeader('Content-Type', 'application/json');
3141
- res.send(text);
3142
- } catch (error) {
3143
- res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
3144
- }
3145
- });
3146
-
3147
- // ============================================================================
3148
- // OPENAI ROUTES
3149
- // ============================================================================
3150
-
3151
- // OpenAI Health Check
3152
- app.get("/api/openai/health", async (_req, res) => {
3153
- try {
3154
- const openaiKey = process.env.OPENAI_API_KEY;
3155
- if (!openaiKey) {
3156
- return res.status(503).json({
3157
- status: "unhealthy",
3158
- openai_status: false,
3159
- error: "OpenAI API key not configured",
3160
- provider: "openai"
3161
- });
3162
- }
3163
-
3164
- const response = await fetch("https://api.openai.com/v1/models", {
3165
- headers: { "Authorization": \`Bearer \${openaiKey}\` }
3166
- });
3167
-
3168
- const isHealthy = response.ok;
3169
- res.json({
3170
- status: isHealthy ? "healthy" : "unhealthy",
3171
- openai_status: isHealthy,
3172
- provider: "openai"
3173
- });
3174
- } catch (error) {
3175
- res.status(503).json({
3176
- status: "unhealthy",
3177
- openai_status: false,
3178
- error: error.message,
3179
- provider: "openai"
3180
- });
3181
- }
3182
- });
3183
-
3184
- // OpenAI Chat Completions
3185
- app.post("/api/openai/chat/completions", async (req, res) => {
3186
- try {
3187
- const openaiKey = requireOpenAIKey();
3188
- const isStreaming = req.body?.stream === true;
3189
-
3190
- // Strip the openai: prefix from model name and remove provider field
3191
- const { provider, ...cleanBody } = req.body;
3192
- const requestBody = {
3193
- ...cleanBody,
3194
- model: req.body?.model?.replace(/^openai:/, "") || "gpt-4o"
3195
- };
3196
-
3197
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
3198
- method: "POST",
3199
- headers: {
3200
- "Content-Type": "application/json",
3201
- "Authorization": \`Bearer \${openaiKey}\`
3202
- },
3203
- body: JSON.stringify(requestBody)
3204
- });
3205
-
3206
- if (!response.ok) {
3207
- const errorText = await response.text();
3208
- return res.status(response.status).json({
3209
- error: \`OpenAI chat failed: \${response.status}\`,
3210
- details: errorText
3211
- });
3212
- }
3213
-
3214
- if (isStreaming) {
3215
- await handleStreamingResponse(response, res);
3216
- } else {
3217
- const text = await response.text();
3218
- res.setHeader('Content-Type', 'application/json');
3219
- res.send(text);
3220
- }
3221
- } catch (error) {
3222
- res.status(500).json({ error: error.message });
3223
- }
3224
- });
3225
-
3226
- // OpenAI Chat (alternative route)
3227
- app.post("/api/openai/chat", async (req, res) => {
3228
- // Route to the completions endpoint for compatibility
3229
- req.url = "/api/openai/chat/completions";
3230
- return app._router.handle(req, res);
3231
- });
3232
-
3233
- // OpenAI Completions
3234
- app.post("/api/openai/completions", async (req, res) => {
3235
- try {
3236
- const openaiKey = requireOpenAIKey();
3237
- const isStreaming = req.body?.stream === true;
3238
-
3239
- // Strip the openai: prefix from model name and remove provider field
3240
- const { provider, ...cleanBody } = req.body;
3241
- const requestBody = {
3242
- ...cleanBody,
3243
- model: req.body?.model?.replace(/^openai:/, "") || "gpt-3.5-turbo-instruct"
3244
- };
3245
-
3246
- const response = await fetch("https://api.openai.com/v1/completions", {
3247
- method: "POST",
3248
- headers: {
3249
- "Content-Type": "application/json",
3250
- "Authorization": \`Bearer \${openaiKey}\`
3251
- },
3252
- body: JSON.stringify(requestBody)
3253
- });
3254
-
3255
- if (!response.ok) {
3256
- const errorText = await response.text();
3257
- return res.status(response.status).json({
3258
- error: \`OpenAI completions failed: \${response.status}\`,
3259
- details: errorText
3260
- });
3261
- }
3262
-
3263
- if (isStreaming) {
3264
- await handleStreamingResponse(response, res);
3265
- } else {
3266
- const text = await response.text();
3267
- res.setHeader('Content-Type', 'application/json');
3268
- res.send(text);
3269
- }
3270
- } catch (error) {
3271
- res.status(500).json({ error: error.message });
3272
- }
3273
- });
3274
-
3275
- // OpenAI Generate (converts to chat format for conversation starters)
3276
- app.post("/api/openai/generate", async (req, res) => {
3277
- try {
3278
- const openaiKey = requireOpenAIKey();
3279
- const prompt = req.body?.prompt || "";
3280
- const model = req.body?.model?.replace(/^openai:/, "") || "gpt-4o";
3281
- const isStreaming = req.body?.stream === true;
3282
-
3283
- // Convert generate request to chat format
3284
- const chatBody = {
3285
- model: model,
3286
- messages: [
3287
- {
3288
- role: "user",
3289
- content: prompt
3290
- }
3291
- ],
3292
- stream: isStreaming,
3293
- max_tokens: req.body?.max_tokens || 150,
3294
- temperature: req.body?.temperature || 0.7
3295
- };
3296
-
3297
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
3298
- method: "POST",
3299
- headers: {
3300
- "Content-Type": "application/json",
3301
- "Authorization": \`Bearer \${openaiKey}\`
3302
- },
3303
- body: JSON.stringify(chatBody)
3304
- });
3305
-
3306
- if (!response.ok) {
3307
- const errorText = await response.text();
3308
- return res.status(response.status).json({
3309
- error: \`OpenAI generate failed: \${response.status}\`,
3310
- details: errorText
3311
- });
3312
- }
3313
-
3314
- if (isStreaming) {
3315
- await handleStreamingResponse(response, res);
3316
- } else {
3317
- const data = await response.json();
3318
- // Convert chat response back to generate format
3319
- const generateResponse = {
3320
- model: model,
3321
- created_at: new Date().toISOString(),
3322
- response: data.choices?.[0]?.message?.content || "",
3323
- done: true,
3324
- context: [],
3325
- total_duration: 0,
3326
- load_duration: 0,
3327
- prompt_eval_count: 0,
3328
- prompt_eval_duration: 0,
3329
- eval_count: data.usage?.completion_tokens || 0,
3330
- eval_duration: 0
3331
- };
3332
- res.json(generateResponse);
3333
- }
3334
- } catch (error) {
3335
- res.status(500).json({ error: error.message });
3336
- }
3337
- });
3338
-
3339
- // OpenAI Models
3340
- app.get("/api/openai/models", async (_req, res) => {
3341
- try {
3342
- const openaiKey = requireOpenAIKey();
3343
-
3344
- const response = await fetch("https://api.openai.com/v1/models", {
3345
- headers: { "Authorization": \`Bearer \${openaiKey}\` }
3346
- });
3347
-
3348
- if (!response.ok) {
3349
- const errorText = await response.text();
3350
- return res.status(response.status).json({
3351
- error: \`OpenAI models failed: \${response.status}\`,
3352
- details: errorText
3353
- });
3354
- }
3355
-
3356
- const text = await response.text();
3357
- res.setHeader('Content-Type', 'application/json');
3358
- res.send(text);
3359
- } catch (error) {
3360
- res.status(500).json({ error: error.message });
3361
- }
3362
- });
3363
-
3364
- // ============================================================================
3365
- // OLLAMA ROUTES
3366
- // ============================================================================
3367
-
3368
- // Ollama Health Check
3369
- app.get("/api/ollama/health", async (_req, res) => {
3370
- try {
3371
- console.log(\`Ollama health check at: \${OLLAMA_BASE_URL}/api/tags\`);
3372
- const response = await fetch(\`\${OLLAMA_BASE_URL}/api/tags\`);
3373
- const isHealthy = response.ok;
3374
-
3375
- res.json({
3376
- status: isHealthy ? "healthy" : "unhealthy",
3377
- ollama_status: isHealthy,
3378
- provider: "ollama",
3379
- url: OLLAMA_BASE_URL
3380
- });
3381
- } catch (error) {
3382
- console.log(\`Ollama health check error: \${error.message}\`);
3383
- res.status(503).json({
3384
- status: "offline",
3385
- ollama_status: false,
3386
- error: error.message,
3387
- provider: "ollama",
3388
- url: OLLAMA_BASE_URL
3389
- });
3390
- }
3391
- });
3392
-
3393
- // Ollama Chat
3394
- app.post("/api/ollama/chat", async (req, res) => {
3395
- try {
3396
- const response = await fetch(\`\${OLLAMA_BASE_URL}/api/chat\`, {
3397
- method: "POST",
3398
- headers: { "Content-Type": "application/json" },
3399
- body: JSON.stringify(req.body)
3400
- });
3401
-
3402
- if (!response.ok) {
3403
- const errorText = await response.text();
3404
- return res.status(response.status).json({
3405
- error: \`Ollama chat failed: \${response.status}\`,
3406
- details: errorText
3407
- });
3408
- }
3409
-
3410
- const isStreaming = req.body?.stream === true;
3411
- if (isStreaming) {
3412
- await handleStreamingResponse(response, res);
3413
- } else {
3414
- const text = await response.text();
3415
- res.setHeader('Content-Type', 'application/json');
3416
- res.send(text);
3417
- }
3418
- } catch (error) {
3419
- res.status(500).json({ error: error.message });
3420
- }
3421
- });
3422
-
3423
- // Ollama Generate
3424
- app.post("/api/ollama/generate", async (req, res) => {
3425
- try {
3426
- const response = await fetch(\`\${OLLAMA_BASE_URL}/api/generate\`, {
3427
- method: "POST",
3428
- headers: { "Content-Type": "application/json" },
3429
- body: JSON.stringify(req.body)
3430
- });
3431
-
3432
- if (!response.ok) {
3433
- const errorText = await response.text();
3434
- return res.status(response.status).json({
3435
- error: \`Ollama generate failed: \${response.status}\`,
3436
- details: errorText
3437
- });
3438
- }
3439
-
3440
- const isStreaming = req.body?.stream === true;
3441
- if (isStreaming) {
3442
- await handleStreamingResponse(response, res);
3443
- } else {
3444
- const text = await response.text();
3445
- res.setHeader('Content-Type', 'application/json');
3446
- res.send(text);
3447
- }
3448
- } catch (error) {
3449
- res.status(500).json({ error: error.message });
3450
- }
3451
- });
3452
-
3453
- // Ollama Models
3454
- app.get("/api/ollama/models", async (_req, res) => {
3455
- try {
3456
- const response = await fetch(\`\${OLLAMA_BASE_URL}/api/tags\`);
3457
-
3458
- if (!response.ok) {
3459
- const errorText = await response.text();
3460
- return res.status(response.status).json({
3461
- error: \`Ollama models failed: \${response.status}\`,
3462
- details: errorText
3463
- });
3464
- }
3465
-
3466
- const text = await response.text();
3467
- res.setHeader('Content-Type', 'application/json');
3468
- res.send(text);
3469
- } catch (error) {
3470
- res.status(500).json({ error: error.message });
3471
- }
3472
- });
3473
-
3474
- // Ollama Embedding
3475
- app.post("/api/ollama/embed", async (req, res) => {
3476
- try {
3477
- const response = await fetch(\`\${OLLAMA_BASE_URL}/api/embeddings\`, {
3478
- method: "POST",
3479
- headers: { "Content-Type": "application/json" },
3480
- body: JSON.stringify(req.body)
3481
- });
3482
-
3483
- if (!response.ok) {
3484
- const errorText = await response.text();
3485
- return res.status(response.status).json({
3486
- error: \`Ollama embed failed: \${response.status}\`,
3487
- details: errorText
3488
- });
3489
- }
3490
-
3491
- const text = await response.text();
3492
- res.setHeader('Content-Type', 'application/json');
3493
- res.send(text);
3494
- } catch (error) {
3495
- res.status(500).json({ error: error.message });
3496
- }
3497
- });
3498
-
3499
- // ============================================================================
3500
- // TTS ROUTES (not implemented - placeholder for compatibility)
3501
- // ============================================================================
3502
-
3503
- // TTS Main endpoint
3504
- app.post("/api/tts", async (_req, res) => {
3505
- res.status(501).json({
3506
- error: "TTS integration not implemented",
3507
- message: "Text-to-speech functionality is not available in this quickstart gateway"
3508
- });
3509
- });
3510
-
3511
- // TTS Stream endpoint
3512
- app.post("/api/tts/stream", async (_req, res) => {
3513
- res.status(501).json({
3514
- error: "TTS streaming not implemented",
3515
- message: "Text-to-speech streaming functionality is not available in this quickstart gateway"
3516
- });
3517
- });
3518
-
3519
- // TTS Real-time stream endpoint
3520
- app.post("/api/tts/stream-realtime", async (_req, res) => {
3521
- res.status(501).json({
3522
- error: "TTS real-time streaming not implemented",
3523
- message: "Text-to-speech real-time streaming functionality is not available in this quickstart gateway"
3524
- });
3525
- });
3526
-
3527
- // TTS Models endpoint - returns empty models
3528
- app.get("/api/tts/models", async (_req, res) => {
3529
- res.json({
3530
- models: []
3531
- });
3532
- });
3533
-
3534
- // TTS Available models endpoint - returns empty models with defaults
3535
- app.get("/api/tts/available-models", async (_req, res) => {
3536
- res.json({
3537
- models: [],
3538
- defaultModel: null,
3539
- fallbackModel: null
3540
- });
3541
- });
3542
-
3543
- // ============================================================================
3544
- // MCP (Model Context Protocol) TOOL ROUTES
3545
- // ============================================================================
3546
-
3547
- // MCP Health Check
3548
- app.get("/api/mcp/health", async (_req, res) => {
3549
- res.json({
3550
- status: "healthy",
3551
- timestamp: new Date().toISOString(),
3552
- totalTools: 0,
3553
- enabledTools: 0,
3554
- availableTools: [],
3555
- message: "MCP tools are not implemented in this quickstart gateway"
3556
- });
3557
- });
3558
-
3559
- // Get available MCP tools
3560
- app.get("/api/mcp/tools", async (_req, res) => {
3561
- res.json([]);
3562
- });
3563
-
3564
- // News endpoint - placeholder implementation
3565
- app.get("/api/mcp/news", async (req, res) => {
3566
- const { topic = "general", count = 10, headlines = false } = req.query;
3567
-
3568
- res.status(501).json({
3569
- error: "News service not implemented",
3570
- message: "MCP news functionality is not available in this quickstart gateway",
3571
- suggestion: "Implement this endpoint to connect to a news API service",
3572
- requestedParams: { topic, count, headlines }
3573
- });
3574
- });
3575
-
3576
- // Weather endpoint - placeholder implementation
3577
- app.get("/api/mcp/weather", async (req, res) => {
3578
- const { zip, latitude, longitude } = req.query;
3579
-
3580
- res.status(501).json({
3581
- error: "Weather service not implemented",
3582
- message: "MCP weather functionality is not available in this quickstart gateway",
3583
- suggestion: "Implement this endpoint to connect to a weather API service",
3584
- requestedParams: { zip, latitude, longitude }
3585
- });
3586
- });
3587
-
3588
- // Documentation search endpoint - placeholder implementation
3589
- app.get("/api/mcp/docs", async (req, res) => {
3590
- const { query, framework, count = 10 } = req.query;
3591
-
3592
- if (!query) {
3593
- return res.status(400).json({
3594
- error: "Query parameter is required",
3595
- message: "Please provide a search query"
3596
- });
3597
- }
3598
-
3599
- res.status(501).json({
3600
- error: "Documentation search not implemented",
3601
- message: "MCP docs functionality is not available in this quickstart gateway",
3602
- suggestion: "Implement this endpoint to connect to a documentation search service",
3603
- requestedParams: { query, framework, count }
3604
- });
3605
- });
3606
-
3607
- // Get supported documentation frameworks
3608
- app.get("/api/mcp/docs/frameworks", async (_req, res) => {
3609
- res.json({
3610
- frameworks: [],
3611
- message: "Documentation frameworks not configured in this quickstart gateway"
3612
- });
3613
- });
3614
-
3615
- // Sports scores endpoint - placeholder implementation
3616
- app.get("/api/mcp/sports", async (req, res) => {
3617
- const { league, date } = req.query;
3618
-
3619
- res.status(501).json({
3620
- error: "Sports service not implemented",
3621
- message: "MCP sports functionality is not available in this quickstart gateway",
3622
- suggestion: "Implement this endpoint to connect to a sports API service",
3623
- requestedParams: { league, date }
3624
- });
3625
- });
3626
-
3627
- // Get supported sports leagues
3628
- app.get("/api/mcp/sports/leagues", async (_req, res) => {
3629
- res.json({
3630
- leagues: [],
3631
- message: "Sports leagues not configured in this quickstart gateway"
3632
- });
3633
- });
3634
-
3635
- // Image generation endpoint - placeholder implementation
3636
- app.post("/api/mcp/generate-image", async (req, res) => {
3637
- const { prompt, size, quality, style } = req.body;
3638
-
3639
- if (!prompt) {
3640
- return res.status(400).json({
3641
- error: "Prompt is required",
3642
- message: "Please provide a prompt for image generation"
3643
- });
3644
- }
3645
-
3646
- res.status(501).json({
3647
- success: false,
3648
- error: "Image generation not implemented",
3649
- message: "MCP image generation functionality is not available in this quickstart gateway",
3650
- suggestion: "Implement this endpoint to connect to an image generation API service",
3651
- requestedParams: { prompt, size, quality, style }
3652
- });
3653
- });
3654
-
3655
- // ============================================================================
3656
- // NOT IMPLEMENTED ROUTES (for graceful degradation)
3657
- // ============================================================================
3658
-
3659
- app.all("/api/anthropic/*", (_req, res) => {
3660
- res.status(501).json({
3661
- error: "Anthropic route not implemented",
3662
- message: "Extend the quickstart gateway if you need additional Anthropic endpoints beyond the defaults."
3663
- });
3664
- });
3665
-
3666
- const port = Number(process.env.PORT ?? ${ctx.gatewayPort});
3667
- app.listen(port, () => {
3668
- console.log("\u26A1 Bandit quickstart gateway ready on http://localhost:" + port);
3669
- console.log("\u{1F4E1} Supported providers: Bandit AI, OpenAI, Azure OpenAI, Anthropic, XAI, Ollama");
3670
- console.log("\u{1F517} Provider-specific routes:");
3671
- console.log(" \u2022 /api/bandit/* - Bandit AI endpoints");
3672
- console.log(" \u2022 /api/openai/* - OpenAI endpoints");
3673
- console.log(" \u2022 /api/azure/* - Azure OpenAI endpoints");
3674
- console.log(" \u2022 /api/anthropic/* - Anthropic endpoints");
3675
- console.log(" \u2022 /api/xai/* - XAI endpoints");
3676
- console.log(" \u2022 /api/ollama/* - Ollama endpoints");
3677
- console.log(" \u2022 /api/health - Overall health check");
3678
- });
3679
- `;
3680
- return ensureTrailingNewline(normalizeLineEndings(gatewaySource));
3681
- };
3682
- var buildGitignore = () => ensureTrailingNewline(
3683
- normalizeLineEndings(
3684
- `node_modules
3685
- .env
3686
- .vite
3687
- .idea
3688
- .DS_Store
3689
- coverage
3690
- dist
3691
- `
3692
- )
3693
- );
3694
- var buildReadme = (ctx) => ensureTrailingNewline(
3695
- normalizeLineEndings(
3696
- `# ${ctx.projectTitle} \u2014 Bandit Quickstart
3697
-
3698
- This project was generated by the Bandit Engine CLI. It ships with a React + Vite frontend that consumes \`@burtson-labs/bandit-engine\`, a lightweight Express gateway you can adapt for production, and a Next.js App Router API scaffold in \`server/next-app/\`.
3699
-
3700
- ## \u{1F680} Next steps
3701
- - \`npm install\`
3702
- - \`cp .env.example .env\`
3703
- - Fill in your Bandit AI, OpenAI, Azure OpenAI, Anthropic, or xAI credentials (or point \`OLLAMA_URL\` at your local server)
3704
- - \`npm run dev\`
3705
-
3706
- The command runs the gateway and the frontend together. Visit http://localhost:${ctx.frontendPort} to see the chat and modal in action.
3707
-
3708
- ## \u{1F527} Customizing your assistant
3709
- - **Branding & personas**: edit \`public/config.json\` to tweak logos, colors, and starter models.
3710
- - **Provider defaults**: update \`.env\` to switch providers or change the default upstream model IDs.
3711
- - **Gateway routes**: open \`server/gateway.js\` to add auth, logging, or connect additional providers.
3712
-
3713
- ## \u{1F4E6} What\u2019s inside
3714
- - React + Vite 5 with Material UI theming
3715
- - Bandit chat surface + modal wired via \`ChatProvider\`
3716
- - Express gateway proxying Bandit AI, OpenAI, Azure OpenAI, Anthropic, XAI, or Ollama to keep API keys server-side
3717
- - Next.js App Router gateway scaffold in 'server/next-app/' for projects that prefer Next
3718
- - Friendly defaults you can evolve into your production stack
3719
-
3720
- Need more? Run \`npx @burtson-labs/bandit-engine create --help\` to explore additional options.
3721
- `
3722
- )
3723
- );
3724
-
3725
- // src/cli/createQuickstart.ts
3726
- var createQuickstartProject = async (options) => {
3727
- const resolvedDir = import_node_path2.default.resolve(process.cwd(), options.targetDir);
3728
- const rawProjectName = options.projectName ?? import_node_path2.default.basename(resolvedDir);
3729
- const packageName = normalizePackageName(rawProjectName);
3730
- const projectTitle = toTitleCase(rawProjectName) || "Bandit Quickstart";
3731
- await ensureWritableDirectory(resolvedDir, Boolean(options.force));
3732
- const skipPrompts = Boolean(options.skipPrompts);
3733
- const provider = options.provider ? normalizeProvider(options.provider) : skipPrompts ? "ollama" : await promptForProvider();
3734
- const promptAnswers = skipPrompts ? {} : await promptForMissingData({
3735
- brandingText: options.brandingText,
3736
- provider
3737
- });
3738
- const brandingText = options.brandingText ?? (typeof promptAnswers.brandingText === "string" && promptAnswers.brandingText.trim().length > 0 ? promptAnswers.brandingText.trim() : `${projectTitle} Assistant`);
3739
- const logoResolution = {
3740
- dataUrl: "",
3741
- fileName: "",
3742
- fileContent: Buffer.alloc(0),
3743
- hasTransparentLogo: true,
3744
- isDefault: true
3745
- };
3746
- const gatewayPort = sanitizePort(options.gatewayPort ?? promptAnswers.gatewayPort ?? 5151);
3747
- const frontendPort = sanitizePort(options.frontendPort ?? promptAnswers.frontendPort ?? 5183);
3748
- const defaultModelId = sanitizeModelIdentifier(
3749
- options.defaultModelId ?? inferDefaultModelId(provider)
3750
- );
3751
- const fallbackModelRaw = options.fallbackModelId ? options.fallbackModelId : inferFallbackModelId(provider, defaultModelId);
3752
- const fallbackModelId = fallbackModelRaw ? sanitizeModelIdentifier(fallbackModelRaw) : void 0;
3753
- const inputs = {
3754
- targetDir: resolvedDir,
3755
- projectTitle,
3756
- packageName,
3757
- brandingText,
3758
- provider,
3759
- defaultModelId,
3760
- fallbackModelId,
3761
- gatewayPort,
3762
- frontendPort,
3763
- logo: logoResolution
3764
- };
3765
- const createdFiles = await writeProject(inputs);
3766
- return {
3767
- projectDir: resolvedDir,
3768
- packageName,
3769
- brandingText,
3770
- defaultModelId,
3771
- fallbackModelId,
3772
- gatewayPort,
3773
- frontendPort,
3774
- createdFiles
3775
- };
3776
- };
3777
- var normalizePackageName = (input) => {
3778
- const fallback = "bandit-quickstart";
3779
- const kebab = toKebabCase(input || fallback);
3780
- if (!kebab) {
3781
- return fallback;
3782
- }
3783
- return /^[a-z]/.test(kebab) ? kebab : `bandit-${kebab}`;
3784
- };
3785
- var ensureWritableDirectory = async (dir, force) => {
3786
- const exists = await import_fs_extra.default.pathExists(dir);
3787
- if (!exists) {
3788
- await import_fs_extra.default.ensureDir(dir);
3789
- return;
3790
- }
3791
- const entries = await import_fs_extra.default.readdir(dir);
3792
- if (entries.length > 0 && !force) {
3793
- throw new Error(
3794
- `Target directory "${dir}" is not empty. Re-run with --force to overwrite or choose a new folder.`
3795
- );
3796
- }
3797
- };
3798
- var normalizeProvider = (value) => {
3799
- const normalized = (value ?? "openai").toLowerCase();
3800
- if (normalized === "ollama") {
3801
- return "ollama";
3802
- }
3803
- if (normalized === "azure" || normalized === "azure-openai" || normalized === "azureopenai") {
3804
- return "azure";
3805
- }
3806
- if (normalized === "anthropic") {
3807
- return "anthropic";
3808
- }
3809
- if (normalized === "xai" || normalized === "grok") {
3810
- return "xai";
3811
- }
3812
- if (normalized === "bandit" || normalized === "banditai" || normalized === "bandit-ai") {
3813
- return "bandit";
3814
- }
3815
- return "openai";
3816
- };
3817
- var inferDefaultModelId = (provider) => {
3818
- if (provider === "ollama") {
3819
- return "llama3.1";
3820
- }
3821
- if (provider === "azure") {
3822
- return "azure:gpt-4o";
3823
- }
3824
- if (provider === "anthropic") {
3825
- return "anthropic:claude-3-5-haiku-latest";
3826
- }
3827
- if (provider === "xai") {
3828
- return "xai:grok-2-latest";
3829
- }
3830
- if (provider === "bandit") {
3831
- return "bandit-core-1";
3832
- }
3833
- return "openai:gpt-4o-mini";
3834
- };
3835
- var inferFallbackModelId = (provider, defaultId) => {
3836
- if (provider === "ollama") {
3837
- const normalized = defaultId.toLowerCase();
3838
- if (normalized.startsWith("llama3")) {
3839
- return "llama2";
3840
- }
3841
- return "llama3";
3842
- }
3843
- if (provider === "azure") {
3844
- return defaultId === "azure:gpt-4o-mini" ? "azure:gpt-4o" : "azure:gpt-4o-mini";
3845
- }
3846
- if (provider === "anthropic") {
3847
- return defaultId === "anthropic:claude-3-5-haiku-latest" ? "anthropic:claude-3-haiku-20240307" : "anthropic:claude-3-5-haiku-latest";
3848
- }
3849
- if (provider === "xai") {
3850
- return defaultId === "xai:grok-2-mini" ? "xai:grok-2-latest" : "xai:grok-2-mini";
3851
- }
3852
- if (provider === "bandit") {
3853
- return void 0;
3854
- }
3855
- return defaultId === "openai:gpt-4.1-mini" ? "openai:gpt-4o-mini" : "openai:gpt-4.1-mini";
3856
- };
3857
- var promptForProvider = async () => {
3858
- const providerOptions = [
3859
- { label: "Ollama (self-hosted) \u2014 default", value: "ollama" },
3860
- { label: "OpenAI", value: "openai" },
3861
- { label: "Bandit AI (Bandit Core)", value: "bandit" },
3862
- { label: "Azure OpenAI", value: "azure" },
3863
- { label: "Anthropic", value: "anthropic" },
3864
- { label: "xAI (Grok)", value: "xai" }
3865
- ];
3866
- const messageLines = [
3867
- "Which provider should we configure for the gateway?",
3868
- ...providerOptions.map((option, index) => ` ${index + 1}. ${option.label}`),
3869
- "Enter a number:"
3870
- ];
3871
- const onCancel = () => {
3872
- throw new Error("Command cancelled.");
3873
- };
3874
- const answers = await (0, import_prompts.default)(
3875
- {
3876
- type: "number",
3877
- name: "providerIndex",
3878
- message: messageLines.join("\n"),
3879
- initial: 1,
3880
- validate: (input) => {
3881
- if (!Number.isInteger(input)) {
3882
- return "Enter a whole number.";
3883
- }
3884
- return input >= 1 && input <= providerOptions.length ? true : `Enter a number between 1 and ${providerOptions.length}.`;
3885
- }
3886
- },
3887
- { onCancel }
3888
- );
3889
- const selectedIndex = typeof answers.providerIndex === "number" && answers.providerIndex >= 1 ? answers.providerIndex - 1 : 0;
3890
- return providerOptions[selectedIndex]?.value ?? "ollama";
3891
- };
3892
- var sanitizePort = (value) => {
3893
- const port = Number(value);
3894
- if (Number.isNaN(port) || port <= 0 || port >= 65535) {
3895
- return 8080;
3896
- }
3897
- return port;
3898
- };
3899
- var promptForMissingData = async (options) => {
3900
- const questions = [];
3901
- if (!options.brandingText) {
3902
- questions.push({
3903
- type: "text",
3904
- name: "brandingText",
3905
- message: "What should we display for the app name? (Press Enter to accept)",
3906
- initial: "Bandit Quickstart"
3907
- });
3908
- }
3909
- const defaultGatewayPort = options.provider === "ollama" ? 11435 : 8080;
3910
- const defaultFrontendPort = 5183;
3911
- questions.push({
3912
- type: "text",
3913
- name: "frontendPort",
3914
- message: "Frontend port (Press Enter to accept)",
3915
- initial: String(defaultFrontendPort),
3916
- validate: (value) => {
3917
- if (typeof value !== "string" || value.trim().length === 0) {
3918
- return true;
3919
- }
3920
- const numericValue = Number(value);
3921
- return Number.isFinite(numericValue) && numericValue > 0 && numericValue < 65535 ? true : "Enter a port between 1 and 65535";
3922
- }
3923
- });
3924
- questions.push({
3925
- type: "text",
3926
- name: "gatewayPort",
3927
- message: "Gateway port (Press Enter to accept)",
3928
- initial: String(defaultGatewayPort),
3929
- validate: (value) => {
3930
- if (typeof value !== "string" || value.trim().length === 0) {
3931
- return true;
3932
- }
3933
- const numericValue = Number(value);
3934
- return Number.isFinite(numericValue) && numericValue > 0 && numericValue < 65535 ? true : "Enter a port between 1 and 65535";
3935
- }
3936
- });
3937
- const onCancel = () => {
3938
- throw new Error("Command cancelled.");
3939
- };
3940
- const answers = await (0, import_prompts.default)(questions, { onCancel });
3941
- const parsedGatewayPort = typeof answers.gatewayPort === "number" ? answers.gatewayPort : typeof answers.gatewayPort === "string" && answers.gatewayPort.trim().length > 0 ? Number(answers.gatewayPort) : defaultGatewayPort;
3942
- const parsedFrontendPort = typeof answers.frontendPort === "number" ? answers.frontendPort : typeof answers.frontendPort === "string" && answers.frontendPort.trim().length > 0 ? Number(answers.frontendPort) : defaultFrontendPort;
3943
- return {
3944
- brandingText: typeof answers.brandingText === "string" ? answers.brandingText : void 0,
3945
- gatewayPort: Number.isFinite(parsedGatewayPort) ? parsedGatewayPort : defaultGatewayPort,
3946
- frontendPort: Number.isFinite(parsedFrontendPort) ? parsedFrontendPort : defaultFrontendPort
3947
- };
3948
- };
3949
- var writeProject = async (inputs) => {
3950
- const { targetDir } = inputs;
3951
- const createdFiles = [];
3952
- const context = {
3953
- packageName: inputs.packageName,
3954
- projectTitle: inputs.projectTitle,
3955
- engineVersion: package_default.version,
3956
- brandingText: inputs.brandingText,
3957
- logoBase64: inputs.logo.dataUrl,
3958
- hasTransparentLogo: inputs.logo.hasTransparentLogo,
3959
- isDefaultLogo: inputs.logo.isDefault,
3960
- gatewayPort: inputs.gatewayPort,
3961
- frontendPort: inputs.frontendPort,
3962
- defaultProvider: inputs.provider,
3963
- defaultGatewayUrl: `http://localhost:${inputs.gatewayPort}`,
3964
- defaultModelId: inputs.defaultModelId,
3965
- fallbackModelId: inputs.fallbackModelId,
3966
- gatewayModels: buildGatewayModels(inputs)
3967
- };
3968
- const files = {
3969
- "package.json": buildPackageJson(context),
3970
- "tsconfig.json": buildTsConfig(),
3971
- "src/env.d.ts": buildEnvDts(),
3972
- "vite.config.ts": buildViteConfig(context),
3973
- "src/main.tsx": buildMainTsx(),
3974
- "index.html": buildIndexHtml(),
3975
- "src/App.tsx": buildAppTsx(context),
3976
- "src/index.css": buildIndexCss(),
3977
- "src/theme.ts": buildThemeTs(),
3978
- "public/config.json": buildBrandingConfig(context),
3979
- "server/gateway.js": buildGatewayServer(context),
3980
- "server/next-app/app/api/chat/completions/route.ts": buildNextChatRoute(context),
3981
- "server/next-app/app/api/health/route.ts": buildNextHealthRoute(),
3982
- "server/next-app/app/api/models/route.ts": buildNextModelsRoute(context),
3983
- "server/next-app/README.md": buildNextGatewayReadme(),
3984
- ".env.example": buildEnvExample(context),
3985
- ".gitignore": buildGitignore(),
3986
- "README.md": buildReadme(context)
3987
- };
3988
- if (!inputs.logo.isDefault && inputs.logo.fileName) {
3989
- files[import_node_path2.default.posix.join("public", inputs.logo.fileName)] = inputs.logo.fileContent;
3990
- }
3991
- for (const [relativePath, content] of Object.entries(files)) {
3992
- const destination = import_node_path2.default.join(targetDir, relativePath);
3993
- await import_fs_extra.default.ensureDir(import_node_path2.default.dirname(destination));
3994
- if (Buffer.isBuffer(content)) {
3995
- await import_fs_extra.default.writeFile(destination, content);
3996
- } else {
3997
- await import_fs_extra.default.writeFile(destination, ensureTrailingNewline(content), "utf8");
3998
- }
3999
- createdFiles.push(relativePath);
4000
- }
4001
- return createdFiles;
4002
- };
4003
- var buildGatewayModels = (inputs) => {
4004
- const seen = /* @__PURE__ */ new Set();
4005
- const models = [];
4006
- const pushModel = (modelId) => {
4007
- if (!modelId) return;
4008
- if (seen.has(modelId)) return;
4009
- seen.add(modelId);
4010
- const provider = modelId.includes(":") ? modelId.split(":")[0] : inputs.provider;
4011
- const nameSegment = modelId.includes(":") ? modelId.split(":")[1] : modelId;
4012
- models.push({
4013
- id: modelId,
4014
- name: toTitleCase(nameSegment.replace(/[-_.]/g, " ")),
4015
- provider
4016
- });
4017
- };
4018
- pushModel(inputs.defaultModelId);
4019
- pushModel(inputs.fallbackModelId);
4020
- return models;
4021
- };
4022
-
4023
- // src/cli/index.ts
4024
- var logIntro = () => {
4025
- console.log("\u{1F977} Bandit CLI \u2014 Burtson Labs \u{1F9EA}");
4026
- };
4027
- var program = new import_commander.Command();
4028
- program.name("bandit").description("Bandit Engine developer utilities").version(package_default.version).showHelpAfterError();
4029
- program.command("create").description("Scaffold a Bandit quickstart project with a frontend and gateway").argument("[directory]", "Relative path for your new project", "bandit-quickstart").option("-f, --force", "Overwrite the target directory if it already contains files", false).option("--branding-text <text>", "Assistant display name shown in the UI").option(
4030
- "--provider <provider>",
4031
- "Default gateway provider (openai, azure, anthropic, ollama)"
4032
- ).option(
4033
- "--frontend-port <port>",
4034
- "Frontend dev server port (default: 5183)",
4035
- (value) => parseInt(value, 10)
4036
- ).option("--gateway-port <port>", "Gateway port (default: 8080)", (value) => parseInt(value, 10)).option("-y, --yes", "Skip interactive prompts and accept defaults", false).option("--skip-prompts", "Alias for --yes", false).action(async (directory, cmdOptions) => {
4037
- try {
4038
- const targetDir = directory ?? "bandit-quickstart";
4039
- const projectName = import_node_path3.default.basename(import_node_path3.default.resolve(process.cwd(), targetDir));
4040
- logIntro();
4041
- const skipPrompts = Boolean(cmdOptions.skipPrompts ?? cmdOptions.yes);
4042
- const result = await createQuickstartProject({
4043
- targetDir,
4044
- projectName,
4045
- force: Boolean(cmdOptions.force),
4046
- brandingText: cmdOptions.brandingText,
4047
- provider: typeof cmdOptions.provider === "string" ? cmdOptions.provider : void 0,
4048
- frontendPort: Number.isFinite(cmdOptions.frontendPort) ? cmdOptions.frontendPort : void 0,
4049
- gatewayPort: Number.isFinite(cmdOptions.gatewayPort) ? cmdOptions.gatewayPort : void 0,
4050
- skipPrompts
4051
- });
4052
- const relativeDir = import_node_path3.default.relative(process.cwd(), result.projectDir) || ".";
4053
- console.log("\n\u2705 Quickstart ready!");
4054
- console.log(` Location: ${result.projectDir}`);
4055
- console.log(` Package: ${result.packageName}`);
4056
- console.log(` App name: ${result.brandingText}`);
4057
- console.log(` Frontend: http://localhost:${result.frontendPort}`);
4058
- console.log(` Gateway: http://localhost:${result.gatewayPort}`);
4059
- console.log("\nNext steps:");
4060
- console.log(` cd ${relativeDir}`);
4061
- console.log(" npm install");
4062
- console.log(" cp .env.example .env");
4063
- console.log(" npm run dev");
4064
- console.log("");
4065
- console.log("\u{1F50D} Before you dive in:");
4066
- console.log(" \u2022 Open .env to confirm the provider credentials and URLs match your setup.");
4067
- console.log(" \u2022 server/gateway.js is a scaffold Express proxy that keeps API keys server-side\u2014extend it with auth, logging, and your production logic.");
4068
- console.log(" \u2022 If you prefer Next.js App Router, check server/next-app/ for a starter route handler.");
4069
- console.log("");
4070
- } catch (error) {
4071
- const message = error instanceof Error ? error.message : "Failed to create Bandit quickstart project.";
4072
- console.error(`
4073
- \u274C ${message}`);
4074
- process.exitCode = 1;
4075
- }
4076
- });
4077
- async function main() {
4078
- await program.parseAsync(process.argv);
4079
- }
4080
- main().catch((error) => {
4081
- console.error(error instanceof Error ? error.message : error);
4082
- process.exit(1);
4083
- });
4084
- //# sourceMappingURL=cli.js.map