@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.
- package/README.md +3 -2
- 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 +1078 -62
- 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.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) =>
|
|
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
|
-
);
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ? "
|
|
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-
|
|
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-
|
|
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: "
|
|
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: "
|
|
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 ?? "
|
|
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(`
|