@burtson-labs/bandit-engine 2.0.35 → 2.0.37

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 (43) hide show
  1. package/README.md +6 -5
  2. package/dist/{aiProviderStore-3YS2BZU3.mjs → aiProviderStore-UJRDUYOF.mjs} +2 -2
  3. package/dist/{chat-2LYIZNWZ.mjs → chat-SZK3EBDO.mjs} +5 -5
  4. package/dist/chat-provider.js +227 -11
  5. package/dist/chat-provider.js.map +1 -1
  6. package/dist/chat-provider.mjs +4 -4
  7. package/dist/{chunk-6PQRG6W4.mjs → chunk-2ZZA2IFL.mjs} +3 -3
  8. package/dist/{chunk-GBANNFRD.mjs → chunk-ED5NNDKO.mjs} +3 -3
  9. package/dist/{chunk-XD5VJCFN.mjs → chunk-FJO5ZWYU.mjs} +3 -3
  10. package/dist/{chunk-XXMCI2WK.mjs → chunk-G4OXOTNJ.mjs} +41 -8
  11. package/dist/{chunk-XXMCI2WK.mjs.map → chunk-G4OXOTNJ.mjs.map} +1 -1
  12. package/dist/{chunk-LG2JCTOE.mjs → chunk-PLNFTIGX.mjs} +4 -4
  13. package/dist/{chunk-7RLN6ZGT.mjs → chunk-S635Q6OQ.mjs} +3 -3
  14. package/dist/{chunk-IGD4KGB5.mjs → chunk-ZAVV2AT5.mjs} +4 -4
  15. package/dist/{chunk-IHJPVIGB.mjs → chunk-ZNNOTDRD.mjs} +208 -1
  16. package/dist/chunk-ZNNOTDRD.mjs.map +1 -0
  17. package/dist/cli/cli.js +1104 -68
  18. package/dist/cli/cli.js.map +1 -1
  19. package/dist/{gateway-BiHRHJMM.d.ts → gateway-Ckf_KusF.d.mts} +4 -4
  20. package/dist/{gateway-BiHRHJMM.d.mts → gateway-Ckf_KusF.d.ts} +4 -4
  21. package/dist/index.d.mts +2 -2
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.js +318 -69
  24. package/dist/index.js.map +1 -1
  25. package/dist/index.mjs +8 -8
  26. package/dist/management/management.js +316 -67
  27. package/dist/management/management.js.map +1 -1
  28. package/dist/management/management.mjs +6 -6
  29. package/dist/modals/chat-modal/chat-modal.js +236 -20
  30. package/dist/modals/chat-modal/chat-modal.js.map +1 -1
  31. package/dist/modals/chat-modal/chat-modal.mjs +4 -4
  32. package/dist/public-types.d.mts +1 -1
  33. package/dist/public-types.d.ts +1 -1
  34. package/package.json +1 -1
  35. package/dist/chunk-IHJPVIGB.mjs.map +0 -1
  36. /package/dist/{aiProviderStore-3YS2BZU3.mjs.map → aiProviderStore-UJRDUYOF.mjs.map} +0 -0
  37. /package/dist/{chat-2LYIZNWZ.mjs.map → chat-SZK3EBDO.mjs.map} +0 -0
  38. /package/dist/{chunk-6PQRG6W4.mjs.map → chunk-2ZZA2IFL.mjs.map} +0 -0
  39. /package/dist/{chunk-GBANNFRD.mjs.map → chunk-ED5NNDKO.mjs.map} +0 -0
  40. /package/dist/{chunk-XD5VJCFN.mjs.map → chunk-FJO5ZWYU.mjs.map} +0 -0
  41. /package/dist/{chunk-LG2JCTOE.mjs.map → chunk-PLNFTIGX.mjs.map} +0 -0
  42. /package/dist/{chunk-7RLN6ZGT.mjs.map → chunk-S635Q6OQ.mjs.map} +0 -0
  43. /package/dist/{chunk-IGD4KGB5.mjs.map → chunk-ZAVV2AT5.mjs.map} +0 -0
package/dist/cli/cli.js CHANGED
@@ -30,7 +30,7 @@ var import_commander = require("commander");
30
30
  // package.json
31
31
  var package_default = {
32
32
  name: "@burtson-labs/bandit-engine",
33
- version: "2.0.35",
33
+ version: "2.0.37",
34
34
  license: "BUSL-1.1",
35
35
  main: "dist/index.js",
36
36
  module: "dist/index.mjs",
@@ -162,14 +162,29 @@ var toTitleCase = (value) => {
162
162
  };
163
163
  var formatJson = (value) => `${JSON.stringify(value, null, 2)}
164
164
  `;
165
+ var KNOWN_PROVIDERS = /* @__PURE__ */ new Set(["openai", "azure", "azure-openai", "azureopenai", "anthropic", "xai", "ollama"]);
165
166
  var sanitizeModelIdentifier = (value) => {
166
167
  const trimmed = value.trim();
167
168
  if (!trimmed.includes(":")) {
168
169
  return trimmed.toLowerCase();
169
170
  }
170
- const [provider, model] = trimmed.split(/:(.+)/).filter(Boolean);
171
- const cleanModel = model.replace(/[^a-zA-Z0-9_.-]/g, "-").replace(/-+/g, "-").toLowerCase();
172
- return `${provider.toLowerCase()}:${cleanModel}`;
171
+ const segments = trimmed.split(/:(.+)/).filter(Boolean);
172
+ if (segments.length < 2) {
173
+ return trimmed.toLowerCase();
174
+ }
175
+ const [candidateProvider, rest] = segments;
176
+ const provider = candidateProvider.toLowerCase();
177
+ const cleanRest = rest.trim().replace(/[^a-zA-Z0-9_.:-]/g, "-").replace(/-+/g, "-").toLowerCase();
178
+ if (KNOWN_PROVIDERS.has(provider)) {
179
+ if (provider === "azure-openai" || provider === "azureopenai") {
180
+ return `azure:${cleanRest}`;
181
+ }
182
+ if (provider === "ollama") {
183
+ return cleanRest;
184
+ }
185
+ return `${provider}:${cleanRest}`;
186
+ }
187
+ return [candidateProvider, rest].filter(Boolean).join(":").replace(/[^a-zA-Z0-9_.:-]/g, "-").replace(/-+/g, "-").toLowerCase();
173
188
  };
174
189
  var normalizeLineEndings = (content) => content.replace(/\r\n/g, "\n");
175
190
  var ensureTrailingNewline = (content) => content.endsWith("\n") ? content : `${content}
@@ -214,33 +229,52 @@ var buildPackageJson = (ctx) => formatJson({
214
229
  "vite": "^7.1.9"
215
230
  }
216
231
  });
217
- var buildEnvExample = (ctx) => ensureTrailingNewline(
218
- normalizeLineEndings(
219
- `# Frontend configuration
220
- VITE_DEV_PORT=${ctx.frontendPort}
221
- VITE_GATEWAY_URL=${ctx.defaultGatewayUrl}
222
- VITE_DEFAULT_MODEL=${ctx.defaultModelId}
223
- VITE_FALLBACK_MODEL=${ctx.fallbackModelId ?? ""}
224
- VITE_GATEWAY_PROVIDER=${ctx.defaultProvider}
225
- VITE_BRANDING_TEXT=${ctx.brandingText}
226
-
227
- # Gateway configuration
228
- # OPENAI_API_KEY=sk-................................
229
- # AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
230
- # AZURE_OPENAI_API_KEY=................................................................
231
- # AZURE_OPENAI_API_VERSION=2024-08-01-preview
232
- # AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-4o
233
- # AZURE_OPENAI_COMPLETIONS_DEPLOYMENT=gpt-35-turbo-instruct
234
- # AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT=text-embedding-3-large
235
- # ANTHROPIC_API_KEY=sk-ant-................................
236
- # ANTHROPIC_BASE_URL=https://api.anthropic.com
237
- # ANTHROPIC_API_VERSION=2023-06-01
238
- # ANTHROPIC_MAX_TOKENS=1024
239
- # OLLAMA_URL=http://localhost:11434
240
- # PORT=${ctx.gatewayPort}
241
- `
242
- )
243
- );
232
+ var buildEnvExample = (ctx) => {
233
+ const lines = [
234
+ "# Frontend configuration",
235
+ `VITE_DEV_PORT=${ctx.frontendPort}`,
236
+ `VITE_GATEWAY_URL=${ctx.defaultGatewayUrl}`,
237
+ `VITE_DEFAULT_MODEL=${ctx.defaultModelId}`,
238
+ `VITE_FALLBACK_MODEL=${ctx.fallbackModelId ?? ""}`,
239
+ `VITE_GATEWAY_PROVIDER=${ctx.defaultProvider}`,
240
+ `VITE_BRANDING_TEXT=${ctx.brandingText}`,
241
+ "",
242
+ "# Gateway configuration",
243
+ "# These values power server/gateway.js \u2014 update them before running in production."
244
+ ];
245
+ switch (ctx.defaultProvider) {
246
+ case "openai":
247
+ lines.push("OPENAI_API_KEY=");
248
+ break;
249
+ case "azure":
250
+ lines.push("AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com");
251
+ lines.push("AZURE_OPENAI_API_KEY=");
252
+ lines.push("AZURE_OPENAI_API_VERSION=2024-08-01-preview");
253
+ lines.push("AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-4o");
254
+ lines.push("AZURE_OPENAI_COMPLETIONS_DEPLOYMENT=gpt-35-turbo-instruct");
255
+ lines.push("AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT=text-embedding-3-large");
256
+ break;
257
+ case "anthropic":
258
+ lines.push("ANTHROPIC_API_KEY=");
259
+ lines.push("ANTHROPIC_BASE_URL=https://api.anthropic.com");
260
+ lines.push("ANTHROPIC_API_VERSION=2023-06-01");
261
+ lines.push("ANTHROPIC_MAX_TOKENS=1024");
262
+ break;
263
+ case "xai":
264
+ lines.push("XAI_API_KEY=");
265
+ lines.push("XAI_BASE_URL=https://api.x.ai/v1");
266
+ break;
267
+ case "ollama":
268
+ default:
269
+ lines.push("OLLAMA_URL=http://localhost:11434");
270
+ break;
271
+ }
272
+ lines.push(`PORT=${ctx.gatewayPort}`);
273
+ lines.push(
274
+ "# If you switch providers later, copy the relevant block above and update the credentials."
275
+ );
276
+ return ensureTrailingNewline(normalizeLineEndings(lines.join("\n")));
277
+ };
244
278
  var buildTsConfig = () => formatJson({
245
279
  compilerOptions: {
246
280
  target: "ESNext",
@@ -439,7 +473,7 @@ const gatewayBaseUrl = (import.meta.env.VITE_GATEWAY_URL ?? "${ctx.defaultGatewa
439
473
  const defaultModelId = import.meta.env.VITE_DEFAULT_MODEL ?? "${ctx.defaultModelId}";
440
474
  const fallbackModelId = import.meta.env.VITE_FALLBACK_MODEL ?? ${ctx.fallbackModelId ? `${QUOTE}${ctx.fallbackModelId}${QUOTE}` : "undefined"};
441
475
  const brandingText = import.meta.env.VITE_BRANDING_TEXT ?? "${ctx.brandingText}";
442
- const provider = (import.meta.env.VITE_GATEWAY_PROVIDER ?? "${ctx.defaultProvider}") as "openai" | "ollama" | "azure" | "anthropic";
476
+ const provider = (import.meta.env.VITE_GATEWAY_PROVIDER ?? "${ctx.defaultProvider}") as "openai" | "ollama" | "azure" | "anthropic" | "xai";
443
477
 
444
478
  const gatewayApiUrl = gatewayBaseUrl.endsWith("/api") ? gatewayBaseUrl : gatewayBaseUrl + "/api";
445
479
  const banditHeadLogoUrl = "https://cdn.burtson.ai/images/bandit-head.png";
@@ -621,7 +655,7 @@ function App() {
621
655
  {brandingText}
622
656
  </Typography>
623
657
  <Typography variant="body1" color="text.secondary">
624
- Build, brand, and launch your assistant with a drop-in chat surface plus a secure gateway for OpenAI, Azure OpenAI, Anthropic, or Ollama.
658
+ Build, brand, and launch your assistant with a drop-in chat surface plus a secure gateway for OpenAI, Azure OpenAI, Anthropic, XAI, or Ollama.
625
659
  </Typography>
626
660
  <Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
627
661
  <Button component={RouterLink} to="/chat" variant="contained" color="primary">
@@ -668,7 +702,7 @@ function App() {
668
702
  Ship secure gateways
669
703
  </Typography>
670
704
  <Typography variant="body2" color="text.secondary">
671
- Keep API keys server-side while proxying requests to OpenAI, Azure OpenAI, Anthropic, or Ollama through the included Express gateway.
705
+ Keep API keys server-side while proxying requests to OpenAI, Azure OpenAI, Anthropic, XAI, or Ollama through the included Express gateway.
672
706
  </Typography>
673
707
  </CardContent>
674
708
  </Card>
@@ -815,21 +849,679 @@ function App() {
815
849
  );
816
850
  }
817
851
 
818
- export default App;
852
+ export default App;
853
+ `;
854
+ const withResponse = template.replace(/__RESPONSE_STATUS__/g, responseStatusExpr);
855
+ const withGatewayError = withResponse.replace(/__GATEWAY_ERROR__/g, gatewayErrorExpr);
856
+ return ensureTrailingNewline(normalizeLineEndings(withGatewayError));
857
+ };
858
+ var buildBrandingConfig = (ctx) => formatJson({
859
+ branding: {
860
+ logoBase64: ctx.isDefaultLogo ? null : ctx.logoBase64,
861
+ brandingText: ctx.brandingText,
862
+ theme: "bandit-dark",
863
+ hasTransparentLogo: ctx.isDefaultLogo ? true : ctx.hasTransparentLogo
864
+ },
865
+ knowledgeDocs: []
866
+ });
867
+ var NEXT_CHAT_ROUTE_TEMPLATE = `import { NextRequest, NextResponse } from "next/server";
868
+
869
+ export const dynamic = "force-dynamic";
870
+
871
+ const DEFAULT_PROVIDER = "__DEFAULT_PROVIDER__";
872
+ const DEFAULT_MODEL = "__DEFAULT_MODEL__";
873
+ const FALLBACK_MODEL = __FALLBACK_MODEL__;
874
+
875
+ const OLLAMA_URL = (process.env.OLLAMA_URL ?? "http://localhost:11434").replace(/\\/$/, "");
876
+ const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
877
+ const AZURE_OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT ? process.env.AZURE_OPENAI_ENDPOINT.replace(/\\/$/, "") : undefined;
878
+ const AZURE_OPENAI_API_KEY = process.env.AZURE_OPENAI_API_KEY;
879
+ const AZURE_OPENAI_API_VERSION = process.env.AZURE_OPENAI_API_VERSION ?? "2024-08-01-preview";
880
+ const AZURE_OPENAI_CHAT_DEPLOYMENT = process.env.AZURE_OPENAI_CHAT_DEPLOYMENT;
881
+ const AZURE_OPENAI_COMPLETIONS_DEPLOYMENT = process.env.AZURE_OPENAI_COMPLETIONS_DEPLOYMENT ?? AZURE_OPENAI_CHAT_DEPLOYMENT;
882
+ const AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT = process.env.AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT;
883
+ const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
884
+ const ANTHROPIC_BASE_URL = (process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com").replace(/\\/$/, "");
885
+ const ANTHROPIC_API_VERSION = process.env.ANTHROPIC_API_VERSION ?? "2023-06-01";
886
+ const ANTHROPIC_MAX_TOKENS = Number.isFinite(Number(process.env.ANTHROPIC_MAX_TOKENS))
887
+ ? Number(process.env.ANTHROPIC_MAX_TOKENS)
888
+ : 1024;
889
+ const XAI_API_KEY = process.env.XAI_API_KEY;
890
+ const XAI_BASE_URL = (process.env.XAI_BASE_URL ?? "https://api.x.ai/v1").replace(/\\/$/, "");
891
+
892
+ interface GatewayChatBody {
893
+ provider?: string;
894
+ model?: string;
895
+ messages?: Array<{ role: string; content: unknown }>;
896
+ prompt?: string;
897
+ stream?: boolean;
898
+ temperature?: number;
899
+ max_tokens?: number;
900
+ top_p?: number;
901
+ stop?: string | string[];
902
+ stop_sequences?: string | string[];
903
+ tools?: unknown;
904
+ tool_choice?: unknown;
905
+ metadata?: unknown;
906
+ thinking?: unknown;
907
+ images?: string[];
908
+ [key: string]: unknown;
909
+ }
910
+
911
+ const normalizeProvider = (input: string): "openai" | "azure" | "anthropic" | "ollama" | "xai" => {
912
+ const value = input.toLowerCase();
913
+ if (value === "azure-openai" || value === "azureopenai" || value === "azure") return "azure";
914
+ if (value === "anthropic" || value === "claude") return "anthropic";
915
+ if (value === "ollama") return "ollama";
916
+ if (value === "xai" || value === "grok") return "xai";
917
+ return "openai";
918
+ };
919
+
920
+ const stripPrefix = (model: unknown, prefix: string, fallback: string): string => {
921
+ if (typeof model === "string") {
922
+ return model.replace(new RegExp(\`^\${prefix}:\`), "");
923
+ }
924
+ return fallback;
925
+ };
926
+
927
+ const requireOpenAIKey = () => {
928
+ if (!OPENAI_API_KEY) {
929
+ throw new Error("Missing OPENAI_API_KEY. Add it to your .env file to route requests to OpenAI.");
930
+ }
931
+ return OPENAI_API_KEY;
932
+ };
933
+
934
+ const requireXAIKey = () => {
935
+ if (!XAI_API_KEY) {
936
+ throw new Error("Missing XAI_API_KEY. Add it to your .env file to route requests to xAI.");
937
+ }
938
+ return XAI_API_KEY;
939
+ };
940
+
941
+ const requireAnthropicKey = () => {
942
+ if (!ANTHROPIC_API_KEY) {
943
+ throw new Error("Missing ANTHROPIC_API_KEY. Add it to your .env file to route requests to Anthropic.");
944
+ }
945
+ return ANTHROPIC_API_KEY;
946
+ };
947
+
948
+ const isAzureConfigured = () => Boolean(AZURE_OPENAI_ENDPOINT && AZURE_OPENAI_API_KEY);
949
+
950
+ const requireAzureBaseConfig = () => {
951
+ if (!AZURE_OPENAI_ENDPOINT) {
952
+ throw new Error("Missing AZURE_OPENAI_ENDPOINT. Add it to your .env file to route requests to Azure OpenAI.");
953
+ }
954
+ if (!AZURE_OPENAI_API_KEY) {
955
+ throw new Error("Missing AZURE_OPENAI_API_KEY. Add it to your .env file to route requests to Azure OpenAI.");
956
+ }
957
+ return {
958
+ endpoint: AZURE_OPENAI_ENDPOINT,
959
+ apiKey: AZURE_OPENAI_API_KEY,
960
+ };
961
+ };
962
+
963
+ const buildAzureDeploymentUrl = (deployment: string | undefined, suffix: string) => {
964
+ if (!deployment) {
965
+ throw new Error(\`Missing Azure OpenAI \${suffix.split("/")[0]} deployment name.\`);
966
+ }
967
+ const { endpoint } = requireAzureBaseConfig();
968
+ const normalized = suffix.replace(/^\\/+/, "");
969
+ return \`\${endpoint}/openai/deployments/\${deployment}/\${normalized}?api-version=\${AZURE_OPENAI_API_VERSION}\`;
970
+ };
971
+
972
+ const resolveAzureDeployment = (model: unknown, fallback: string | undefined, kind: "chat" | "completions" | "embeddings") => {
973
+ const explicit = typeof model === "string" ? model.replace(/^azure:/, "") : undefined;
974
+ if (explicit) return explicit;
975
+ if (kind === "embeddings") return AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT ?? fallback;
976
+ if (kind === "completions") return AZURE_OPENAI_COMPLETIONS_DEPLOYMENT ?? fallback;
977
+ return AZURE_OPENAI_CHAT_DEPLOYMENT ?? fallback;
978
+ };
979
+
980
+ const flattenGatewayContent = (content: unknown): string => {
981
+ if (typeof content === "string") return content;
982
+ if (Array.isArray(content)) {
983
+ return content
984
+ .map((part) => {
985
+ if (typeof part === "string") return part;
986
+ if (part && typeof part === "object" && "type" in part) {
987
+ const typed = part as { type?: string; text?: string; image_url?: { url?: string } };
988
+ if (typed.type === "text" && typeof typed.text === "string") return typed.text;
989
+ if (typed.type === "image_url" && typed.image_url?.url) return \`[Image: \${typed.image_url.url}]\`;
990
+ }
991
+ return JSON.stringify(part ?? {});
992
+ })
993
+ .join("\\n");
994
+ }
995
+ if (content && typeof content === "object") return JSON.stringify(content);
996
+ return "";
997
+ };
998
+
999
+ const toAnthropicMessages = (messages: Array<{ role: string; content: unknown }> = []) => {
1000
+ const anthropicMessages: Array<{ role: "user" | "assistant"; content: Array<{ type: "text"; text: string }> }> = [];
1001
+ let systemPrompt = "";
1002
+
1003
+ for (const message of messages) {
1004
+ if (!message) continue;
1005
+ const text = flattenGatewayContent(message.content);
1006
+ if (message.role === "system") {
1007
+ systemPrompt = systemPrompt ? \`\${systemPrompt}\\n\\n\${text}\` : text;
1008
+ continue;
1009
+ }
1010
+ const role = message.role === "assistant" ? "assistant" : "user";
1011
+ anthropicMessages.push({
1012
+ role,
1013
+ content: [{ type: "text", text }],
1014
+ });
1015
+ }
1016
+
1017
+ return { messages: anthropicMessages, system: systemPrompt || undefined };
1018
+ };
1019
+
1020
+ const convertAnthropicResponseToGateway = (responseBody: any, modelName: string) => {
1021
+ if (!responseBody) {
1022
+ return {
1023
+ id: \`anthropic-\${Date.now()}\`,
1024
+ object: "chat.completion",
1025
+ created: Math.floor(Date.now() / 1000),
1026
+ model: modelName.startsWith("anthropic:") ? modelName : \`anthropic:\${modelName}\`,
1027
+ choices: [],
1028
+ };
1029
+ }
1030
+
1031
+ const textContent = Array.isArray(responseBody.content)
1032
+ ? responseBody.content
1033
+ .filter((item: any) => item && item.type === "text" && typeof item.text === "string")
1034
+ .map((item: any) => item.text)
1035
+ .join("\\n")
1036
+ : typeof responseBody.content === "string"
1037
+ ? responseBody.content
1038
+ : "";
1039
+
1040
+ const promptTokens = responseBody.usage?.input_tokens ?? 0;
1041
+ const completionTokens = responseBody.usage?.output_tokens ?? 0;
1042
+
1043
+ return {
1044
+ id: responseBody.id ?? \`anthropic-\${Date.now()}\`,
1045
+ object: "chat.completion",
1046
+ created: Math.floor(Date.now() / 1000),
1047
+ model: modelName.startsWith("anthropic:") ? modelName : \`anthropic:\${modelName}\`,
1048
+ choices: [
1049
+ {
1050
+ index: 0,
1051
+ message: {
1052
+ role: responseBody.role ?? "assistant",
1053
+ content: textContent,
1054
+ },
1055
+ finish_reason: responseBody.stop_reason ?? responseBody.stop_sequence ?? null,
1056
+ },
1057
+ ],
1058
+ usage: responseBody.usage
1059
+ ? {
1060
+ prompt_tokens: promptTokens,
1061
+ completion_tokens: completionTokens,
1062
+ total_tokens: promptTokens + completionTokens,
1063
+ }
1064
+ : undefined,
1065
+ };
1066
+ };
1067
+
1068
+ const passthroughResponse = (upstream: Response) => {
1069
+ const headers = new Headers(upstream.headers);
1070
+ return new Response(upstream.body, {
1071
+ status: upstream.status,
1072
+ statusText: upstream.statusText,
1073
+ headers,
1074
+ });
1075
+ };
1076
+
1077
+ const jsonResponse = async (upstream: Response) => {
1078
+ const data = await upstream.json().catch(async () => ({ raw: await upstream.text() }));
1079
+ return NextResponse.json(data, { status: upstream.status });
1080
+ };
1081
+
1082
+ const errorResponse = (status: number, error: unknown) =>
1083
+ NextResponse.json(
1084
+ {
1085
+ error: error instanceof Error ? error.message : String(error ?? "Unknown error"),
1086
+ },
1087
+ { status }
1088
+ );
1089
+
1090
+ export async function POST(request: NextRequest) {
1091
+ const body = (await request.json()) as GatewayChatBody;
1092
+ const provider = normalizeProvider(body.provider ?? DEFAULT_PROVIDER);
1093
+ const stream = body.stream !== false;
1094
+
1095
+ try {
1096
+ switch (provider) {
1097
+ case "openai": {
1098
+ const openaiKey = requireOpenAIKey();
1099
+ const { provider: _provider, ...cleanBody } = body;
1100
+ const requestBody = {
1101
+ ...cleanBody,
1102
+ stream,
1103
+ model: stripPrefix(body.model ?? DEFAULT_MODEL, "openai", "gpt-4o"),
1104
+ };
1105
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
1106
+ method: "POST",
1107
+ headers: {
1108
+ "Content-Type": "application/json",
1109
+ Authorization: \`Bearer \${openaiKey}\`,
1110
+ },
1111
+ body: JSON.stringify(requestBody),
1112
+ });
1113
+ if (!response.ok) {
1114
+ const details = await response.text();
1115
+ return NextResponse.json({ error: \`OpenAI chat failed: \${response.status}\`, details }, { status: response.status });
1116
+ }
1117
+ return stream ? passthroughResponse(response) : jsonResponse(response);
1118
+ }
1119
+
1120
+ case "xai": {
1121
+ const xaiKey = requireXAIKey();
1122
+ const { provider: _provider, ...cleanBody } = body;
1123
+ const requestBody = {
1124
+ ...cleanBody,
1125
+ stream,
1126
+ model: stripPrefix(body.model ?? DEFAULT_MODEL, "xai", "grok-2-latest"),
1127
+ };
1128
+ const response = await fetch(XAI_BASE_URL + "/chat/completions", {
1129
+ method: "POST",
1130
+ headers: {
1131
+ "Content-Type": "application/json",
1132
+ Authorization: "Bearer " + xaiKey,
1133
+ },
1134
+ body: JSON.stringify(requestBody),
1135
+ });
1136
+ if (!response.ok) {
1137
+ const details = await response.text();
1138
+ return NextResponse.json({ error: "xAI chat failed: " + response.status, details }, { status: response.status });
1139
+ }
1140
+ return stream ? passthroughResponse(response) : jsonResponse(response);
1141
+ }
1142
+
1143
+ case "anthropic": {
1144
+ const anthropicKey = requireAnthropicKey();
1145
+ const requestedModel = stripPrefix(body.model ?? DEFAULT_MODEL, "anthropic", "claude-3-5-haiku-latest");
1146
+ const stopSequences = Array.isArray(body.stop)
1147
+ ? body.stop
1148
+ : Array.isArray(body.stop_sequences)
1149
+ ? body.stop_sequences
1150
+ : body.stop
1151
+ ? [body.stop]
1152
+ : undefined;
1153
+ const { messages, system } = toAnthropicMessages(Array.isArray(body.messages) ? body.messages : []);
1154
+ const fallbackText = typeof body.prompt === "string" && body.prompt.trim().length > 0
1155
+ ? body.prompt
1156
+ : "Hello from Bandit quickstart gateway";
1157
+
1158
+ const requestBody: Record<string, unknown> = {
1159
+ model: requestedModel,
1160
+ messages: messages.length > 0
1161
+ ? messages
1162
+ : [
1163
+ {
1164
+ role: "user",
1165
+ content: [{ type: "text", text: fallbackText }],
1166
+ },
1167
+ ],
1168
+ stream,
1169
+ max_tokens: typeof body.max_tokens === "number" && body.max_tokens > 0 ? body.max_tokens : ANTHROPIC_MAX_TOKENS,
1170
+ };
1171
+
1172
+ if (system) requestBody.system = system;
1173
+ if (typeof body.temperature === "number") requestBody.temperature = body.temperature;
1174
+ if (typeof body.top_p === "number") requestBody.top_p = body.top_p;
1175
+ if (typeof body.top_k === "number") requestBody.top_k = body.top_k;
1176
+ if (stopSequences) requestBody.stop_sequences = stopSequences;
1177
+ if (body.metadata) requestBody.metadata = body.metadata;
1178
+ if (body.tools) requestBody.tools = body.tools;
1179
+ if (body.tool_choice) requestBody.tool_choice = body.tool_choice;
1180
+ if (body.thinking) requestBody.thinking = body.thinking;
1181
+
1182
+ const response = await fetch(\`\${ANTHROPIC_BASE_URL}/v1/messages\`, {
1183
+ method: "POST",
1184
+ headers: {
1185
+ "Content-Type": "application/json",
1186
+ "x-api-key": anthropicKey,
1187
+ "anthropic-version": ANTHROPIC_API_VERSION,
1188
+ },
1189
+ body: JSON.stringify(requestBody),
1190
+ });
1191
+
1192
+ if (!response.ok) {
1193
+ const details = await response.text();
1194
+ return NextResponse.json({ error: \`Anthropic chat failed: \${response.status}\`, details }, { status: response.status });
1195
+ }
1196
+
1197
+ if (stream) {
1198
+ return passthroughResponse(response);
1199
+ }
1200
+
1201
+ const data = await response.json();
1202
+ const normalized = convertAnthropicResponseToGateway(data, requestedModel);
1203
+ return NextResponse.json(normalized);
1204
+ }
1205
+
1206
+ case "azure": {
1207
+ const { apiKey } = requireAzureBaseConfig();
1208
+ const deployment = resolveAzureDeployment(body.model, AZURE_OPENAI_CHAT_DEPLOYMENT, "chat");
1209
+ const { provider: _provider, model: _model, ...cleanBody } = body;
1210
+ const requestBody = {
1211
+ ...cleanBody,
1212
+ stream,
1213
+ };
1214
+
1215
+ const response = await fetch(buildAzureDeploymentUrl(deployment, "chat/completions"), {
1216
+ method: "POST",
1217
+ headers: {
1218
+ "Content-Type": "application/json",
1219
+ "api-key": apiKey,
1220
+ },
1221
+ body: JSON.stringify(requestBody),
1222
+ });
1223
+
1224
+ if (!response.ok) {
1225
+ const details = await response.text();
1226
+ return NextResponse.json({ error: \`Azure OpenAI chat failed: \${response.status}\`, details }, { status: response.status });
1227
+ }
1228
+
1229
+ return stream ? passthroughResponse(response) : jsonResponse(response);
1230
+ }
1231
+
1232
+ case "ollama": {
1233
+ const { provider: _provider, ...cleanBody } = body;
1234
+ const requestBody = {
1235
+ ...cleanBody,
1236
+ stream,
1237
+ model: stripPrefix(body.model ?? DEFAULT_MODEL, "ollama", "llama3.1"),
1238
+ };
1239
+
1240
+ const response = await fetch(\`\${OLLAMA_URL}/api/chat\`, {
1241
+ method: "POST",
1242
+ headers: {
1243
+ "Content-Type": "application/json",
1244
+ },
1245
+ body: JSON.stringify(requestBody),
1246
+ });
1247
+
1248
+ if (!response.ok) {
1249
+ const details = await response.text();
1250
+ return NextResponse.json({ error: \`Ollama chat failed: \${response.status}\`, details }, { status: response.status });
1251
+ }
1252
+
1253
+ return stream ? passthroughResponse(response) : jsonResponse(response);
1254
+ }
1255
+
1256
+ default:
1257
+ return errorResponse(400, \`Unsupported provider: \${provider}\`);
1258
+ }
1259
+ } catch (error) {
1260
+ const message = error instanceof Error ? error.message : String(error);
1261
+ const status = message.startsWith("Missing") ? 400 : 500;
1262
+ return errorResponse(status, error);
1263
+ }
1264
+ }
1265
+
1266
+ `;
1267
+ var NEXT_HEALTH_ROUTE_TEMPLATE = `import { NextResponse } from "next/server";
1268
+
1269
+ export const dynamic = "force-dynamic";
1270
+
1271
+ const QUICKSTART_VERSION = "0.1.0";
1272
+ const OLLAMA_URL = (process.env.OLLAMA_URL ?? "http://localhost:11434").replace(/\\/$/, "");
1273
+ const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
1274
+ const AZURE_OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT ? process.env.AZURE_OPENAI_ENDPOINT.replace(/\\/$/, "") : undefined;
1275
+ const AZURE_OPENAI_API_KEY = process.env.AZURE_OPENAI_API_KEY;
1276
+ const AZURE_OPENAI_API_VERSION = process.env.AZURE_OPENAI_API_VERSION ?? "2024-08-01-preview";
1277
+ const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
1278
+ const ANTHROPIC_BASE_URL = (process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com").replace(/\\/$/, "");
1279
+ const ANTHROPIC_API_VERSION = process.env.ANTHROPIC_API_VERSION ?? "2023-06-01";
1280
+ const XAI_API_KEY = process.env.XAI_API_KEY;
1281
+ const XAI_BASE_URL = (process.env.XAI_BASE_URL ?? "https://api.x.ai/v1").replace(/\\/$/, "");
1282
+
1283
+ const isAzureConfigured = () => Boolean(AZURE_OPENAI_ENDPOINT && AZURE_OPENAI_API_KEY);
1284
+
1285
+ const buildAzurePath = (path: string) => {
1286
+ const normalized = path.replace(/^\\/+/, "");
1287
+ if (!AZURE_OPENAI_ENDPOINT) {
1288
+ throw new Error("Missing AZURE_OPENAI_ENDPOINT. Add it to your .env file to route requests to Azure OpenAI.");
1289
+ }
1290
+ return \`\${AZURE_OPENAI_ENDPOINT}/openai/\${normalized}?api-version=\${AZURE_OPENAI_API_VERSION}\`;
1291
+ };
1292
+
1293
+ export async function GET() {
1294
+ const providers: Array<Record<string, unknown>> = [];
1295
+
1296
+ // OpenAI
1297
+ try {
1298
+ if (OPENAI_API_KEY) {
1299
+ const response = await fetch("https://api.openai.com/v1/models", {
1300
+ headers: { Authorization: \`Bearer \${OPENAI_API_KEY}\` },
1301
+ });
1302
+ providers.push({
1303
+ name: "openai",
1304
+ status: response.ok ? "healthy" : "unhealthy",
1305
+ provider: "openai",
1306
+ });
1307
+ } else {
1308
+ providers.push({
1309
+ name: "openai",
1310
+ status: "unconfigured",
1311
+ provider: "openai",
1312
+ error: "API key not configured",
1313
+ });
1314
+ }
1315
+ } catch (error) {
1316
+ providers.push({
1317
+ name: "openai",
1318
+ status: "unhealthy",
1319
+ provider: "openai",
1320
+ error: error instanceof Error ? error.message : String(error),
1321
+ });
1322
+ }
1323
+
1324
+ // Azure
1325
+ if (AZURE_OPENAI_ENDPOINT || AZURE_OPENAI_API_KEY) {
1326
+ if (!isAzureConfigured()) {
1327
+ providers.push({
1328
+ name: "azure",
1329
+ status: "unconfigured",
1330
+ provider: "azure",
1331
+ error: "Endpoint or API key not configured",
1332
+ endpoint: AZURE_OPENAI_ENDPOINT,
1333
+ });
1334
+ } else {
1335
+ try {
1336
+ const response = await fetch(buildAzurePath("deployments"), {
1337
+ headers: { "api-key": AZURE_OPENAI_API_KEY ?? "" },
1338
+ });
1339
+ providers.push({
1340
+ name: "azure",
1341
+ status: response.ok ? "healthy" : "unhealthy",
1342
+ provider: "azure",
1343
+ endpoint: AZURE_OPENAI_ENDPOINT,
1344
+ });
1345
+ } catch (error) {
1346
+ providers.push({
1347
+ name: "azure",
1348
+ status: "unhealthy",
1349
+ provider: "azure",
1350
+ endpoint: AZURE_OPENAI_ENDPOINT,
1351
+ error: error instanceof Error ? error.message : String(error),
1352
+ });
1353
+ }
1354
+ }
1355
+ } else {
1356
+ providers.push({
1357
+ name: "azure",
1358
+ status: "unconfigured",
1359
+ provider: "azure",
1360
+ error: "Endpoint or API key not configured",
1361
+ });
1362
+ }
1363
+
1364
+ // Anthropic
1365
+ if (ANTHROPIC_API_KEY) {
1366
+ try {
1367
+ const response = await fetch(\`\${ANTHROPIC_BASE_URL}/v1/models\`, {
1368
+ headers: {
1369
+ "x-api-key": ANTHROPIC_API_KEY,
1370
+ "anthropic-version": ANTHROPIC_API_VERSION,
1371
+ },
1372
+ });
1373
+ providers.push({
1374
+ name: "anthropic",
1375
+ status: response.ok ? "healthy" : "unhealthy",
1376
+ provider: "anthropic",
1377
+ endpoint: ANTHROPIC_BASE_URL,
1378
+ });
1379
+ } catch (error) {
1380
+ providers.push({
1381
+ name: "anthropic",
1382
+ status: "unhealthy",
1383
+ provider: "anthropic",
1384
+ endpoint: ANTHROPIC_BASE_URL,
1385
+ error: error instanceof Error ? error.message : String(error),
1386
+ });
1387
+ }
1388
+ } else {
1389
+ providers.push({
1390
+ name: "anthropic",
1391
+ status: "unconfigured",
1392
+ provider: "anthropic",
1393
+ error: "API key not configured",
1394
+ });
1395
+ }
1396
+
1397
+ // xAI
1398
+ if (XAI_API_KEY) {
1399
+ try {
1400
+ const response = await fetch(XAI_BASE_URL + "/models", {
1401
+ headers: { Authorization: "Bearer " + XAI_API_KEY },
1402
+ });
1403
+ providers.push({
1404
+ name: "xai",
1405
+ status: response.ok ? "healthy" : "unhealthy",
1406
+ provider: "xai",
1407
+ endpoint: XAI_BASE_URL,
1408
+ });
1409
+ } catch (error) {
1410
+ providers.push({
1411
+ name: "xai",
1412
+ status: "unhealthy",
1413
+ provider: "xai",
1414
+ endpoint: XAI_BASE_URL,
1415
+ error: error instanceof Error ? error.message : String(error),
1416
+ });
1417
+ }
1418
+ } else {
1419
+ providers.push({
1420
+ name: "xai",
1421
+ status: "unconfigured",
1422
+ provider: "xai",
1423
+ error: "API key not configured",
1424
+ endpoint: XAI_BASE_URL,
1425
+ });
1426
+ }
1427
+
1428
+ // Ollama
1429
+ try {
1430
+ const response = await fetch(\`\${OLLAMA_URL}/api/tags\`);
1431
+ providers.push({
1432
+ name: "ollama",
1433
+ status: response.ok ? "healthy" : "unhealthy",
1434
+ provider: "ollama",
1435
+ url: OLLAMA_URL,
1436
+ });
1437
+ } catch (error) {
1438
+ providers.push({
1439
+ name: "ollama",
1440
+ status: "offline",
1441
+ provider: "ollama",
1442
+ url: OLLAMA_URL,
1443
+ error: error instanceof Error ? error.message : String(error),
1444
+ });
1445
+ }
1446
+
1447
+ const overallHealthy = providers.some((provider) => provider.status === "healthy");
1448
+
1449
+ return NextResponse.json({
1450
+ status: overallHealthy ? "healthy" : "unhealthy",
1451
+ version: QUICKSTART_VERSION,
1452
+ uptime: Math.round(process.uptime()),
1453
+ providers,
1454
+ });
1455
+ }
1456
+
819
1457
  `;
820
- const withResponse = template.replace(/__RESPONSE_STATUS__/g, responseStatusExpr);
821
- const withGatewayError = withResponse.replace(/__GATEWAY_ERROR__/g, gatewayErrorExpr);
822
- return ensureTrailingNewline(normalizeLineEndings(withGatewayError));
1458
+ var NEXT_MODELS_ROUTE_TEMPLATE = `import { NextResponse } from "next/server";
1459
+
1460
+ export const dynamic = "force-dynamic";
1461
+
1462
+ const BASE_GATEWAY_MODELS = __GATEWAY_MODELS__;
1463
+
1464
+ export function toGatewayModels() {
1465
+ return BASE_GATEWAY_MODELS.map((model) => ({
1466
+ ...model,
1467
+ created: Date.now(),
1468
+ modified_at: new Date().toISOString(),
1469
+ size: 0,
1470
+ digest: "",
1471
+ details: {
1472
+ format: "chat",
1473
+ family: model.provider,
1474
+ families: [model.provider],
1475
+ parameter_size: "",
1476
+ quantization_level: "",
1477
+ },
1478
+ }));
1479
+ }
1480
+
1481
+ export async function GET() {
1482
+ return NextResponse.json({ models: toGatewayModels() });
1483
+ }
1484
+
1485
+ `;
1486
+ var NEXT_GATEWAY_README_TEMPLATE = `# Next.js Gateway API
1487
+
1488
+ This directory contains a minimal Next.js App Router implementation of the Bandit gateway API. It mirrors the Express gateway in
1489
+ \`server/gateway.js\` but is ready to drop into a Next.js project.
1490
+
1491
+ ## Routes
1492
+
1493
+ - \`app/api/health/route.ts\` \u2013 provider health and availability checks
1494
+ - \`app/api/chat/completions/route.ts\` \u2013 provider-aware chat completions endpoint (OpenAI, Azure OpenAI, Anthropic, xAI, Ollama)
1495
+ - \`app/api/models/route.ts\` \u2013 exposes the scaffolded gateway model metadata used by the frontend
1496
+
1497
+ ## Usage
1498
+
1499
+ 1. Copy the contents of \`server/next-app/\` into the \`app/\` directory of a Next.js project.
1500
+ 2. Ensure the environment variables listed in \`.env.example\` are available to the Next.js runtime. At minimum you will want the
1501
+ provider API keys you plan to use (OpenAI, Azure OpenAI, Anthropic, xAI, or Ollama).
1502
+ 3. Start Next.js with \`npm run dev\` (or your project\u2019s equivalent). The routes are server-only (\`export const dynamic = "force-dynamic"\`)
1503
+ and can coexist with any frontend pages.
1504
+
1505
+ The generated routes favour clarity over cleverness so you can extend them with custom auth, logging, and provider routing logic.
1506
+ `;
1507
+ var buildNextChatRoute = (ctx) => {
1508
+ const fallbackModel = ctx.fallbackModelId ? `"${ctx.fallbackModelId}"` : "undefined";
1509
+ return ensureTrailingNewline(
1510
+ normalizeLineEndings(
1511
+ NEXT_CHAT_ROUTE_TEMPLATE.replace(/__DEFAULT_PROVIDER__/g, ctx.defaultProvider).replace(/__DEFAULT_MODEL__/g, ctx.defaultModelId).replace(/__FALLBACK_MODEL__/g, fallbackModel)
1512
+ )
1513
+ );
823
1514
  };
824
- var buildBrandingConfig = (ctx) => formatJson({
825
- branding: {
826
- logoBase64: ctx.isDefaultLogo ? null : ctx.logoBase64,
827
- brandingText: ctx.brandingText,
828
- theme: "bandit-dark",
829
- hasTransparentLogo: ctx.isDefaultLogo ? true : ctx.hasTransparentLogo
830
- },
831
- knowledgeDocs: []
832
- });
1515
+ var buildNextHealthRoute = () => ensureTrailingNewline(normalizeLineEndings(NEXT_HEALTH_ROUTE_TEMPLATE));
1516
+ var buildNextModelsRoute = (ctx) => {
1517
+ const modelsDefinition = JSON.stringify(ctx.gatewayModels, null, 2);
1518
+ return ensureTrailingNewline(
1519
+ normalizeLineEndings(
1520
+ NEXT_MODELS_ROUTE_TEMPLATE.replace("__GATEWAY_MODELS__", modelsDefinition)
1521
+ )
1522
+ );
1523
+ };
1524
+ var buildNextGatewayReadme = () => ensureTrailingNewline(normalizeLineEndings(NEXT_GATEWAY_README_TEMPLATE));
833
1525
  var buildGatewayServer = (ctx) => {
834
1526
  const modelsDefinition = JSON.stringify(ctx.gatewayModels, null, 2);
835
1527
  const gatewaySource = `import express from "express";
@@ -859,6 +1551,8 @@ const ANTHROPIC_API_VERSION = process.env.ANTHROPIC_API_VERSION ?? "2023-06-01";
859
1551
  const ANTHROPIC_MAX_TOKENS = Number.isFinite(Number(process.env.ANTHROPIC_MAX_TOKENS))
860
1552
  ? Number(process.env.ANTHROPIC_MAX_TOKENS)
861
1553
  : 1024;
1554
+ const XAI_API_KEY = process.env.XAI_API_KEY;
1555
+ const XAI_BASE_URL = (process.env.XAI_BASE_URL ?? "https://api.x.ai/v1").replace(/\\/$/, "");
862
1556
 
863
1557
  const toGatewayModels = () =>
864
1558
  BASE_GATEWAY_MODELS.map((model) => ({
@@ -1065,6 +1759,14 @@ const requireOpenAIKey = () => {
1065
1759
  return key;
1066
1760
  };
1067
1761
 
1762
+ const requireXAIKey = () => {
1763
+ const key = XAI_API_KEY;
1764
+ if (!key) {
1765
+ throw new Error("Missing XAI_API_KEY. Add it to your .env file to route requests to xAI.");
1766
+ }
1767
+ return key;
1768
+ };
1769
+
1068
1770
  // Utility function to handle streaming responses
1069
1771
  const handleStreamingResponse = async (upstreamResponse, res) => {
1070
1772
  res.setHeader('Content-Type', 'text/event-stream');
@@ -1093,6 +1795,93 @@ const handleStreamingResponse = async (upstreamResponse, res) => {
1093
1795
  }
1094
1796
  };
1095
1797
 
1798
+ const relayAnthropicStream = async (upstreamResponse, res) => {
1799
+ res.setHeader('Content-Type', 'text/event-stream');
1800
+ res.setHeader('Cache-Control', 'no-cache');
1801
+ res.setHeader('Connection', 'keep-alive');
1802
+ res.setHeader('Access-Control-Allow-Origin', '*');
1803
+
1804
+ const reader = upstreamResponse.body?.getReader();
1805
+ if (!reader) {
1806
+ const fallback = await upstreamResponse.text();
1807
+ res.write("data: " + JSON.stringify({ choices: [{ delta: { content: fallback } }] }) + "\\n\\n");
1808
+ res.write("data: [DONE]\\n\\n");
1809
+ return res.end();
1810
+ }
1811
+
1812
+ const decoder = new TextDecoder();
1813
+ let buffer = '';
1814
+
1815
+ const sendChunk = (payload) => {
1816
+ res.write("data: " + JSON.stringify(payload) + "\\n\\n");
1817
+ };
1818
+
1819
+ try {
1820
+ while (true) {
1821
+ const { value, done } = await reader.read();
1822
+ if (done) break;
1823
+ buffer += decoder.decode(value, { stream: true });
1824
+
1825
+ let delimiterIndex;
1826
+ while ((delimiterIndex = buffer.indexOf('\\n\\n')) >= 0) {
1827
+ const rawEvent = buffer.slice(0, delimiterIndex).trim();
1828
+ buffer = buffer.slice(delimiterIndex + 2);
1829
+ if (!rawEvent) continue;
1830
+
1831
+ const lines = rawEvent.split('\\n');
1832
+ const eventLine = lines.find((line) => line.startsWith('event:')) ?? '';
1833
+ const dataLine = lines.find((line) => line.startsWith('data:')) ?? '';
1834
+ const event = eventLine.replace('event:', '').trim();
1835
+ const trimmedData = dataLine.replace('data:', '').trim();
1836
+
1837
+ if (!trimmedData) {
1838
+ continue;
1839
+ }
1840
+
1841
+ let parsed;
1842
+ try {
1843
+ parsed = JSON.parse(trimmedData);
1844
+ } catch (error) {
1845
+ console.error('Anthropic stream parse error', error, { rawEvent });
1846
+ continue;
1847
+ }
1848
+
1849
+ if (event === 'content_block_delta') {
1850
+ const textChunk = parsed?.delta?.text ?? '';
1851
+ if (textChunk) {
1852
+ sendChunk({
1853
+ choices: [
1854
+ {
1855
+ delta: {
1856
+ content: textChunk,
1857
+ },
1858
+ },
1859
+ ],
1860
+ });
1861
+ }
1862
+ } else if (event === 'message_stop') {
1863
+ sendChunk({
1864
+ choices: [
1865
+ {
1866
+ delta: {},
1867
+ finish_reason: 'stop',
1868
+ },
1869
+ ],
1870
+ });
1871
+ }
1872
+ }
1873
+ }
1874
+ } catch (error) {
1875
+ console.error('Anthropic streaming relay error', error);
1876
+ sendChunk({
1877
+ error: error instanceof Error ? error.message : String(error),
1878
+ });
1879
+ } finally {
1880
+ res.write("data: [DONE]\\n\\n");
1881
+ res.end();
1882
+ }
1883
+ };
1884
+
1096
1885
  // ============================================================================
1097
1886
  // GENERAL HEALTH & MODELS
1098
1887
  // ============================================================================
@@ -1202,6 +1991,37 @@ app.get("/api/health", async (_req, res) => {
1202
1991
  });
1203
1992
  }
1204
1993
 
1994
+ // Check xAI
1995
+ if (XAI_API_KEY) {
1996
+ try {
1997
+ const response = await fetch(XAI_BASE_URL + "/models", {
1998
+ headers: { "Authorization": "Bearer " + XAI_API_KEY }
1999
+ });
2000
+ providers.push({
2001
+ name: "xai",
2002
+ status: response.ok ? "healthy" : "unhealthy",
2003
+ provider: "xai",
2004
+ endpoint: XAI_BASE_URL
2005
+ });
2006
+ } catch (error) {
2007
+ providers.push({
2008
+ name: "xai",
2009
+ status: "unhealthy",
2010
+ provider: "xai",
2011
+ error: error instanceof Error ? error.message : String(error),
2012
+ endpoint: XAI_BASE_URL
2013
+ });
2014
+ }
2015
+ } else {
2016
+ providers.push({
2017
+ name: "xai",
2018
+ status: "unconfigured",
2019
+ provider: "xai",
2020
+ error: "API key not configured",
2021
+ endpoint: XAI_BASE_URL
2022
+ });
2023
+ }
2024
+
1205
2025
  // Check Ollama
1206
2026
  try {
1207
2027
  console.log(\`Checking Ollama health at: \${OLLAMA_BASE_URL}/api/tags\`);
@@ -1277,7 +2097,7 @@ app.post("/api/anthropic/chat/completions", async (req, res) => {
1277
2097
  const requestedModel =
1278
2098
  stripAnthropicModelPrefix(rawBody.model) ??
1279
2099
  stripAnthropicModelPrefix("${ctx.defaultModelId}") ??
1280
- "claude-3-5-sonnet-latest";
2100
+ "claude-3-5-haiku-latest";
1281
2101
 
1282
2102
  const stopSequences = Array.isArray(rawBody.stop)
1283
2103
  ? rawBody.stop
@@ -1360,7 +2180,7 @@ app.post("/api/anthropic/chat/completions", async (req, res) => {
1360
2180
  }
1361
2181
 
1362
2182
  if (isStreaming) {
1363
- await handleStreamingResponse(response, res);
2183
+ await relayAnthropicStream(response, res);
1364
2184
  } else {
1365
2185
  const data = await response.json();
1366
2186
  const normalized = convertAnthropicResponseToGateway(data, requestedModel);
@@ -1455,7 +2275,7 @@ app.post("/api/anthropic/completions", async (req, res) => {
1455
2275
  }
1456
2276
 
1457
2277
  if (isStreaming) {
1458
- await handleStreamingResponse(response, res);
2278
+ await relayAnthropicStream(response, res);
1459
2279
  } else {
1460
2280
  const data = await response.json();
1461
2281
  const formatted = convertAnthropicResponseToGenerate(data, requestedModel);
@@ -1728,6 +2548,201 @@ app.post("/api/azure/embed", async (req, res) => {
1728
2548
  }
1729
2549
  });
1730
2550
 
2551
+ // ============================================================================
2552
+ // XAI ROUTES
2553
+ // ============================================================================
2554
+
2555
+ // xAI Health Check
2556
+ app.get("/api/xai/health", async (_req, res) => {
2557
+ try {
2558
+ const xaiKey = requireXAIKey();
2559
+ const response = await fetch(XAI_BASE_URL + "/models", {
2560
+ headers: { "Authorization": "Bearer " + xaiKey }
2561
+ });
2562
+ const isHealthy = response.ok;
2563
+ res.json({
2564
+ status: isHealthy ? "healthy" : "unhealthy",
2565
+ xai_status: isHealthy,
2566
+ provider: "xai"
2567
+ });
2568
+ } catch (error) {
2569
+ res.status(503).json({
2570
+ status: "unhealthy",
2571
+ xai_status: false,
2572
+ error: error instanceof Error ? error.message : String(error),
2573
+ provider: "xai"
2574
+ });
2575
+ }
2576
+ });
2577
+
2578
+ // xAI Chat Completions
2579
+ app.post("/api/xai/chat/completions", async (req, res) => {
2580
+ try {
2581
+ const xaiKey = requireXAIKey();
2582
+ const isStreaming = req.body?.stream === true;
2583
+ const { provider, ...cleanBody } = req.body ?? {};
2584
+ const requestBody = {
2585
+ ...cleanBody,
2586
+ model: req.body?.model?.replace(/^xai:/, "") || "grok-2-latest"
2587
+ };
2588
+
2589
+ const response = await fetch(XAI_BASE_URL + "/chat/completions", {
2590
+ method: "POST",
2591
+ headers: {
2592
+ "Content-Type": "application/json",
2593
+ "Authorization": "Bearer " + xaiKey
2594
+ },
2595
+ body: JSON.stringify(requestBody)
2596
+ });
2597
+
2598
+ if (!response.ok) {
2599
+ const errorText = await response.text();
2600
+ return res.status(response.status).json({
2601
+ error: "xAI chat failed: " + response.status,
2602
+ details: errorText
2603
+ });
2604
+ }
2605
+
2606
+ if (isStreaming) {
2607
+ await handleStreamingResponse(response, res);
2608
+ } else {
2609
+ const text = await response.text();
2610
+ res.setHeader('Content-Type', 'application/json');
2611
+ res.send(text);
2612
+ }
2613
+ } catch (error) {
2614
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
2615
+ }
2616
+ });
2617
+
2618
+ app.post("/api/xai/chat", async (req, res) => {
2619
+ req.url = "/api/xai/chat/completions";
2620
+ return app._router.handle(req, res);
2621
+ });
2622
+
2623
+ // xAI Completions
2624
+ app.post("/api/xai/completions", async (req, res) => {
2625
+ try {
2626
+ const xaiKey = requireXAIKey();
2627
+ const isStreaming = req.body?.stream === true;
2628
+ const { provider, ...cleanBody } = req.body ?? {};
2629
+ const requestBody = {
2630
+ ...cleanBody,
2631
+ model: req.body?.model?.replace(/^xai:/, "") || "grok-2-mini"
2632
+ };
2633
+
2634
+ const response = await fetch(XAI_BASE_URL + "/completions", {
2635
+ method: "POST",
2636
+ headers: {
2637
+ "Content-Type": "application/json",
2638
+ "Authorization": "Bearer " + xaiKey
2639
+ },
2640
+ body: JSON.stringify(requestBody)
2641
+ });
2642
+
2643
+ if (!response.ok) {
2644
+ const errorText = await response.text();
2645
+ return res.status(response.status).json({
2646
+ error: "xAI completions failed: " + response.status,
2647
+ details: errorText
2648
+ });
2649
+ }
2650
+
2651
+ if (isStreaming) {
2652
+ await handleStreamingResponse(response, res);
2653
+ } else {
2654
+ const text = await response.text();
2655
+ res.setHeader('Content-Type', 'application/json');
2656
+ res.send(text);
2657
+ }
2658
+ } catch (error) {
2659
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
2660
+ }
2661
+ });
2662
+
2663
+ // xAI Generate
2664
+ app.post("/api/xai/generate", async (req, res) => {
2665
+ try {
2666
+ const xaiKey = requireXAIKey();
2667
+ const prompt = req.body?.prompt || "";
2668
+ const model = req.body?.model?.replace(/^xai:/, "") || "grok-2-latest";
2669
+ const isStreaming = req.body?.stream === true;
2670
+
2671
+ const chatBody = {
2672
+ model,
2673
+ messages: [
2674
+ { role: "user", content: prompt }
2675
+ ],
2676
+ stream: isStreaming,
2677
+ max_tokens: req.body?.max_tokens || 150,
2678
+ temperature: req.body?.temperature ?? 0.7
2679
+ };
2680
+
2681
+ const response = await fetch(XAI_BASE_URL + "/chat/completions", {
2682
+ method: "POST",
2683
+ headers: {
2684
+ "Content-Type": "application/json",
2685
+ "Authorization": "Bearer " + xaiKey
2686
+ },
2687
+ body: JSON.stringify(chatBody)
2688
+ });
2689
+
2690
+ if (!response.ok) {
2691
+ const errorText = await response.text();
2692
+ return res.status(response.status).json({
2693
+ error: "xAI generate failed: " + response.status,
2694
+ details: errorText
2695
+ });
2696
+ }
2697
+
2698
+ if (isStreaming) {
2699
+ await handleStreamingResponse(response, res);
2700
+ } else {
2701
+ const data = await response.json();
2702
+ const generateResponse = {
2703
+ model,
2704
+ created_at: new Date().toISOString(),
2705
+ response: data.choices?.[0]?.message?.content || "",
2706
+ done: true,
2707
+ context: [],
2708
+ total_duration: 0,
2709
+ load_duration: 0,
2710
+ prompt_eval_count: data.usage?.prompt_tokens || 0,
2711
+ prompt_eval_duration: 0,
2712
+ eval_count: data.usage?.completion_tokens || 0,
2713
+ eval_duration: 0
2714
+ };
2715
+ res.json(generateResponse);
2716
+ }
2717
+ } catch (error) {
2718
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
2719
+ }
2720
+ });
2721
+
2722
+ // xAI Models
2723
+ app.get("/api/xai/models", 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
+
2730
+ if (!response.ok) {
2731
+ const errorText = await response.text();
2732
+ return res.status(response.status).json({
2733
+ error: "xAI models failed: " + response.status,
2734
+ details: errorText
2735
+ });
2736
+ }
2737
+
2738
+ const text = await response.text();
2739
+ res.setHeader('Content-Type', 'application/json');
2740
+ res.send(text);
2741
+ } catch (error) {
2742
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
2743
+ }
2744
+ });
2745
+
1731
2746
  // ============================================================================
1732
2747
  // OPENAI ROUTES
1733
2748
  // ============================================================================
@@ -2250,11 +3265,12 @@ app.all("/api/anthropic/*", (_req, res) => {
2250
3265
  const port = Number(process.env.PORT ?? ${ctx.gatewayPort});
2251
3266
  app.listen(port, () => {
2252
3267
  console.log("\u26A1 Bandit quickstart gateway ready on http://localhost:" + port);
2253
- console.log("\u{1F4E1} Supported providers: OpenAI, Azure OpenAI, Anthropic, Ollama");
3268
+ console.log("\u{1F4E1} Supported providers: OpenAI, Azure OpenAI, Anthropic, XAI, Ollama");
2254
3269
  console.log("\u{1F517} Provider-specific routes:");
2255
3270
  console.log(" \u2022 /api/openai/* - OpenAI endpoints");
2256
3271
  console.log(" \u2022 /api/azure/* - Azure OpenAI endpoints");
2257
3272
  console.log(" \u2022 /api/anthropic/* - Anthropic endpoints");
3273
+ console.log(" \u2022 /api/xai/* - XAI endpoints");
2258
3274
  console.log(" \u2022 /api/ollama/* - Ollama endpoints");
2259
3275
  console.log(" \u2022 /api/health - Overall health check");
2260
3276
  });
@@ -2273,20 +3289,16 @@ dist
2273
3289
  `
2274
3290
  )
2275
3291
  );
2276
- var buildNpmrc = () => ensureTrailingNewline(
2277
- normalizeLineEndings(`registry=https://registry.npmjs.org/
2278
- `)
2279
- );
2280
3292
  var buildReadme = (ctx) => ensureTrailingNewline(
2281
3293
  normalizeLineEndings(
2282
3294
  `# ${ctx.projectTitle} \u2014 Bandit Quickstart
2283
3295
 
2284
- This project was generated by the Bandit Engine CLI. It ships with a React + Vite frontend that consumes \`@burtson-labs/bandit-engine\` and a lightweight Express gateway you can adapt for production.
3296
+ 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/\`.
2285
3297
 
2286
3298
  ## \u{1F680} Next steps
2287
3299
  - \`npm install\`
2288
3300
  - \`cp .env.example .env\`
2289
- - Fill in your OpenAI, Azure OpenAI, or Anthropic credentials (or point \`OLLAMA_URL\` at your local server)
3301
+ - Fill in your OpenAI, Azure OpenAI, Anthropic, or xAI credentials (or point \`OLLAMA_URL\` at your local server)
2290
3302
  - \`npm run dev\`
2291
3303
 
2292
3304
  The command runs the gateway and the frontend together. Visit http://localhost:${ctx.frontendPort} to see the chat and modal in action.
@@ -2299,7 +3311,8 @@ The command runs the gateway and the frontend together. Visit http://localhost:$
2299
3311
  ## \u{1F4E6} What\u2019s inside
2300
3312
  - React + Vite 5 with Material UI theming
2301
3313
  - Bandit chat surface + modal wired via \`ChatProvider\`
2302
- - Express gateway proxying OpenAI, Azure OpenAI, Anthropic, or Ollama to keep API keys server-side
3314
+ - Express gateway proxying OpenAI, Azure OpenAI, Anthropic, XAI, or Ollama to keep API keys server-side
3315
+ - Next.js App Router gateway scaffold in 'server/next-app/' for projects that prefer Next
2303
3316
  - Friendly defaults you can evolve into your production stack
2304
3317
 
2305
3318
  Need more? Run \`npx @burtson-labs/bandit-engine create --help\` to explore additional options.
@@ -2315,7 +3328,7 @@ var createQuickstartProject = async (options) => {
2315
3328
  const projectTitle = toTitleCase(rawProjectName) || "Bandit Quickstart";
2316
3329
  await ensureWritableDirectory(resolvedDir, Boolean(options.force));
2317
3330
  const skipPrompts = Boolean(options.skipPrompts);
2318
- const provider = options.provider ? normalizeProvider(options.provider) : skipPrompts ? "openai" : await promptForProvider();
3331
+ const provider = options.provider ? normalizeProvider(options.provider) : skipPrompts ? "ollama" : await promptForProvider();
2319
3332
  const promptAnswers = skipPrompts ? {} : await promptForMissingData({
2320
3333
  brandingText: options.brandingText,
2321
3334
  provider
@@ -2333,7 +3346,8 @@ var createQuickstartProject = async (options) => {
2333
3346
  const defaultModelId = sanitizeModelIdentifier(
2334
3347
  options.defaultModelId ?? inferDefaultModelId(provider)
2335
3348
  );
2336
- const fallbackModelId = options.fallbackModelId ? sanitizeModelIdentifier(options.fallbackModelId) : inferFallbackModelId(provider, defaultModelId);
3349
+ const fallbackModelRaw = options.fallbackModelId ? options.fallbackModelId : inferFallbackModelId(provider, defaultModelId);
3350
+ const fallbackModelId = fallbackModelRaw ? sanitizeModelIdentifier(fallbackModelRaw) : void 0;
2337
3351
  const inputs = {
2338
3352
  targetDir: resolvedDir,
2339
3353
  projectTitle,
@@ -2390,38 +3404,52 @@ var normalizeProvider = (value) => {
2390
3404
  if (normalized === "anthropic") {
2391
3405
  return "anthropic";
2392
3406
  }
3407
+ if (normalized === "xai" || normalized === "grok") {
3408
+ return "xai";
3409
+ }
2393
3410
  return "openai";
2394
3411
  };
2395
3412
  var inferDefaultModelId = (provider) => {
2396
3413
  if (provider === "ollama") {
2397
- return "ollama:llama3.1";
3414
+ return "llama3.1";
2398
3415
  }
2399
3416
  if (provider === "azure") {
2400
3417
  return "azure:gpt-4o";
2401
3418
  }
2402
3419
  if (provider === "anthropic") {
2403
- return "anthropic:claude-3-5-sonnet-latest";
3420
+ return "anthropic:claude-3-5-haiku-latest";
3421
+ }
3422
+ if (provider === "xai") {
3423
+ return "xai:grok-2-latest";
2404
3424
  }
2405
3425
  return "openai:gpt-4o-mini";
2406
3426
  };
2407
3427
  var inferFallbackModelId = (provider, defaultId) => {
2408
3428
  if (provider === "ollama") {
2409
- return defaultId === "ollama:llama3" ? "ollama:llama2" : "ollama:llama3";
3429
+ const normalized = defaultId.toLowerCase();
3430
+ if (normalized.startsWith("llama3")) {
3431
+ return "llama2";
3432
+ }
3433
+ return "llama3";
2410
3434
  }
2411
3435
  if (provider === "azure") {
2412
3436
  return defaultId === "azure:gpt-4o-mini" ? "azure:gpt-4o" : "azure:gpt-4o-mini";
2413
3437
  }
2414
3438
  if (provider === "anthropic") {
2415
- return defaultId === "anthropic:claude-3-5-haiku-latest" ? "anthropic:claude-3-5-sonnet-latest" : "anthropic:claude-3-5-haiku-latest";
3439
+ return defaultId === "anthropic:claude-3-5-haiku-latest" ? "anthropic:claude-3-haiku-20240307" : "anthropic:claude-3-5-haiku-latest";
3440
+ }
3441
+ if (provider === "xai") {
3442
+ return defaultId === "xai:grok-2-mini" ? "xai:grok-2-latest" : "xai:grok-2-mini";
2416
3443
  }
2417
3444
  return defaultId === "openai:gpt-4.1-mini" ? "openai:gpt-4o-mini" : "openai:gpt-4.1-mini";
2418
3445
  };
2419
3446
  var promptForProvider = async () => {
2420
3447
  const providerOptions = [
2421
- { label: "OpenAI (default)", value: "openai" },
3448
+ { label: "Ollama (self-hosted) \u2014 default", value: "ollama" },
3449
+ { label: "OpenAI", value: "openai" },
2422
3450
  { label: "Azure OpenAI", value: "azure" },
2423
3451
  { label: "Anthropic", value: "anthropic" },
2424
- { label: "Ollama (self-hosted)", value: "ollama" }
3452
+ { label: "xAI (Grok)", value: "xai" }
2425
3453
  ];
2426
3454
  const messageLines = [
2427
3455
  "Which provider should we configure for the gateway?",
@@ -2447,7 +3475,7 @@ var promptForProvider = async () => {
2447
3475
  { onCancel }
2448
3476
  );
2449
3477
  const selectedIndex = typeof answers.providerIndex === "number" && answers.providerIndex >= 1 ? answers.providerIndex - 1 : 0;
2450
- return providerOptions[selectedIndex]?.value ?? "openai";
3478
+ return providerOptions[selectedIndex]?.value ?? "ollama";
2451
3479
  };
2452
3480
  var sanitizePort = (value) => {
2453
3481
  const port = Number(value);
@@ -2537,9 +3565,12 @@ var writeProject = async (inputs) => {
2537
3565
  "src/theme.ts": buildThemeTs(),
2538
3566
  "public/config.json": buildBrandingConfig(context),
2539
3567
  "server/gateway.js": buildGatewayServer(context),
3568
+ "server/next-app/app/api/chat/completions/route.ts": buildNextChatRoute(context),
3569
+ "server/next-app/app/api/health/route.ts": buildNextHealthRoute(),
3570
+ "server/next-app/app/api/models/route.ts": buildNextModelsRoute(context),
3571
+ "server/next-app/README.md": buildNextGatewayReadme(),
2540
3572
  ".env.example": buildEnvExample(context),
2541
3573
  ".gitignore": buildGitignore(),
2542
- ".npmrc": buildNpmrc(),
2543
3574
  "README.md": buildReadme(context)
2544
3575
  };
2545
3576
  if (!inputs.logo.isDefault && inputs.logo.fileName) {
@@ -2619,6 +3650,11 @@ program.command("create").description("Scaffold a Bandit quickstart project with
2619
3650
  console.log(" cp .env.example .env");
2620
3651
  console.log(" npm run dev");
2621
3652
  console.log("");
3653
+ console.log("\u{1F50D} Before you dive in:");
3654
+ console.log(" \u2022 Open .env to confirm the provider credentials and URLs match your setup.");
3655
+ 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.");
3656
+ console.log(" \u2022 If you prefer Next.js App Router, check server/next-app/ for a starter route handler.");
3657
+ console.log("");
2622
3658
  } catch (error) {
2623
3659
  const message = error instanceof Error ? error.message : "Failed to create Bandit quickstart project.";
2624
3660
  console.error(`