@burtson-labs/bandit-engine 2.0.35 → 2.0.36

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 +3 -2
  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 +1078 -62
  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.36",
34
34
  license: "BUSL-1.1",
35
35
  main: "dist/index.js",
36
36
  module: "dist/index.mjs",
@@ -214,33 +214,52 @@ var buildPackageJson = (ctx) => formatJson({
214
214
  "vite": "^7.1.9"
215
215
  }
216
216
  });
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
- );
217
+ var buildEnvExample = (ctx) => {
218
+ const lines = [
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
+ "# These values power server/gateway.js \u2014 update them before running in production."
229
+ ];
230
+ switch (ctx.defaultProvider) {
231
+ case "openai":
232
+ lines.push("OPENAI_API_KEY=");
233
+ break;
234
+ case "azure":
235
+ lines.push("AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com");
236
+ lines.push("AZURE_OPENAI_API_KEY=");
237
+ lines.push("AZURE_OPENAI_API_VERSION=2024-08-01-preview");
238
+ lines.push("AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-4o");
239
+ lines.push("AZURE_OPENAI_COMPLETIONS_DEPLOYMENT=gpt-35-turbo-instruct");
240
+ lines.push("AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT=text-embedding-3-large");
241
+ break;
242
+ case "anthropic":
243
+ lines.push("ANTHROPIC_API_KEY=");
244
+ lines.push("ANTHROPIC_BASE_URL=https://api.anthropic.com");
245
+ lines.push("ANTHROPIC_API_VERSION=2023-06-01");
246
+ lines.push("ANTHROPIC_MAX_TOKENS=1024");
247
+ break;
248
+ case "xai":
249
+ lines.push("XAI_API_KEY=");
250
+ lines.push("XAI_BASE_URL=https://api.x.ai/v1");
251
+ break;
252
+ case "ollama":
253
+ default:
254
+ lines.push("OLLAMA_URL=http://localhost:11434");
255
+ break;
256
+ }
257
+ lines.push(`PORT=${ctx.gatewayPort}`);
258
+ lines.push(
259
+ "# If you switch providers later, copy the relevant block above and update the credentials."
260
+ );
261
+ return ensureTrailingNewline(normalizeLineEndings(lines.join("\n")));
262
+ };
244
263
  var buildTsConfig = () => formatJson({
245
264
  compilerOptions: {
246
265
  target: "ESNext",
@@ -439,7 +458,7 @@ const gatewayBaseUrl = (import.meta.env.VITE_GATEWAY_URL ?? "${ctx.defaultGatewa
439
458
  const defaultModelId = import.meta.env.VITE_DEFAULT_MODEL ?? "${ctx.defaultModelId}";
440
459
  const fallbackModelId = import.meta.env.VITE_FALLBACK_MODEL ?? ${ctx.fallbackModelId ? `${QUOTE}${ctx.fallbackModelId}${QUOTE}` : "undefined"};
441
460
  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";
461
+ const provider = (import.meta.env.VITE_GATEWAY_PROVIDER ?? "${ctx.defaultProvider}") as "openai" | "ollama" | "azure" | "anthropic" | "xai";
443
462
 
444
463
  const gatewayApiUrl = gatewayBaseUrl.endsWith("/api") ? gatewayBaseUrl : gatewayBaseUrl + "/api";
445
464
  const banditHeadLogoUrl = "https://cdn.burtson.ai/images/bandit-head.png";
@@ -621,7 +640,7 @@ function App() {
621
640
  {brandingText}
622
641
  </Typography>
623
642
  <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.
643
+ 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
644
  </Typography>
626
645
  <Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
627
646
  <Button component={RouterLink} to="/chat" variant="contained" color="primary">
@@ -668,7 +687,7 @@ function App() {
668
687
  Ship secure gateways
669
688
  </Typography>
670
689
  <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.
690
+ Keep API keys server-side while proxying requests to OpenAI, Azure OpenAI, Anthropic, XAI, or Ollama through the included Express gateway.
672
691
  </Typography>
673
692
  </CardContent>
674
693
  </Card>
@@ -815,21 +834,679 @@ function App() {
815
834
  );
816
835
  }
817
836
 
818
- export default App;
837
+ export default App;
838
+ `;
839
+ const withResponse = template.replace(/__RESPONSE_STATUS__/g, responseStatusExpr);
840
+ const withGatewayError = withResponse.replace(/__GATEWAY_ERROR__/g, gatewayErrorExpr);
841
+ return ensureTrailingNewline(normalizeLineEndings(withGatewayError));
842
+ };
843
+ var buildBrandingConfig = (ctx) => formatJson({
844
+ branding: {
845
+ logoBase64: ctx.isDefaultLogo ? null : ctx.logoBase64,
846
+ brandingText: ctx.brandingText,
847
+ theme: "bandit-dark",
848
+ hasTransparentLogo: ctx.isDefaultLogo ? true : ctx.hasTransparentLogo
849
+ },
850
+ knowledgeDocs: []
851
+ });
852
+ var NEXT_CHAT_ROUTE_TEMPLATE = `import { NextRequest, NextResponse } from "next/server";
853
+
854
+ export const dynamic = "force-dynamic";
855
+
856
+ const DEFAULT_PROVIDER = "__DEFAULT_PROVIDER__";
857
+ const DEFAULT_MODEL = "__DEFAULT_MODEL__";
858
+ const FALLBACK_MODEL = __FALLBACK_MODEL__;
859
+
860
+ const OLLAMA_URL = (process.env.OLLAMA_URL ?? "http://localhost:11434").replace(/\\/$/, "");
861
+ const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
862
+ const AZURE_OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT ? process.env.AZURE_OPENAI_ENDPOINT.replace(/\\/$/, "") : undefined;
863
+ const AZURE_OPENAI_API_KEY = process.env.AZURE_OPENAI_API_KEY;
864
+ const AZURE_OPENAI_API_VERSION = process.env.AZURE_OPENAI_API_VERSION ?? "2024-08-01-preview";
865
+ const AZURE_OPENAI_CHAT_DEPLOYMENT = process.env.AZURE_OPENAI_CHAT_DEPLOYMENT;
866
+ const AZURE_OPENAI_COMPLETIONS_DEPLOYMENT = process.env.AZURE_OPENAI_COMPLETIONS_DEPLOYMENT ?? AZURE_OPENAI_CHAT_DEPLOYMENT;
867
+ const AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT = process.env.AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT;
868
+ const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
869
+ const ANTHROPIC_BASE_URL = (process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com").replace(/\\/$/, "");
870
+ const ANTHROPIC_API_VERSION = process.env.ANTHROPIC_API_VERSION ?? "2023-06-01";
871
+ const ANTHROPIC_MAX_TOKENS = Number.isFinite(Number(process.env.ANTHROPIC_MAX_TOKENS))
872
+ ? Number(process.env.ANTHROPIC_MAX_TOKENS)
873
+ : 1024;
874
+ const XAI_API_KEY = process.env.XAI_API_KEY;
875
+ const XAI_BASE_URL = (process.env.XAI_BASE_URL ?? "https://api.x.ai/v1").replace(/\\/$/, "");
876
+
877
+ interface GatewayChatBody {
878
+ provider?: string;
879
+ model?: string;
880
+ messages?: Array<{ role: string; content: unknown }>;
881
+ prompt?: string;
882
+ stream?: boolean;
883
+ temperature?: number;
884
+ max_tokens?: number;
885
+ top_p?: number;
886
+ stop?: string | string[];
887
+ stop_sequences?: string | string[];
888
+ tools?: unknown;
889
+ tool_choice?: unknown;
890
+ metadata?: unknown;
891
+ thinking?: unknown;
892
+ images?: string[];
893
+ [key: string]: unknown;
894
+ }
895
+
896
+ const normalizeProvider = (input: string): "openai" | "azure" | "anthropic" | "ollama" | "xai" => {
897
+ const value = input.toLowerCase();
898
+ if (value === "azure-openai" || value === "azureopenai" || value === "azure") return "azure";
899
+ if (value === "anthropic" || value === "claude") return "anthropic";
900
+ if (value === "ollama") return "ollama";
901
+ if (value === "xai" || value === "grok") return "xai";
902
+ return "openai";
903
+ };
904
+
905
+ const stripPrefix = (model: unknown, prefix: string, fallback: string): string => {
906
+ if (typeof model === "string") {
907
+ return model.replace(new RegExp(\`^\${prefix}:\`), "");
908
+ }
909
+ return fallback;
910
+ };
911
+
912
+ const requireOpenAIKey = () => {
913
+ if (!OPENAI_API_KEY) {
914
+ throw new Error("Missing OPENAI_API_KEY. Add it to your .env file to route requests to OpenAI.");
915
+ }
916
+ return OPENAI_API_KEY;
917
+ };
918
+
919
+ const requireXAIKey = () => {
920
+ if (!XAI_API_KEY) {
921
+ throw new Error("Missing XAI_API_KEY. Add it to your .env file to route requests to xAI.");
922
+ }
923
+ return XAI_API_KEY;
924
+ };
925
+
926
+ const requireAnthropicKey = () => {
927
+ if (!ANTHROPIC_API_KEY) {
928
+ throw new Error("Missing ANTHROPIC_API_KEY. Add it to your .env file to route requests to Anthropic.");
929
+ }
930
+ return ANTHROPIC_API_KEY;
931
+ };
932
+
933
+ const isAzureConfigured = () => Boolean(AZURE_OPENAI_ENDPOINT && AZURE_OPENAI_API_KEY);
934
+
935
+ const requireAzureBaseConfig = () => {
936
+ if (!AZURE_OPENAI_ENDPOINT) {
937
+ throw new Error("Missing AZURE_OPENAI_ENDPOINT. Add it to your .env file to route requests to Azure OpenAI.");
938
+ }
939
+ if (!AZURE_OPENAI_API_KEY) {
940
+ throw new Error("Missing AZURE_OPENAI_API_KEY. Add it to your .env file to route requests to Azure OpenAI.");
941
+ }
942
+ return {
943
+ endpoint: AZURE_OPENAI_ENDPOINT,
944
+ apiKey: AZURE_OPENAI_API_KEY,
945
+ };
946
+ };
947
+
948
+ const buildAzureDeploymentUrl = (deployment: string | undefined, suffix: string) => {
949
+ if (!deployment) {
950
+ throw new Error(\`Missing Azure OpenAI \${suffix.split("/")[0]} deployment name.\`);
951
+ }
952
+ const { endpoint } = requireAzureBaseConfig();
953
+ const normalized = suffix.replace(/^\\/+/, "");
954
+ return \`\${endpoint}/openai/deployments/\${deployment}/\${normalized}?api-version=\${AZURE_OPENAI_API_VERSION}\`;
955
+ };
956
+
957
+ const resolveAzureDeployment = (model: unknown, fallback: string | undefined, kind: "chat" | "completions" | "embeddings") => {
958
+ const explicit = typeof model === "string" ? model.replace(/^azure:/, "") : undefined;
959
+ if (explicit) return explicit;
960
+ if (kind === "embeddings") return AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT ?? fallback;
961
+ if (kind === "completions") return AZURE_OPENAI_COMPLETIONS_DEPLOYMENT ?? fallback;
962
+ return AZURE_OPENAI_CHAT_DEPLOYMENT ?? fallback;
963
+ };
964
+
965
+ const flattenGatewayContent = (content: unknown): string => {
966
+ if (typeof content === "string") return content;
967
+ if (Array.isArray(content)) {
968
+ return content
969
+ .map((part) => {
970
+ if (typeof part === "string") return part;
971
+ if (part && typeof part === "object" && "type" in part) {
972
+ const typed = part as { type?: string; text?: string; image_url?: { url?: string } };
973
+ if (typed.type === "text" && typeof typed.text === "string") return typed.text;
974
+ if (typed.type === "image_url" && typed.image_url?.url) return \`[Image: \${typed.image_url.url}]\`;
975
+ }
976
+ return JSON.stringify(part ?? {});
977
+ })
978
+ .join("\\n");
979
+ }
980
+ if (content && typeof content === "object") return JSON.stringify(content);
981
+ return "";
982
+ };
983
+
984
+ const toAnthropicMessages = (messages: Array<{ role: string; content: unknown }> = []) => {
985
+ const anthropicMessages: Array<{ role: "user" | "assistant"; content: Array<{ type: "text"; text: string }> }> = [];
986
+ let systemPrompt = "";
987
+
988
+ for (const message of messages) {
989
+ if (!message) continue;
990
+ const text = flattenGatewayContent(message.content);
991
+ if (message.role === "system") {
992
+ systemPrompt = systemPrompt ? \`\${systemPrompt}\\n\\n\${text}\` : text;
993
+ continue;
994
+ }
995
+ const role = message.role === "assistant" ? "assistant" : "user";
996
+ anthropicMessages.push({
997
+ role,
998
+ content: [{ type: "text", text }],
999
+ });
1000
+ }
1001
+
1002
+ return { messages: anthropicMessages, system: systemPrompt || undefined };
1003
+ };
1004
+
1005
+ const convertAnthropicResponseToGateway = (responseBody: any, modelName: string) => {
1006
+ if (!responseBody) {
1007
+ return {
1008
+ id: \`anthropic-\${Date.now()}\`,
1009
+ object: "chat.completion",
1010
+ created: Math.floor(Date.now() / 1000),
1011
+ model: modelName.startsWith("anthropic:") ? modelName : \`anthropic:\${modelName}\`,
1012
+ choices: [],
1013
+ };
1014
+ }
1015
+
1016
+ const textContent = Array.isArray(responseBody.content)
1017
+ ? responseBody.content
1018
+ .filter((item: any) => item && item.type === "text" && typeof item.text === "string")
1019
+ .map((item: any) => item.text)
1020
+ .join("\\n")
1021
+ : typeof responseBody.content === "string"
1022
+ ? responseBody.content
1023
+ : "";
1024
+
1025
+ const promptTokens = responseBody.usage?.input_tokens ?? 0;
1026
+ const completionTokens = responseBody.usage?.output_tokens ?? 0;
1027
+
1028
+ return {
1029
+ id: responseBody.id ?? \`anthropic-\${Date.now()}\`,
1030
+ object: "chat.completion",
1031
+ created: Math.floor(Date.now() / 1000),
1032
+ model: modelName.startsWith("anthropic:") ? modelName : \`anthropic:\${modelName}\`,
1033
+ choices: [
1034
+ {
1035
+ index: 0,
1036
+ message: {
1037
+ role: responseBody.role ?? "assistant",
1038
+ content: textContent,
1039
+ },
1040
+ finish_reason: responseBody.stop_reason ?? responseBody.stop_sequence ?? null,
1041
+ },
1042
+ ],
1043
+ usage: responseBody.usage
1044
+ ? {
1045
+ prompt_tokens: promptTokens,
1046
+ completion_tokens: completionTokens,
1047
+ total_tokens: promptTokens + completionTokens,
1048
+ }
1049
+ : undefined,
1050
+ };
1051
+ };
1052
+
1053
+ const passthroughResponse = (upstream: Response) => {
1054
+ const headers = new Headers(upstream.headers);
1055
+ return new Response(upstream.body, {
1056
+ status: upstream.status,
1057
+ statusText: upstream.statusText,
1058
+ headers,
1059
+ });
1060
+ };
1061
+
1062
+ const jsonResponse = async (upstream: Response) => {
1063
+ const data = await upstream.json().catch(async () => ({ raw: await upstream.text() }));
1064
+ return NextResponse.json(data, { status: upstream.status });
1065
+ };
1066
+
1067
+ const errorResponse = (status: number, error: unknown) =>
1068
+ NextResponse.json(
1069
+ {
1070
+ error: error instanceof Error ? error.message : String(error ?? "Unknown error"),
1071
+ },
1072
+ { status }
1073
+ );
1074
+
1075
+ export async function POST(request: NextRequest) {
1076
+ const body = (await request.json()) as GatewayChatBody;
1077
+ const provider = normalizeProvider(body.provider ?? DEFAULT_PROVIDER);
1078
+ const stream = body.stream !== false;
1079
+
1080
+ try {
1081
+ switch (provider) {
1082
+ case "openai": {
1083
+ const openaiKey = requireOpenAIKey();
1084
+ const { provider: _provider, ...cleanBody } = body;
1085
+ const requestBody = {
1086
+ ...cleanBody,
1087
+ stream,
1088
+ model: stripPrefix(body.model ?? DEFAULT_MODEL, "openai", "gpt-4o"),
1089
+ };
1090
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
1091
+ method: "POST",
1092
+ headers: {
1093
+ "Content-Type": "application/json",
1094
+ Authorization: \`Bearer \${openaiKey}\`,
1095
+ },
1096
+ body: JSON.stringify(requestBody),
1097
+ });
1098
+ if (!response.ok) {
1099
+ const details = await response.text();
1100
+ return NextResponse.json({ error: \`OpenAI chat failed: \${response.status}\`, details }, { status: response.status });
1101
+ }
1102
+ return stream ? passthroughResponse(response) : jsonResponse(response);
1103
+ }
1104
+
1105
+ case "xai": {
1106
+ const xaiKey = requireXAIKey();
1107
+ const { provider: _provider, ...cleanBody } = body;
1108
+ const requestBody = {
1109
+ ...cleanBody,
1110
+ stream,
1111
+ model: stripPrefix(body.model ?? DEFAULT_MODEL, "xai", "grok-2-latest"),
1112
+ };
1113
+ const response = await fetch(XAI_BASE_URL + "/chat/completions", {
1114
+ method: "POST",
1115
+ headers: {
1116
+ "Content-Type": "application/json",
1117
+ Authorization: "Bearer " + xaiKey,
1118
+ },
1119
+ body: JSON.stringify(requestBody),
1120
+ });
1121
+ if (!response.ok) {
1122
+ const details = await response.text();
1123
+ return NextResponse.json({ error: "xAI chat failed: " + response.status, details }, { status: response.status });
1124
+ }
1125
+ return stream ? passthroughResponse(response) : jsonResponse(response);
1126
+ }
1127
+
1128
+ case "anthropic": {
1129
+ const anthropicKey = requireAnthropicKey();
1130
+ const requestedModel = stripPrefix(body.model ?? DEFAULT_MODEL, "anthropic", "claude-3-5-haiku-latest");
1131
+ const stopSequences = Array.isArray(body.stop)
1132
+ ? body.stop
1133
+ : Array.isArray(body.stop_sequences)
1134
+ ? body.stop_sequences
1135
+ : body.stop
1136
+ ? [body.stop]
1137
+ : undefined;
1138
+ const { messages, system } = toAnthropicMessages(Array.isArray(body.messages) ? body.messages : []);
1139
+ const fallbackText = typeof body.prompt === "string" && body.prompt.trim().length > 0
1140
+ ? body.prompt
1141
+ : "Hello from Bandit quickstart gateway";
1142
+
1143
+ const requestBody: Record<string, unknown> = {
1144
+ model: requestedModel,
1145
+ messages: messages.length > 0
1146
+ ? messages
1147
+ : [
1148
+ {
1149
+ role: "user",
1150
+ content: [{ type: "text", text: fallbackText }],
1151
+ },
1152
+ ],
1153
+ stream,
1154
+ max_tokens: typeof body.max_tokens === "number" && body.max_tokens > 0 ? body.max_tokens : ANTHROPIC_MAX_TOKENS,
1155
+ };
1156
+
1157
+ if (system) requestBody.system = system;
1158
+ if (typeof body.temperature === "number") requestBody.temperature = body.temperature;
1159
+ if (typeof body.top_p === "number") requestBody.top_p = body.top_p;
1160
+ if (typeof body.top_k === "number") requestBody.top_k = body.top_k;
1161
+ if (stopSequences) requestBody.stop_sequences = stopSequences;
1162
+ if (body.metadata) requestBody.metadata = body.metadata;
1163
+ if (body.tools) requestBody.tools = body.tools;
1164
+ if (body.tool_choice) requestBody.tool_choice = body.tool_choice;
1165
+ if (body.thinking) requestBody.thinking = body.thinking;
1166
+
1167
+ const response = await fetch(\`\${ANTHROPIC_BASE_URL}/v1/messages\`, {
1168
+ method: "POST",
1169
+ headers: {
1170
+ "Content-Type": "application/json",
1171
+ "x-api-key": anthropicKey,
1172
+ "anthropic-version": ANTHROPIC_API_VERSION,
1173
+ },
1174
+ body: JSON.stringify(requestBody),
1175
+ });
1176
+
1177
+ if (!response.ok) {
1178
+ const details = await response.text();
1179
+ return NextResponse.json({ error: \`Anthropic chat failed: \${response.status}\`, details }, { status: response.status });
1180
+ }
1181
+
1182
+ if (stream) {
1183
+ return passthroughResponse(response);
1184
+ }
1185
+
1186
+ const data = await response.json();
1187
+ const normalized = convertAnthropicResponseToGateway(data, requestedModel);
1188
+ return NextResponse.json(normalized);
1189
+ }
1190
+
1191
+ case "azure": {
1192
+ const { apiKey } = requireAzureBaseConfig();
1193
+ const deployment = resolveAzureDeployment(body.model, AZURE_OPENAI_CHAT_DEPLOYMENT, "chat");
1194
+ const { provider: _provider, model: _model, ...cleanBody } = body;
1195
+ const requestBody = {
1196
+ ...cleanBody,
1197
+ stream,
1198
+ };
1199
+
1200
+ const response = await fetch(buildAzureDeploymentUrl(deployment, "chat/completions"), {
1201
+ method: "POST",
1202
+ headers: {
1203
+ "Content-Type": "application/json",
1204
+ "api-key": apiKey,
1205
+ },
1206
+ body: JSON.stringify(requestBody),
1207
+ });
1208
+
1209
+ if (!response.ok) {
1210
+ const details = await response.text();
1211
+ return NextResponse.json({ error: \`Azure OpenAI chat failed: \${response.status}\`, details }, { status: response.status });
1212
+ }
1213
+
1214
+ return stream ? passthroughResponse(response) : jsonResponse(response);
1215
+ }
1216
+
1217
+ case "ollama": {
1218
+ const { provider: _provider, ...cleanBody } = body;
1219
+ const requestBody = {
1220
+ ...cleanBody,
1221
+ stream,
1222
+ model: stripPrefix(body.model ?? DEFAULT_MODEL, "ollama", "llama3.1"),
1223
+ };
1224
+
1225
+ const response = await fetch(\`\${OLLAMA_URL}/api/chat\`, {
1226
+ method: "POST",
1227
+ headers: {
1228
+ "Content-Type": "application/json",
1229
+ },
1230
+ body: JSON.stringify(requestBody),
1231
+ });
1232
+
1233
+ if (!response.ok) {
1234
+ const details = await response.text();
1235
+ return NextResponse.json({ error: \`Ollama chat failed: \${response.status}\`, details }, { status: response.status });
1236
+ }
1237
+
1238
+ return stream ? passthroughResponse(response) : jsonResponse(response);
1239
+ }
1240
+
1241
+ default:
1242
+ return errorResponse(400, \`Unsupported provider: \${provider}\`);
1243
+ }
1244
+ } catch (error) {
1245
+ const message = error instanceof Error ? error.message : String(error);
1246
+ const status = message.startsWith("Missing") ? 400 : 500;
1247
+ return errorResponse(status, error);
1248
+ }
1249
+ }
1250
+
1251
+ `;
1252
+ var NEXT_HEALTH_ROUTE_TEMPLATE = `import { NextResponse } from "next/server";
1253
+
1254
+ export const dynamic = "force-dynamic";
1255
+
1256
+ const QUICKSTART_VERSION = "0.1.0";
1257
+ const OLLAMA_URL = (process.env.OLLAMA_URL ?? "http://localhost:11434").replace(/\\/$/, "");
1258
+ const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
1259
+ const AZURE_OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT ? process.env.AZURE_OPENAI_ENDPOINT.replace(/\\/$/, "") : undefined;
1260
+ const AZURE_OPENAI_API_KEY = process.env.AZURE_OPENAI_API_KEY;
1261
+ const AZURE_OPENAI_API_VERSION = process.env.AZURE_OPENAI_API_VERSION ?? "2024-08-01-preview";
1262
+ const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
1263
+ const ANTHROPIC_BASE_URL = (process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com").replace(/\\/$/, "");
1264
+ const ANTHROPIC_API_VERSION = process.env.ANTHROPIC_API_VERSION ?? "2023-06-01";
1265
+ const XAI_API_KEY = process.env.XAI_API_KEY;
1266
+ const XAI_BASE_URL = (process.env.XAI_BASE_URL ?? "https://api.x.ai/v1").replace(/\\/$/, "");
1267
+
1268
+ const isAzureConfigured = () => Boolean(AZURE_OPENAI_ENDPOINT && AZURE_OPENAI_API_KEY);
1269
+
1270
+ const buildAzurePath = (path: string) => {
1271
+ const normalized = path.replace(/^\\/+/, "");
1272
+ if (!AZURE_OPENAI_ENDPOINT) {
1273
+ throw new Error("Missing AZURE_OPENAI_ENDPOINT. Add it to your .env file to route requests to Azure OpenAI.");
1274
+ }
1275
+ return \`\${AZURE_OPENAI_ENDPOINT}/openai/\${normalized}?api-version=\${AZURE_OPENAI_API_VERSION}\`;
1276
+ };
1277
+
1278
+ export async function GET() {
1279
+ const providers: Array<Record<string, unknown>> = [];
1280
+
1281
+ // OpenAI
1282
+ try {
1283
+ if (OPENAI_API_KEY) {
1284
+ const response = await fetch("https://api.openai.com/v1/models", {
1285
+ headers: { Authorization: \`Bearer \${OPENAI_API_KEY}\` },
1286
+ });
1287
+ providers.push({
1288
+ name: "openai",
1289
+ status: response.ok ? "healthy" : "unhealthy",
1290
+ provider: "openai",
1291
+ });
1292
+ } else {
1293
+ providers.push({
1294
+ name: "openai",
1295
+ status: "unconfigured",
1296
+ provider: "openai",
1297
+ error: "API key not configured",
1298
+ });
1299
+ }
1300
+ } catch (error) {
1301
+ providers.push({
1302
+ name: "openai",
1303
+ status: "unhealthy",
1304
+ provider: "openai",
1305
+ error: error instanceof Error ? error.message : String(error),
1306
+ });
1307
+ }
1308
+
1309
+ // Azure
1310
+ if (AZURE_OPENAI_ENDPOINT || AZURE_OPENAI_API_KEY) {
1311
+ if (!isAzureConfigured()) {
1312
+ providers.push({
1313
+ name: "azure",
1314
+ status: "unconfigured",
1315
+ provider: "azure",
1316
+ error: "Endpoint or API key not configured",
1317
+ endpoint: AZURE_OPENAI_ENDPOINT,
1318
+ });
1319
+ } else {
1320
+ try {
1321
+ const response = await fetch(buildAzurePath("deployments"), {
1322
+ headers: { "api-key": AZURE_OPENAI_API_KEY ?? "" },
1323
+ });
1324
+ providers.push({
1325
+ name: "azure",
1326
+ status: response.ok ? "healthy" : "unhealthy",
1327
+ provider: "azure",
1328
+ endpoint: AZURE_OPENAI_ENDPOINT,
1329
+ });
1330
+ } catch (error) {
1331
+ providers.push({
1332
+ name: "azure",
1333
+ status: "unhealthy",
1334
+ provider: "azure",
1335
+ endpoint: AZURE_OPENAI_ENDPOINT,
1336
+ error: error instanceof Error ? error.message : String(error),
1337
+ });
1338
+ }
1339
+ }
1340
+ } else {
1341
+ providers.push({
1342
+ name: "azure",
1343
+ status: "unconfigured",
1344
+ provider: "azure",
1345
+ error: "Endpoint or API key not configured",
1346
+ });
1347
+ }
1348
+
1349
+ // Anthropic
1350
+ if (ANTHROPIC_API_KEY) {
1351
+ try {
1352
+ const response = await fetch(\`\${ANTHROPIC_BASE_URL}/v1/models\`, {
1353
+ headers: {
1354
+ "x-api-key": ANTHROPIC_API_KEY,
1355
+ "anthropic-version": ANTHROPIC_API_VERSION,
1356
+ },
1357
+ });
1358
+ providers.push({
1359
+ name: "anthropic",
1360
+ status: response.ok ? "healthy" : "unhealthy",
1361
+ provider: "anthropic",
1362
+ endpoint: ANTHROPIC_BASE_URL,
1363
+ });
1364
+ } catch (error) {
1365
+ providers.push({
1366
+ name: "anthropic",
1367
+ status: "unhealthy",
1368
+ provider: "anthropic",
1369
+ endpoint: ANTHROPIC_BASE_URL,
1370
+ error: error instanceof Error ? error.message : String(error),
1371
+ });
1372
+ }
1373
+ } else {
1374
+ providers.push({
1375
+ name: "anthropic",
1376
+ status: "unconfigured",
1377
+ provider: "anthropic",
1378
+ error: "API key not configured",
1379
+ });
1380
+ }
1381
+
1382
+ // xAI
1383
+ if (XAI_API_KEY) {
1384
+ try {
1385
+ const response = await fetch(XAI_BASE_URL + "/models", {
1386
+ headers: { Authorization: "Bearer " + XAI_API_KEY },
1387
+ });
1388
+ providers.push({
1389
+ name: "xai",
1390
+ status: response.ok ? "healthy" : "unhealthy",
1391
+ provider: "xai",
1392
+ endpoint: XAI_BASE_URL,
1393
+ });
1394
+ } catch (error) {
1395
+ providers.push({
1396
+ name: "xai",
1397
+ status: "unhealthy",
1398
+ provider: "xai",
1399
+ endpoint: XAI_BASE_URL,
1400
+ error: error instanceof Error ? error.message : String(error),
1401
+ });
1402
+ }
1403
+ } else {
1404
+ providers.push({
1405
+ name: "xai",
1406
+ status: "unconfigured",
1407
+ provider: "xai",
1408
+ error: "API key not configured",
1409
+ endpoint: XAI_BASE_URL,
1410
+ });
1411
+ }
1412
+
1413
+ // Ollama
1414
+ try {
1415
+ const response = await fetch(\`\${OLLAMA_URL}/api/tags\`);
1416
+ providers.push({
1417
+ name: "ollama",
1418
+ status: response.ok ? "healthy" : "unhealthy",
1419
+ provider: "ollama",
1420
+ url: OLLAMA_URL,
1421
+ });
1422
+ } catch (error) {
1423
+ providers.push({
1424
+ name: "ollama",
1425
+ status: "offline",
1426
+ provider: "ollama",
1427
+ url: OLLAMA_URL,
1428
+ error: error instanceof Error ? error.message : String(error),
1429
+ });
1430
+ }
1431
+
1432
+ const overallHealthy = providers.some((provider) => provider.status === "healthy");
1433
+
1434
+ return NextResponse.json({
1435
+ status: overallHealthy ? "healthy" : "unhealthy",
1436
+ version: QUICKSTART_VERSION,
1437
+ uptime: Math.round(process.uptime()),
1438
+ providers,
1439
+ });
1440
+ }
1441
+
819
1442
  `;
820
- const withResponse = template.replace(/__RESPONSE_STATUS__/g, responseStatusExpr);
821
- const withGatewayError = withResponse.replace(/__GATEWAY_ERROR__/g, gatewayErrorExpr);
822
- return ensureTrailingNewline(normalizeLineEndings(withGatewayError));
1443
+ var NEXT_MODELS_ROUTE_TEMPLATE = `import { NextResponse } from "next/server";
1444
+
1445
+ export const dynamic = "force-dynamic";
1446
+
1447
+ const BASE_GATEWAY_MODELS = __GATEWAY_MODELS__;
1448
+
1449
+ export function toGatewayModels() {
1450
+ return BASE_GATEWAY_MODELS.map((model) => ({
1451
+ ...model,
1452
+ created: Date.now(),
1453
+ modified_at: new Date().toISOString(),
1454
+ size: 0,
1455
+ digest: "",
1456
+ details: {
1457
+ format: "chat",
1458
+ family: model.provider,
1459
+ families: [model.provider],
1460
+ parameter_size: "",
1461
+ quantization_level: "",
1462
+ },
1463
+ }));
1464
+ }
1465
+
1466
+ export async function GET() {
1467
+ return NextResponse.json({ models: toGatewayModels() });
1468
+ }
1469
+
1470
+ `;
1471
+ var NEXT_GATEWAY_README_TEMPLATE = `# Next.js Gateway API
1472
+
1473
+ This directory contains a minimal Next.js App Router implementation of the Bandit gateway API. It mirrors the Express gateway in
1474
+ \`server/gateway.js\` but is ready to drop into a Next.js project.
1475
+
1476
+ ## Routes
1477
+
1478
+ - \`app/api/health/route.ts\` \u2013 provider health and availability checks
1479
+ - \`app/api/chat/completions/route.ts\` \u2013 provider-aware chat completions endpoint (OpenAI, Azure OpenAI, Anthropic, xAI, Ollama)
1480
+ - \`app/api/models/route.ts\` \u2013 exposes the scaffolded gateway model metadata used by the frontend
1481
+
1482
+ ## Usage
1483
+
1484
+ 1. Copy the contents of \`server/next-app/\` into the \`app/\` directory of a Next.js project.
1485
+ 2. Ensure the environment variables listed in \`.env.example\` are available to the Next.js runtime. At minimum you will want the
1486
+ provider API keys you plan to use (OpenAI, Azure OpenAI, Anthropic, xAI, or Ollama).
1487
+ 3. Start Next.js with \`npm run dev\` (or your project\u2019s equivalent). The routes are server-only (\`export const dynamic = "force-dynamic"\`)
1488
+ and can coexist with any frontend pages.
1489
+
1490
+ The generated routes favour clarity over cleverness so you can extend them with custom auth, logging, and provider routing logic.
1491
+ `;
1492
+ var buildNextChatRoute = (ctx) => {
1493
+ const fallbackModel = ctx.fallbackModelId ? `"${ctx.fallbackModelId}"` : "undefined";
1494
+ return ensureTrailingNewline(
1495
+ normalizeLineEndings(
1496
+ NEXT_CHAT_ROUTE_TEMPLATE.replace(/__DEFAULT_PROVIDER__/g, ctx.defaultProvider).replace(/__DEFAULT_MODEL__/g, ctx.defaultModelId).replace(/__FALLBACK_MODEL__/g, fallbackModel)
1497
+ )
1498
+ );
823
1499
  };
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
- });
1500
+ var buildNextHealthRoute = () => ensureTrailingNewline(normalizeLineEndings(NEXT_HEALTH_ROUTE_TEMPLATE));
1501
+ var buildNextModelsRoute = (ctx) => {
1502
+ const modelsDefinition = JSON.stringify(ctx.gatewayModels, null, 2);
1503
+ return ensureTrailingNewline(
1504
+ normalizeLineEndings(
1505
+ NEXT_MODELS_ROUTE_TEMPLATE.replace("__GATEWAY_MODELS__", modelsDefinition)
1506
+ )
1507
+ );
1508
+ };
1509
+ var buildNextGatewayReadme = () => ensureTrailingNewline(normalizeLineEndings(NEXT_GATEWAY_README_TEMPLATE));
833
1510
  var buildGatewayServer = (ctx) => {
834
1511
  const modelsDefinition = JSON.stringify(ctx.gatewayModels, null, 2);
835
1512
  const gatewaySource = `import express from "express";
@@ -859,6 +1536,8 @@ const ANTHROPIC_API_VERSION = process.env.ANTHROPIC_API_VERSION ?? "2023-06-01";
859
1536
  const ANTHROPIC_MAX_TOKENS = Number.isFinite(Number(process.env.ANTHROPIC_MAX_TOKENS))
860
1537
  ? Number(process.env.ANTHROPIC_MAX_TOKENS)
861
1538
  : 1024;
1539
+ const XAI_API_KEY = process.env.XAI_API_KEY;
1540
+ const XAI_BASE_URL = (process.env.XAI_BASE_URL ?? "https://api.x.ai/v1").replace(/\\/$/, "");
862
1541
 
863
1542
  const toGatewayModels = () =>
864
1543
  BASE_GATEWAY_MODELS.map((model) => ({
@@ -1065,6 +1744,14 @@ const requireOpenAIKey = () => {
1065
1744
  return key;
1066
1745
  };
1067
1746
 
1747
+ const requireXAIKey = () => {
1748
+ const key = XAI_API_KEY;
1749
+ if (!key) {
1750
+ throw new Error("Missing XAI_API_KEY. Add it to your .env file to route requests to xAI.");
1751
+ }
1752
+ return key;
1753
+ };
1754
+
1068
1755
  // Utility function to handle streaming responses
1069
1756
  const handleStreamingResponse = async (upstreamResponse, res) => {
1070
1757
  res.setHeader('Content-Type', 'text/event-stream');
@@ -1093,6 +1780,93 @@ const handleStreamingResponse = async (upstreamResponse, res) => {
1093
1780
  }
1094
1781
  };
1095
1782
 
1783
+ const relayAnthropicStream = async (upstreamResponse, res) => {
1784
+ res.setHeader('Content-Type', 'text/event-stream');
1785
+ res.setHeader('Cache-Control', 'no-cache');
1786
+ res.setHeader('Connection', 'keep-alive');
1787
+ res.setHeader('Access-Control-Allow-Origin', '*');
1788
+
1789
+ const reader = upstreamResponse.body?.getReader();
1790
+ if (!reader) {
1791
+ const fallback = await upstreamResponse.text();
1792
+ res.write("data: " + JSON.stringify({ choices: [{ delta: { content: fallback } }] }) + "\\n\\n");
1793
+ res.write("data: [DONE]\\n\\n");
1794
+ return res.end();
1795
+ }
1796
+
1797
+ const decoder = new TextDecoder();
1798
+ let buffer = '';
1799
+
1800
+ const sendChunk = (payload) => {
1801
+ res.write("data: " + JSON.stringify(payload) + "\\n\\n");
1802
+ };
1803
+
1804
+ try {
1805
+ while (true) {
1806
+ const { value, done } = await reader.read();
1807
+ if (done) break;
1808
+ buffer += decoder.decode(value, { stream: true });
1809
+
1810
+ let delimiterIndex;
1811
+ while ((delimiterIndex = buffer.indexOf('\\n\\n')) >= 0) {
1812
+ const rawEvent = buffer.slice(0, delimiterIndex).trim();
1813
+ buffer = buffer.slice(delimiterIndex + 2);
1814
+ if (!rawEvent) continue;
1815
+
1816
+ const lines = rawEvent.split('\\n');
1817
+ const eventLine = lines.find((line) => line.startsWith('event:')) ?? '';
1818
+ const dataLine = lines.find((line) => line.startsWith('data:')) ?? '';
1819
+ const event = eventLine.replace('event:', '').trim();
1820
+ const trimmedData = dataLine.replace('data:', '').trim();
1821
+
1822
+ if (!trimmedData) {
1823
+ continue;
1824
+ }
1825
+
1826
+ let parsed;
1827
+ try {
1828
+ parsed = JSON.parse(trimmedData);
1829
+ } catch (error) {
1830
+ console.error('Anthropic stream parse error', error, { rawEvent });
1831
+ continue;
1832
+ }
1833
+
1834
+ if (event === 'content_block_delta') {
1835
+ const textChunk = parsed?.delta?.text ?? '';
1836
+ if (textChunk) {
1837
+ sendChunk({
1838
+ choices: [
1839
+ {
1840
+ delta: {
1841
+ content: textChunk,
1842
+ },
1843
+ },
1844
+ ],
1845
+ });
1846
+ }
1847
+ } else if (event === 'message_stop') {
1848
+ sendChunk({
1849
+ choices: [
1850
+ {
1851
+ delta: {},
1852
+ finish_reason: 'stop',
1853
+ },
1854
+ ],
1855
+ });
1856
+ }
1857
+ }
1858
+ }
1859
+ } catch (error) {
1860
+ console.error('Anthropic streaming relay error', error);
1861
+ sendChunk({
1862
+ error: error instanceof Error ? error.message : String(error),
1863
+ });
1864
+ } finally {
1865
+ res.write("data: [DONE]\\n\\n");
1866
+ res.end();
1867
+ }
1868
+ };
1869
+
1096
1870
  // ============================================================================
1097
1871
  // GENERAL HEALTH & MODELS
1098
1872
  // ============================================================================
@@ -1202,6 +1976,37 @@ app.get("/api/health", async (_req, res) => {
1202
1976
  });
1203
1977
  }
1204
1978
 
1979
+ // Check xAI
1980
+ if (XAI_API_KEY) {
1981
+ try {
1982
+ const response = await fetch(XAI_BASE_URL + "/models", {
1983
+ headers: { "Authorization": "Bearer " + XAI_API_KEY }
1984
+ });
1985
+ providers.push({
1986
+ name: "xai",
1987
+ status: response.ok ? "healthy" : "unhealthy",
1988
+ provider: "xai",
1989
+ endpoint: XAI_BASE_URL
1990
+ });
1991
+ } catch (error) {
1992
+ providers.push({
1993
+ name: "xai",
1994
+ status: "unhealthy",
1995
+ provider: "xai",
1996
+ error: error instanceof Error ? error.message : String(error),
1997
+ endpoint: XAI_BASE_URL
1998
+ });
1999
+ }
2000
+ } else {
2001
+ providers.push({
2002
+ name: "xai",
2003
+ status: "unconfigured",
2004
+ provider: "xai",
2005
+ error: "API key not configured",
2006
+ endpoint: XAI_BASE_URL
2007
+ });
2008
+ }
2009
+
1205
2010
  // Check Ollama
1206
2011
  try {
1207
2012
  console.log(\`Checking Ollama health at: \${OLLAMA_BASE_URL}/api/tags\`);
@@ -1277,7 +2082,7 @@ app.post("/api/anthropic/chat/completions", async (req, res) => {
1277
2082
  const requestedModel =
1278
2083
  stripAnthropicModelPrefix(rawBody.model) ??
1279
2084
  stripAnthropicModelPrefix("${ctx.defaultModelId}") ??
1280
- "claude-3-5-sonnet-latest";
2085
+ "claude-3-5-haiku-latest";
1281
2086
 
1282
2087
  const stopSequences = Array.isArray(rawBody.stop)
1283
2088
  ? rawBody.stop
@@ -1360,7 +2165,7 @@ app.post("/api/anthropic/chat/completions", async (req, res) => {
1360
2165
  }
1361
2166
 
1362
2167
  if (isStreaming) {
1363
- await handleStreamingResponse(response, res);
2168
+ await relayAnthropicStream(response, res);
1364
2169
  } else {
1365
2170
  const data = await response.json();
1366
2171
  const normalized = convertAnthropicResponseToGateway(data, requestedModel);
@@ -1455,7 +2260,7 @@ app.post("/api/anthropic/completions", async (req, res) => {
1455
2260
  }
1456
2261
 
1457
2262
  if (isStreaming) {
1458
- await handleStreamingResponse(response, res);
2263
+ await relayAnthropicStream(response, res);
1459
2264
  } else {
1460
2265
  const data = await response.json();
1461
2266
  const formatted = convertAnthropicResponseToGenerate(data, requestedModel);
@@ -1728,6 +2533,201 @@ app.post("/api/azure/embed", async (req, res) => {
1728
2533
  }
1729
2534
  });
1730
2535
 
2536
+ // ============================================================================
2537
+ // XAI ROUTES
2538
+ // ============================================================================
2539
+
2540
+ // xAI Health Check
2541
+ app.get("/api/xai/health", async (_req, res) => {
2542
+ try {
2543
+ const xaiKey = requireXAIKey();
2544
+ const response = await fetch(XAI_BASE_URL + "/models", {
2545
+ headers: { "Authorization": "Bearer " + xaiKey }
2546
+ });
2547
+ const isHealthy = response.ok;
2548
+ res.json({
2549
+ status: isHealthy ? "healthy" : "unhealthy",
2550
+ xai_status: isHealthy,
2551
+ provider: "xai"
2552
+ });
2553
+ } catch (error) {
2554
+ res.status(503).json({
2555
+ status: "unhealthy",
2556
+ xai_status: false,
2557
+ error: error instanceof Error ? error.message : String(error),
2558
+ provider: "xai"
2559
+ });
2560
+ }
2561
+ });
2562
+
2563
+ // xAI Chat Completions
2564
+ app.post("/api/xai/chat/completions", async (req, res) => {
2565
+ try {
2566
+ const xaiKey = requireXAIKey();
2567
+ const isStreaming = req.body?.stream === true;
2568
+ const { provider, ...cleanBody } = req.body ?? {};
2569
+ const requestBody = {
2570
+ ...cleanBody,
2571
+ model: req.body?.model?.replace(/^xai:/, "") || "grok-2-latest"
2572
+ };
2573
+
2574
+ const response = await fetch(XAI_BASE_URL + "/chat/completions", {
2575
+ method: "POST",
2576
+ headers: {
2577
+ "Content-Type": "application/json",
2578
+ "Authorization": "Bearer " + xaiKey
2579
+ },
2580
+ body: JSON.stringify(requestBody)
2581
+ });
2582
+
2583
+ if (!response.ok) {
2584
+ const errorText = await response.text();
2585
+ return res.status(response.status).json({
2586
+ error: "xAI chat failed: " + response.status,
2587
+ details: errorText
2588
+ });
2589
+ }
2590
+
2591
+ if (isStreaming) {
2592
+ await handleStreamingResponse(response, res);
2593
+ } else {
2594
+ const text = await response.text();
2595
+ res.setHeader('Content-Type', 'application/json');
2596
+ res.send(text);
2597
+ }
2598
+ } catch (error) {
2599
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
2600
+ }
2601
+ });
2602
+
2603
+ app.post("/api/xai/chat", async (req, res) => {
2604
+ req.url = "/api/xai/chat/completions";
2605
+ return app._router.handle(req, res);
2606
+ });
2607
+
2608
+ // xAI Completions
2609
+ app.post("/api/xai/completions", async (req, res) => {
2610
+ try {
2611
+ const xaiKey = requireXAIKey();
2612
+ const isStreaming = req.body?.stream === true;
2613
+ const { provider, ...cleanBody } = req.body ?? {};
2614
+ const requestBody = {
2615
+ ...cleanBody,
2616
+ model: req.body?.model?.replace(/^xai:/, "") || "grok-2-mini"
2617
+ };
2618
+
2619
+ const response = await fetch(XAI_BASE_URL + "/completions", {
2620
+ method: "POST",
2621
+ headers: {
2622
+ "Content-Type": "application/json",
2623
+ "Authorization": "Bearer " + xaiKey
2624
+ },
2625
+ body: JSON.stringify(requestBody)
2626
+ });
2627
+
2628
+ if (!response.ok) {
2629
+ const errorText = await response.text();
2630
+ return res.status(response.status).json({
2631
+ error: "xAI completions failed: " + response.status,
2632
+ details: errorText
2633
+ });
2634
+ }
2635
+
2636
+ if (isStreaming) {
2637
+ await handleStreamingResponse(response, res);
2638
+ } else {
2639
+ const text = await response.text();
2640
+ res.setHeader('Content-Type', 'application/json');
2641
+ res.send(text);
2642
+ }
2643
+ } catch (error) {
2644
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
2645
+ }
2646
+ });
2647
+
2648
+ // xAI Generate
2649
+ app.post("/api/xai/generate", async (req, res) => {
2650
+ try {
2651
+ const xaiKey = requireXAIKey();
2652
+ const prompt = req.body?.prompt || "";
2653
+ const model = req.body?.model?.replace(/^xai:/, "") || "grok-2-latest";
2654
+ const isStreaming = req.body?.stream === true;
2655
+
2656
+ const chatBody = {
2657
+ model,
2658
+ messages: [
2659
+ { role: "user", content: prompt }
2660
+ ],
2661
+ stream: isStreaming,
2662
+ max_tokens: req.body?.max_tokens || 150,
2663
+ temperature: req.body?.temperature ?? 0.7
2664
+ };
2665
+
2666
+ const response = await fetch(XAI_BASE_URL + "/chat/completions", {
2667
+ method: "POST",
2668
+ headers: {
2669
+ "Content-Type": "application/json",
2670
+ "Authorization": "Bearer " + xaiKey
2671
+ },
2672
+ body: JSON.stringify(chatBody)
2673
+ });
2674
+
2675
+ if (!response.ok) {
2676
+ const errorText = await response.text();
2677
+ return res.status(response.status).json({
2678
+ error: "xAI generate failed: " + response.status,
2679
+ details: errorText
2680
+ });
2681
+ }
2682
+
2683
+ if (isStreaming) {
2684
+ await handleStreamingResponse(response, res);
2685
+ } else {
2686
+ const data = await response.json();
2687
+ const generateResponse = {
2688
+ model,
2689
+ created_at: new Date().toISOString(),
2690
+ response: data.choices?.[0]?.message?.content || "",
2691
+ done: true,
2692
+ context: [],
2693
+ total_duration: 0,
2694
+ load_duration: 0,
2695
+ prompt_eval_count: data.usage?.prompt_tokens || 0,
2696
+ prompt_eval_duration: 0,
2697
+ eval_count: data.usage?.completion_tokens || 0,
2698
+ eval_duration: 0
2699
+ };
2700
+ res.json(generateResponse);
2701
+ }
2702
+ } catch (error) {
2703
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
2704
+ }
2705
+ });
2706
+
2707
+ // xAI Models
2708
+ app.get("/api/xai/models", async (_req, res) => {
2709
+ try {
2710
+ const xaiKey = requireXAIKey();
2711
+ const response = await fetch(XAI_BASE_URL + "/models", {
2712
+ headers: { "Authorization": "Bearer " + xaiKey }
2713
+ });
2714
+
2715
+ if (!response.ok) {
2716
+ const errorText = await response.text();
2717
+ return res.status(response.status).json({
2718
+ error: "xAI models failed: " + response.status,
2719
+ details: errorText
2720
+ });
2721
+ }
2722
+
2723
+ const text = await response.text();
2724
+ res.setHeader('Content-Type', 'application/json');
2725
+ res.send(text);
2726
+ } catch (error) {
2727
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
2728
+ }
2729
+ });
2730
+
1731
2731
  // ============================================================================
1732
2732
  // OPENAI ROUTES
1733
2733
  // ============================================================================
@@ -2250,11 +3250,12 @@ app.all("/api/anthropic/*", (_req, res) => {
2250
3250
  const port = Number(process.env.PORT ?? ${ctx.gatewayPort});
2251
3251
  app.listen(port, () => {
2252
3252
  console.log("\u26A1 Bandit quickstart gateway ready on http://localhost:" + port);
2253
- console.log("\u{1F4E1} Supported providers: OpenAI, Azure OpenAI, Anthropic, Ollama");
3253
+ console.log("\u{1F4E1} Supported providers: OpenAI, Azure OpenAI, Anthropic, XAI, Ollama");
2254
3254
  console.log("\u{1F517} Provider-specific routes:");
2255
3255
  console.log(" \u2022 /api/openai/* - OpenAI endpoints");
2256
3256
  console.log(" \u2022 /api/azure/* - Azure OpenAI endpoints");
2257
3257
  console.log(" \u2022 /api/anthropic/* - Anthropic endpoints");
3258
+ console.log(" \u2022 /api/xai/* - XAI endpoints");
2258
3259
  console.log(" \u2022 /api/ollama/* - Ollama endpoints");
2259
3260
  console.log(" \u2022 /api/health - Overall health check");
2260
3261
  });
@@ -2273,20 +3274,16 @@ dist
2273
3274
  `
2274
3275
  )
2275
3276
  );
2276
- var buildNpmrc = () => ensureTrailingNewline(
2277
- normalizeLineEndings(`registry=https://registry.npmjs.org/
2278
- `)
2279
- );
2280
3277
  var buildReadme = (ctx) => ensureTrailingNewline(
2281
3278
  normalizeLineEndings(
2282
3279
  `# ${ctx.projectTitle} \u2014 Bandit Quickstart
2283
3280
 
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.
3281
+ 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
3282
 
2286
3283
  ## \u{1F680} Next steps
2287
3284
  - \`npm install\`
2288
3285
  - \`cp .env.example .env\`
2289
- - Fill in your OpenAI, Azure OpenAI, or Anthropic credentials (or point \`OLLAMA_URL\` at your local server)
3286
+ - Fill in your OpenAI, Azure OpenAI, Anthropic, or xAI credentials (or point \`OLLAMA_URL\` at your local server)
2290
3287
  - \`npm run dev\`
2291
3288
 
2292
3289
  The command runs the gateway and the frontend together. Visit http://localhost:${ctx.frontendPort} to see the chat and modal in action.
@@ -2299,7 +3296,8 @@ The command runs the gateway and the frontend together. Visit http://localhost:$
2299
3296
  ## \u{1F4E6} What\u2019s inside
2300
3297
  - React + Vite 5 with Material UI theming
2301
3298
  - Bandit chat surface + modal wired via \`ChatProvider\`
2302
- - Express gateway proxying OpenAI, Azure OpenAI, Anthropic, or Ollama to keep API keys server-side
3299
+ - Express gateway proxying OpenAI, Azure OpenAI, Anthropic, XAI, or Ollama to keep API keys server-side
3300
+ - Next.js App Router gateway scaffold in 'server/next-app/' for projects that prefer Next
2303
3301
  - Friendly defaults you can evolve into your production stack
2304
3302
 
2305
3303
  Need more? Run \`npx @burtson-labs/bandit-engine create --help\` to explore additional options.
@@ -2315,7 +3313,7 @@ var createQuickstartProject = async (options) => {
2315
3313
  const projectTitle = toTitleCase(rawProjectName) || "Bandit Quickstart";
2316
3314
  await ensureWritableDirectory(resolvedDir, Boolean(options.force));
2317
3315
  const skipPrompts = Boolean(options.skipPrompts);
2318
- const provider = options.provider ? normalizeProvider(options.provider) : skipPrompts ? "openai" : await promptForProvider();
3316
+ const provider = options.provider ? normalizeProvider(options.provider) : skipPrompts ? "ollama" : await promptForProvider();
2319
3317
  const promptAnswers = skipPrompts ? {} : await promptForMissingData({
2320
3318
  brandingText: options.brandingText,
2321
3319
  provider
@@ -2390,6 +3388,9 @@ var normalizeProvider = (value) => {
2390
3388
  if (normalized === "anthropic") {
2391
3389
  return "anthropic";
2392
3390
  }
3391
+ if (normalized === "xai" || normalized === "grok") {
3392
+ return "xai";
3393
+ }
2393
3394
  return "openai";
2394
3395
  };
2395
3396
  var inferDefaultModelId = (provider) => {
@@ -2400,7 +3401,10 @@ var inferDefaultModelId = (provider) => {
2400
3401
  return "azure:gpt-4o";
2401
3402
  }
2402
3403
  if (provider === "anthropic") {
2403
- return "anthropic:claude-3-5-sonnet-latest";
3404
+ return "anthropic:claude-3-5-haiku-latest";
3405
+ }
3406
+ if (provider === "xai") {
3407
+ return "xai:grok-2-latest";
2404
3408
  }
2405
3409
  return "openai:gpt-4o-mini";
2406
3410
  };
@@ -2412,16 +3416,20 @@ var inferFallbackModelId = (provider, defaultId) => {
2412
3416
  return defaultId === "azure:gpt-4o-mini" ? "azure:gpt-4o" : "azure:gpt-4o-mini";
2413
3417
  }
2414
3418
  if (provider === "anthropic") {
2415
- return defaultId === "anthropic:claude-3-5-haiku-latest" ? "anthropic:claude-3-5-sonnet-latest" : "anthropic:claude-3-5-haiku-latest";
3419
+ return defaultId === "anthropic:claude-3-5-haiku-latest" ? "anthropic:claude-3-haiku-20240307" : "anthropic:claude-3-5-haiku-latest";
3420
+ }
3421
+ if (provider === "xai") {
3422
+ return defaultId === "xai:grok-2-mini" ? "xai:grok-2-latest" : "xai:grok-2-mini";
2416
3423
  }
2417
3424
  return defaultId === "openai:gpt-4.1-mini" ? "openai:gpt-4o-mini" : "openai:gpt-4.1-mini";
2418
3425
  };
2419
3426
  var promptForProvider = async () => {
2420
3427
  const providerOptions = [
2421
- { label: "OpenAI (default)", value: "openai" },
3428
+ { label: "Ollama (self-hosted) \u2014 default", value: "ollama" },
3429
+ { label: "OpenAI", value: "openai" },
2422
3430
  { label: "Azure OpenAI", value: "azure" },
2423
3431
  { label: "Anthropic", value: "anthropic" },
2424
- { label: "Ollama (self-hosted)", value: "ollama" }
3432
+ { label: "xAI (Grok)", value: "xai" }
2425
3433
  ];
2426
3434
  const messageLines = [
2427
3435
  "Which provider should we configure for the gateway?",
@@ -2447,7 +3455,7 @@ var promptForProvider = async () => {
2447
3455
  { onCancel }
2448
3456
  );
2449
3457
  const selectedIndex = typeof answers.providerIndex === "number" && answers.providerIndex >= 1 ? answers.providerIndex - 1 : 0;
2450
- return providerOptions[selectedIndex]?.value ?? "openai";
3458
+ return providerOptions[selectedIndex]?.value ?? "ollama";
2451
3459
  };
2452
3460
  var sanitizePort = (value) => {
2453
3461
  const port = Number(value);
@@ -2537,9 +3545,12 @@ var writeProject = async (inputs) => {
2537
3545
  "src/theme.ts": buildThemeTs(),
2538
3546
  "public/config.json": buildBrandingConfig(context),
2539
3547
  "server/gateway.js": buildGatewayServer(context),
3548
+ "server/next-app/app/api/chat/completions/route.ts": buildNextChatRoute(context),
3549
+ "server/next-app/app/api/health/route.ts": buildNextHealthRoute(),
3550
+ "server/next-app/app/api/models/route.ts": buildNextModelsRoute(context),
3551
+ "server/next-app/README.md": buildNextGatewayReadme(),
2540
3552
  ".env.example": buildEnvExample(context),
2541
3553
  ".gitignore": buildGitignore(),
2542
- ".npmrc": buildNpmrc(),
2543
3554
  "README.md": buildReadme(context)
2544
3555
  };
2545
3556
  if (!inputs.logo.isDefault && inputs.logo.fileName) {
@@ -2619,6 +3630,11 @@ program.command("create").description("Scaffold a Bandit quickstart project with
2619
3630
  console.log(" cp .env.example .env");
2620
3631
  console.log(" npm run dev");
2621
3632
  console.log("");
3633
+ console.log("\u{1F50D} Before you dive in:");
3634
+ console.log(" \u2022 Open .env to confirm the provider credentials and URLs match your setup.");
3635
+ 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.");
3636
+ console.log(" \u2022 If you prefer Next.js App Router, check server/next-app/ for a starter route handler.");
3637
+ console.log("");
2622
3638
  } catch (error) {
2623
3639
  const message = error instanceof Error ? error.message : "Failed to create Bandit quickstart project.";
2624
3640
  console.error(`