@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.
- package/README.md +6 -5
- package/dist/{aiProviderStore-3YS2BZU3.mjs → aiProviderStore-UJRDUYOF.mjs} +2 -2
- package/dist/{chat-2LYIZNWZ.mjs → chat-SZK3EBDO.mjs} +5 -5
- package/dist/chat-provider.js +227 -11
- package/dist/chat-provider.js.map +1 -1
- package/dist/chat-provider.mjs +4 -4
- package/dist/{chunk-6PQRG6W4.mjs → chunk-2ZZA2IFL.mjs} +3 -3
- package/dist/{chunk-GBANNFRD.mjs → chunk-ED5NNDKO.mjs} +3 -3
- package/dist/{chunk-XD5VJCFN.mjs → chunk-FJO5ZWYU.mjs} +3 -3
- package/dist/{chunk-XXMCI2WK.mjs → chunk-G4OXOTNJ.mjs} +41 -8
- package/dist/{chunk-XXMCI2WK.mjs.map → chunk-G4OXOTNJ.mjs.map} +1 -1
- package/dist/{chunk-LG2JCTOE.mjs → chunk-PLNFTIGX.mjs} +4 -4
- package/dist/{chunk-7RLN6ZGT.mjs → chunk-S635Q6OQ.mjs} +3 -3
- package/dist/{chunk-IGD4KGB5.mjs → chunk-ZAVV2AT5.mjs} +4 -4
- package/dist/{chunk-IHJPVIGB.mjs → chunk-ZNNOTDRD.mjs} +208 -1
- package/dist/chunk-ZNNOTDRD.mjs.map +1 -0
- package/dist/cli/cli.js +1104 -68
- package/dist/cli/cli.js.map +1 -1
- package/dist/{gateway-BiHRHJMM.d.ts → gateway-Ckf_KusF.d.mts} +4 -4
- package/dist/{gateway-BiHRHJMM.d.mts → gateway-Ckf_KusF.d.ts} +4 -4
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +318 -69
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +8 -8
- package/dist/management/management.js +316 -67
- package/dist/management/management.js.map +1 -1
- package/dist/management/management.mjs +6 -6
- package/dist/modals/chat-modal/chat-modal.js +236 -20
- package/dist/modals/chat-modal/chat-modal.js.map +1 -1
- package/dist/modals/chat-modal/chat-modal.mjs +4 -4
- package/dist/public-types.d.mts +1 -1
- package/dist/public-types.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-IHJPVIGB.mjs.map +0 -1
- /package/dist/{aiProviderStore-3YS2BZU3.mjs.map → aiProviderStore-UJRDUYOF.mjs.map} +0 -0
- /package/dist/{chat-2LYIZNWZ.mjs.map → chat-SZK3EBDO.mjs.map} +0 -0
- /package/dist/{chunk-6PQRG6W4.mjs.map → chunk-2ZZA2IFL.mjs.map} +0 -0
- /package/dist/{chunk-GBANNFRD.mjs.map → chunk-ED5NNDKO.mjs.map} +0 -0
- /package/dist/{chunk-XD5VJCFN.mjs.map → chunk-FJO5ZWYU.mjs.map} +0 -0
- /package/dist/{chunk-LG2JCTOE.mjs.map → chunk-PLNFTIGX.mjs.map} +0 -0
- /package/dist/{chunk-7RLN6ZGT.mjs.map → chunk-S635Q6OQ.mjs.map} +0 -0
- /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.
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
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) =>
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
#
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ? "
|
|
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
|
|
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 "
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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: "
|
|
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: "
|
|
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 ?? "
|
|
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(`
|