@burtson-labs/bandit-engine 2.0.33 → 2.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/cli/cli.js +843 -21
  2. package/dist/cli/cli.js.map +1 -1
  3. package/docs/api_reference/classes/DebugLogger.html +11 -11
  4. package/docs/api_reference/classes/FeatureFlagService.html +13 -13
  5. package/docs/api_reference/classes/NotificationService.html +10 -10
  6. package/docs/api_reference/classes/StreamingTTSClient.html +9 -9
  7. package/docs/api_reference/classes/VectorDatabaseService.html +24 -24
  8. package/docs/api_reference/classes/VectorMigrationService.html +8 -8
  9. package/docs/api_reference/classes/VoiceService.html +2 -2
  10. package/docs/api_reference/enums/TTSState.html +2 -2
  11. package/docs/api_reference/functions/Chat.html +1 -1
  12. package/docs/api_reference/functions/ChatModal.html +1 -1
  13. package/docs/api_reference/functions/ChatProvider.html +1 -1
  14. package/docs/api_reference/functions/FeatureFlagProvider.html +1 -1
  15. package/docs/api_reference/functions/FeedbackButton.html +1 -1
  16. package/docs/api_reference/functions/FeedbackModal.html +1 -1
  17. package/docs/api_reference/functions/Management.html +1 -1
  18. package/docs/api_reference/functions/NotificationProvider.html +1 -1
  19. package/docs/api_reference/functions/SubscriptionExpiredGuard.html +1 -1
  20. package/docs/api_reference/functions/SubscriptionExpiredModal.html +1 -1
  21. package/docs/api_reference/functions/defineCustomElement.html +1 -1
  22. package/docs/api_reference/functions/getCriticalConfig.html +1 -1
  23. package/docs/api_reference/functions/getFeatureMatrix.html +1 -1
  24. package/docs/api_reference/functions/getStreamingTTSClient.html +1 -1
  25. package/docs/api_reference/functions/getSystemConstants.html +1 -1
  26. package/docs/api_reference/functions/getTTSState.html +1 -1
  27. package/docs/api_reference/functions/handleHttpError.html +1 -1
  28. package/docs/api_reference/functions/handleSubscriptionUpgrade.html +1 -1
  29. package/docs/api_reference/functions/handleValidationError.html +1 -1
  30. package/docs/api_reference/functions/initializeCoreSystem.html +1 -1
  31. package/docs/api_reference/functions/pauseTTS.html +1 -1
  32. package/docs/api_reference/functions/previewTierUpgrade.html +1 -1
  33. package/docs/api_reference/functions/resumeTTS.html +1 -1
  34. package/docs/api_reference/functions/showInfoNotification.html +1 -1
  35. package/docs/api_reference/functions/showSuccessNotification.html +1 -1
  36. package/docs/api_reference/functions/speakWithStreaming.html +1 -1
  37. package/docs/api_reference/functions/stopTTS.html +1 -1
  38. package/docs/api_reference/functions/syncSubscriptionWithAPI.html +1 -1
  39. package/docs/api_reference/functions/updateSubscriptionTier.html +1 -1
  40. package/docs/api_reference/functions/useFeatureFlag.html +1 -1
  41. package/docs/api_reference/functions/useFeatureVisibility.html +1 -1
  42. package/docs/api_reference/functions/useFeatures.html +1 -1
  43. package/docs/api_reference/functions/useGatewayHealth.html +1 -1
  44. package/docs/api_reference/functions/useGatewayMemory.html +1 -1
  45. package/docs/api_reference/functions/useGatewayModels.html +1 -1
  46. package/docs/api_reference/functions/useGlobalTTS.html +1 -1
  47. package/docs/api_reference/functions/useNotification.html +1 -1
  48. package/docs/api_reference/functions/useNotificationService.html +1 -1
  49. package/docs/api_reference/functions/useTTS.html +1 -1
  50. package/docs/api_reference/functions/useVectorStore.html +1 -1
  51. package/docs/api_reference/functions/useVoiceStore.html +2 -2
  52. package/docs/api_reference/functions/useVoices.html +1 -1
  53. package/docs/api_reference/functions/validateEnvironment.html +1 -1
  54. package/docs/api_reference/functions/validateSystemIntegrity.html +1 -1
  55. package/docs/api_reference/interfaces/AIChatRequest.html +2 -2
  56. package/docs/api_reference/interfaces/AIChatResponse.html +2 -2
  57. package/docs/api_reference/interfaces/AIGenerateRequest.html +2 -2
  58. package/docs/api_reference/interfaces/AIGenerateResponse.html +2 -2
  59. package/docs/api_reference/interfaces/AIMessage.html +2 -2
  60. package/docs/api_reference/interfaces/AIModel.html +2 -2
  61. package/docs/api_reference/interfaces/AIProviderConfig.html +2 -2
  62. package/docs/api_reference/interfaces/ChatConfig.html +3 -3
  63. package/docs/api_reference/interfaces/ChatModalProps.html +3 -3
  64. package/docs/api_reference/interfaces/CreateMemoryOptions.html +2 -2
  65. package/docs/api_reference/interfaces/FeatureEvaluation.html +7 -7
  66. package/docs/api_reference/interfaces/FeatureFlagConfig.html +9 -9
  67. package/docs/api_reference/interfaces/FeatureFlagContextValue.html +8 -8
  68. package/docs/api_reference/interfaces/FeatureFlagProviderProps.html +2 -2
  69. package/docs/api_reference/interfaces/FeedbackButtonProps.html +10 -10
  70. package/docs/api_reference/interfaces/FeedbackCategories.html +2 -2
  71. package/docs/api_reference/interfaces/FeedbackModalProps.html +2 -2
  72. package/docs/api_reference/interfaces/FeedbackPriorities.html +2 -2
  73. package/docs/api_reference/interfaces/FeedbackRequest.html +2 -2
  74. package/docs/api_reference/interfaces/FeedbackResponse.html +2 -2
  75. package/docs/api_reference/interfaces/FileUploadResult.html +2 -2
  76. package/docs/api_reference/interfaces/GatewayChatRequest.html +2 -2
  77. package/docs/api_reference/interfaces/GatewayChatResponse.html +2 -2
  78. package/docs/api_reference/interfaces/GatewayContract.html +2 -2
  79. package/docs/api_reference/interfaces/GatewayGenerateRequest.html +2 -2
  80. package/docs/api_reference/interfaces/GatewayGenerateResponse.html +2 -2
  81. package/docs/api_reference/interfaces/GatewayHealthResponse.html +2 -2
  82. package/docs/api_reference/interfaces/GatewayMemoryRecord.html +2 -2
  83. package/docs/api_reference/interfaces/GatewayMemoryResponse.html +2 -2
  84. package/docs/api_reference/interfaces/GatewayMessage.html +2 -2
  85. package/docs/api_reference/interfaces/GatewayMessageContent.html +2 -2
  86. package/docs/api_reference/interfaces/GatewayModel.html +2 -2
  87. package/docs/api_reference/interfaces/GatewayModelsResponse.html +2 -2
  88. package/docs/api_reference/interfaces/MemorySearchFilters.html +2 -2
  89. package/docs/api_reference/interfaces/MigrationProgress.html +2 -2
  90. package/docs/api_reference/interfaces/MigrationStatus.html +2 -2
  91. package/docs/api_reference/interfaces/NotificationConfig.html +2 -2
  92. package/docs/api_reference/interfaces/NotificationContextType.html +2 -2
  93. package/docs/api_reference/interfaces/NotificationProviderProps.html +2 -2
  94. package/docs/api_reference/interfaces/PackageSettings.html +2 -2
  95. package/docs/api_reference/interfaces/SearchOptions.html +2 -2
  96. package/docs/api_reference/interfaces/SearchResult.html +2 -2
  97. package/docs/api_reference/interfaces/SubscriptionExpiredGuardProps.html +2 -2
  98. package/docs/api_reference/interfaces/SubscriptionExpiredModalProps.html +2 -2
  99. package/docs/api_reference/interfaces/TTSOptions.html +2 -2
  100. package/docs/api_reference/interfaces/TTSProgress.html +2 -2
  101. package/docs/api_reference/interfaces/TrialUsage.html +2 -2
  102. package/docs/api_reference/interfaces/UploadRequest.html +3 -3
  103. package/docs/api_reference/interfaces/UseTTSReturn.html +2 -2
  104. package/docs/api_reference/interfaces/VectorDocument.html +2 -2
  105. package/docs/api_reference/interfaces/VectorMemory.html +2 -2
  106. package/docs/api_reference/interfaces/VectorMemoryMetadata.html +2 -2
  107. package/docs/api_reference/interfaces/VectorStoreStatus.html +2 -2
  108. package/docs/api_reference/interfaces/VoiceModelsResponse.html +2 -2
  109. package/docs/api_reference/interfaces/VoiceState.html +2 -2
  110. package/docs/api_reference/types/FeatureKey.html +1 -1
  111. package/docs/api_reference/types/FeatureMatrix.html +1 -1
  112. package/docs/api_reference/types/GatewayQueryOptions.html +1 -1
  113. package/docs/api_reference/types/LogContext.html +1 -1
  114. package/docs/api_reference/types/SubscriptionTier.html +1 -1
  115. package/docs/api_reference/variables/DEFAULT_TIER_FEATURES.html +1 -1
  116. package/docs/api_reference/variables/FeatureFlagContext.html +1 -1
  117. package/docs/api_reference/variables/OSS_DEFAULT_FEATURES.html +1 -1
  118. package/docs/api_reference/variables/SYSTEM_FLAGS.html +1 -1
  119. package/docs/api_reference/variables/authenticationService.html +1 -1
  120. package/docs/api_reference/variables/debugLogger-1.html +1 -1
  121. package/docs/api_reference/variables/featureFlagService-1.html +1 -1
  122. package/docs/api_reference/variables/notificationService-1.html +1 -1
  123. package/docs/api_reference/variables/vectorDatabaseService-1.html +1 -1
  124. package/docs/api_reference/variables/vectorMigrationService-1.html +1 -1
  125. package/docs/api_reference/variables/voiceService-1.html +1 -1
  126. package/package.json +2 -2
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",
33
+ version: "2.0.35",
34
34
  license: "BUSL-1.1",
35
35
  main: "dist/index.js",
36
36
  module: "dist/index.mjs",
@@ -73,7 +73,7 @@ var package_default = {
73
73
  scripts: {
74
74
  build: "tsup",
75
75
  dev: "tsup --watch",
76
- docs: 'typedoc src --out docs/api_reference --skipErrorChecking --sourceLinkTemplate "https://github.com/Burtson-Labs/bandit-engine/blob/{gitRevision}/{path}#L{line}" && node ./scripts/post-typedoc.mjs',
76
+ docs: 'typedoc src --out docs/api_reference --skipErrorChecking --sourceLinkTemplate "https://github.com/Burtson-Labs/bandit-engine/blob/main/{path}#L{line}" && node ./scripts/post-typedoc.mjs',
77
77
  lint: 'eslint "src/**/*.{ts,tsx}"',
78
78
  test: "vitest",
79
79
  protect: "node scripts/add-license-headers.js",
@@ -226,6 +226,16 @@ VITE_BRANDING_TEXT=${ctx.brandingText}
226
226
 
227
227
  # Gateway configuration
228
228
  # OPENAI_API_KEY=sk-................................
229
+ # AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
230
+ # AZURE_OPENAI_API_KEY=................................................................
231
+ # AZURE_OPENAI_API_VERSION=2024-08-01-preview
232
+ # AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-4o
233
+ # AZURE_OPENAI_COMPLETIONS_DEPLOYMENT=gpt-35-turbo-instruct
234
+ # AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT=text-embedding-3-large
235
+ # ANTHROPIC_API_KEY=sk-ant-................................
236
+ # ANTHROPIC_BASE_URL=https://api.anthropic.com
237
+ # ANTHROPIC_API_VERSION=2023-06-01
238
+ # ANTHROPIC_MAX_TOKENS=1024
229
239
  # OLLAMA_URL=http://localhost:11434
230
240
  # PORT=${ctx.gatewayPort}
231
241
  `
@@ -429,7 +439,7 @@ const gatewayBaseUrl = (import.meta.env.VITE_GATEWAY_URL ?? "${ctx.defaultGatewa
429
439
  const defaultModelId = import.meta.env.VITE_DEFAULT_MODEL ?? "${ctx.defaultModelId}";
430
440
  const fallbackModelId = import.meta.env.VITE_FALLBACK_MODEL ?? ${ctx.fallbackModelId ? `${QUOTE}${ctx.fallbackModelId}${QUOTE}` : "undefined"};
431
441
  const brandingText = import.meta.env.VITE_BRANDING_TEXT ?? "${ctx.brandingText}";
432
- const provider = (import.meta.env.VITE_GATEWAY_PROVIDER ?? "${ctx.defaultProvider}") as "openai" | "ollama";
442
+ const provider = (import.meta.env.VITE_GATEWAY_PROVIDER ?? "${ctx.defaultProvider}") as "openai" | "ollama" | "azure" | "anthropic";
433
443
 
434
444
  const gatewayApiUrl = gatewayBaseUrl.endsWith("/api") ? gatewayBaseUrl : gatewayBaseUrl + "/api";
435
445
  const banditHeadLogoUrl = "https://cdn.burtson.ai/images/bandit-head.png";
@@ -611,7 +621,7 @@ function App() {
611
621
  {brandingText}
612
622
  </Typography>
613
623
  <Typography variant="body1" color="text.secondary">
614
- Build, brand, and launch your assistant with a drop-in chat surface plus a secure gateway for OpenAI or Ollama.
624
+ Build, brand, and launch your assistant with a drop-in chat surface plus a secure gateway for OpenAI, Azure OpenAI, Anthropic, or Ollama.
615
625
  </Typography>
616
626
  <Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
617
627
  <Button component={RouterLink} to="/chat" variant="contained" color="primary">
@@ -658,7 +668,7 @@ function App() {
658
668
  Ship secure gateways
659
669
  </Typography>
660
670
  <Typography variant="body2" color="text.secondary">
661
- Keep API keys server-side while proxying requests to OpenAI or Ollama through the included Express gateway.
671
+ Keep API keys server-side while proxying requests to OpenAI, Azure OpenAI, Anthropic, or Ollama through the included Express gateway.
662
672
  </Typography>
663
673
  </CardContent>
664
674
  </Card>
@@ -837,6 +847,18 @@ const QUICKSTART_VERSION = "0.1.0";
837
847
  const DEFAULT_PROVIDER = "${ctx.defaultProvider}";
838
848
  const BASE_GATEWAY_MODELS = ${modelsDefinition};
839
849
  const OLLAMA_BASE_URL = (process.env.OLLAMA_URL ?? "http://localhost:11434").replace(/\\/$/, "");
850
+ const AZURE_OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT ? process.env.AZURE_OPENAI_ENDPOINT.replace(/\\/$/, "") : undefined;
851
+ const AZURE_OPENAI_API_KEY = process.env.AZURE_OPENAI_API_KEY;
852
+ const AZURE_OPENAI_API_VERSION = process.env.AZURE_OPENAI_API_VERSION ?? "2024-08-01-preview";
853
+ const AZURE_OPENAI_CHAT_DEPLOYMENT = process.env.AZURE_OPENAI_CHAT_DEPLOYMENT;
854
+ const AZURE_OPENAI_COMPLETIONS_DEPLOYMENT = process.env.AZURE_OPENAI_COMPLETIONS_DEPLOYMENT ?? AZURE_OPENAI_CHAT_DEPLOYMENT;
855
+ const AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT = process.env.AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT;
856
+ const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
857
+ const ANTHROPIC_BASE_URL = (process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com").replace(/\\/$/, "");
858
+ const ANTHROPIC_API_VERSION = process.env.ANTHROPIC_API_VERSION ?? "2023-06-01";
859
+ const ANTHROPIC_MAX_TOKENS = Number.isFinite(Number(process.env.ANTHROPIC_MAX_TOKENS))
860
+ ? Number(process.env.ANTHROPIC_MAX_TOKENS)
861
+ : 1024;
840
862
 
841
863
  const toGatewayModels = () =>
842
864
  BASE_GATEWAY_MODELS.map((model) => ({
@@ -854,6 +876,187 @@ const toGatewayModels = () =>
854
876
  },
855
877
  }));
856
878
 
879
+ const stripAzureModelPrefix = (value) =>
880
+ typeof value === "string" ? value.replace(/^azure:/, "") : undefined;
881
+
882
+ const isAzureConfigured = () => Boolean(AZURE_OPENAI_ENDPOINT && AZURE_OPENAI_API_KEY);
883
+
884
+ const requireAzureBaseConfig = () => {
885
+ if (!AZURE_OPENAI_ENDPOINT) {
886
+ throw new Error("Missing AZURE_OPENAI_ENDPOINT. Add it to your .env file to route requests to Azure OpenAI.");
887
+ }
888
+ if (!AZURE_OPENAI_API_KEY) {
889
+ throw new Error("Missing AZURE_OPENAI_API_KEY. Add it to your .env file to route requests to Azure OpenAI.");
890
+ }
891
+ return {
892
+ endpoint: AZURE_OPENAI_ENDPOINT,
893
+ apiKey: AZURE_OPENAI_API_KEY,
894
+ apiVersion: AZURE_OPENAI_API_VERSION,
895
+ };
896
+ };
897
+
898
+ const resolveAzureDeployment = (explicitValue, fallbackValue, kind) => {
899
+ const fromRequest = stripAzureModelPrefix(explicitValue);
900
+ if (fromRequest) {
901
+ return fromRequest;
902
+ }
903
+ if (fallbackValue) {
904
+ return fallbackValue;
905
+ }
906
+ throw new Error(\`Missing Azure OpenAI \${kind} deployment name. Set AZURE_OPENAI_\${kind.toUpperCase()}_DEPLOYMENT in your .env file.\`);
907
+ };
908
+
909
+ const buildAzureDeploymentUrl = (deployment, suffix) => {
910
+ const { endpoint } = requireAzureBaseConfig();
911
+ const normalizedSuffix = suffix.replace(/^\\//, "");
912
+ return \`\${endpoint}/openai/deployments/\${deployment}/\${normalizedSuffix}?api-version=\${AZURE_OPENAI_API_VERSION}\`;
913
+ };
914
+
915
+ const buildAzurePath = (suffix) => {
916
+ const { endpoint } = requireAzureBaseConfig();
917
+ const normalizedSuffix = suffix.replace(/^\\//, "");
918
+ const hasQuery = normalizedSuffix.includes("?");
919
+ const separator = hasQuery ? "&" : "?";
920
+ return \`\${endpoint}/openai/\${normalizedSuffix}\${separator}api-version=\${AZURE_OPENAI_API_VERSION}\`;
921
+ };
922
+
923
+ const stripAnthropicModelPrefix = (value) =>
924
+ typeof value === "string" ? value.replace(/^anthropic:/, "") : undefined;
925
+
926
+ const isAnthropicConfigured = () => Boolean(ANTHROPIC_API_KEY);
927
+
928
+ const requireAnthropicKey = () => {
929
+ if (!ANTHROPIC_API_KEY) {
930
+ throw new Error("Missing ANTHROPIC_API_KEY. Add it to your .env file to route requests to Anthropic.");
931
+ }
932
+ return ANTHROPIC_API_KEY;
933
+ };
934
+
935
+ const buildAnthropicUrl = (path) => {
936
+ const normalized = path.replace(/^\\//, "");
937
+ return \`\${ANTHROPIC_BASE_URL}/v1/\${normalized}\`;
938
+ };
939
+
940
+ const buildAnthropicHeaders = () => ({
941
+ "Content-Type": "application/json",
942
+ "x-api-key": requireAnthropicKey(),
943
+ "anthropic-version": ANTHROPIC_API_VERSION,
944
+ });
945
+
946
+ const flattenGatewayContent = (content) => {
947
+ if (typeof content === "string") {
948
+ return content;
949
+ }
950
+ if (Array.isArray(content)) {
951
+ return content
952
+ .map((part) => {
953
+ if (typeof part === "string") {
954
+ return part;
955
+ }
956
+ if (part?.type === "text" && typeof part.text === "string") {
957
+ return part.text;
958
+ }
959
+ if (part?.type === "image_url" && part.image_url?.url) {
960
+ return \`[Image: \${part.image_url.url}]\`;
961
+ }
962
+ return JSON.stringify(part ?? {});
963
+ })
964
+ .join("\\n");
965
+ }
966
+ if (content && typeof content === "object") {
967
+ return JSON.stringify(content);
968
+ }
969
+ return "";
970
+ };
971
+
972
+ const toAnthropicMessages = (messages = []) => {
973
+ const anthropicMessages = [];
974
+ let systemPrompt = "";
975
+
976
+ for (const message of messages) {
977
+ if (!message) continue;
978
+ const text = flattenGatewayContent(message.content);
979
+
980
+ if (message.role === "system") {
981
+ systemPrompt = systemPrompt ? \`\${systemPrompt}\\n\\n\${text}\` : text;
982
+ continue;
983
+ }
984
+
985
+ const role = message.role === "assistant" ? "assistant" : "user";
986
+ anthropicMessages.push({
987
+ role,
988
+ content: [{ type: "text", text }],
989
+ });
990
+ }
991
+
992
+ return { messages: anthropicMessages, system: systemPrompt || undefined };
993
+ };
994
+
995
+ const convertAnthropicResponseToGateway = (responseBody, modelName) => {
996
+ if (!responseBody) {
997
+ return {
998
+ id: \`anthropic-\${Date.now()}\`,
999
+ object: "chat.completion",
1000
+ created: Math.floor(Date.now() / 1000),
1001
+ model: modelName.startsWith("anthropic:") ? modelName : \`anthropic:\${modelName}\`,
1002
+ choices: [],
1003
+ };
1004
+ }
1005
+
1006
+ const textContent = Array.isArray(responseBody.content)
1007
+ ? responseBody.content
1008
+ .filter((item) => item && item.type === "text" && typeof item.text === "string")
1009
+ .map((item) => item.text)
1010
+ .join("\\n")
1011
+ : typeof responseBody.content === "string"
1012
+ ? responseBody.content
1013
+ : "";
1014
+
1015
+ const promptTokens = responseBody.usage?.input_tokens ?? 0;
1016
+ const completionTokens = responseBody.usage?.output_tokens ?? 0;
1017
+
1018
+ return {
1019
+ id: responseBody.id ?? \`anthropic-\${Date.now()}\`,
1020
+ object: "chat.completion",
1021
+ created: Math.floor(Date.now() / 1000),
1022
+ model: modelName.startsWith("anthropic:") ? modelName : \`anthropic:\${modelName}\`,
1023
+ choices: [
1024
+ {
1025
+ index: 0,
1026
+ message: {
1027
+ role: responseBody.role ?? "assistant",
1028
+ content: textContent,
1029
+ },
1030
+ finish_reason: responseBody.stop_reason ?? responseBody.stop_sequence ?? null,
1031
+ },
1032
+ ],
1033
+ usage: responseBody.usage
1034
+ ? {
1035
+ prompt_tokens: promptTokens,
1036
+ completion_tokens: completionTokens,
1037
+ total_tokens: promptTokens + completionTokens,
1038
+ }
1039
+ : undefined,
1040
+ };
1041
+ };
1042
+
1043
+ const convertAnthropicResponseToGenerate = (responseBody, modelName) => {
1044
+ const gatewayResponse = convertAnthropicResponseToGateway(responseBody, modelName);
1045
+ const content = gatewayResponse.choices?.[0]?.message?.content ?? "";
1046
+ return {
1047
+ model: gatewayResponse.model,
1048
+ created_at: new Date().toISOString(),
1049
+ response: content,
1050
+ done: true,
1051
+ total_duration: 0,
1052
+ load_duration: 0,
1053
+ prompt_eval_count: gatewayResponse.usage?.prompt_tokens ?? 0,
1054
+ prompt_eval_duration: 0,
1055
+ eval_count: gatewayResponse.usage?.completion_tokens ?? 0,
1056
+ eval_duration: 0,
1057
+ };
1058
+ };
1059
+
857
1060
  const requireOpenAIKey = () => {
858
1061
  const key = process.env.OPENAI_API_KEY;
859
1062
  if (!key) {
@@ -926,6 +1129,79 @@ app.get("/api/health", async (_req, res) => {
926
1129
  });
927
1130
  }
928
1131
 
1132
+ // Check Azure OpenAI
1133
+ if (AZURE_OPENAI_ENDPOINT || AZURE_OPENAI_API_KEY) {
1134
+ if (!isAzureConfigured()) {
1135
+ providers.push({
1136
+ name: "azure",
1137
+ status: "unconfigured",
1138
+ provider: "azure",
1139
+ error: "Endpoint or API key not configured",
1140
+ endpoint: AZURE_OPENAI_ENDPOINT
1141
+ });
1142
+ } else {
1143
+ try {
1144
+ const { endpoint } = requireAzureBaseConfig();
1145
+ const deploymentsUrl = buildAzurePath("deployments");
1146
+ const response = await fetch(deploymentsUrl, {
1147
+ headers: { "api-key": AZURE_OPENAI_API_KEY }
1148
+ });
1149
+ providers.push({
1150
+ name: "azure",
1151
+ status: response.ok ? "healthy" : "unhealthy",
1152
+ provider: "azure",
1153
+ endpoint
1154
+ });
1155
+ } catch (error) {
1156
+ providers.push({
1157
+ name: "azure",
1158
+ status: "unhealthy",
1159
+ provider: "azure",
1160
+ error: error instanceof Error ? error.message : String(error),
1161
+ endpoint: AZURE_OPENAI_ENDPOINT
1162
+ });
1163
+ }
1164
+ }
1165
+ } else {
1166
+ providers.push({
1167
+ name: "azure",
1168
+ status: "unconfigured",
1169
+ provider: "azure",
1170
+ error: "Endpoint or API key not configured"
1171
+ });
1172
+ }
1173
+
1174
+ // Check Anthropic
1175
+ if (ANTHROPIC_API_KEY) {
1176
+ try {
1177
+ const response = await fetch(buildAnthropicUrl("models"), {
1178
+ headers: buildAnthropicHeaders(),
1179
+ method: "GET"
1180
+ });
1181
+ providers.push({
1182
+ name: "anthropic",
1183
+ status: response.ok ? "healthy" : "unhealthy",
1184
+ provider: "anthropic",
1185
+ endpoint: ANTHROPIC_BASE_URL
1186
+ });
1187
+ } catch (error) {
1188
+ providers.push({
1189
+ name: "anthropic",
1190
+ status: "unhealthy",
1191
+ provider: "anthropic",
1192
+ error: error instanceof Error ? error.message : String(error),
1193
+ endpoint: ANTHROPIC_BASE_URL
1194
+ });
1195
+ }
1196
+ } else {
1197
+ providers.push({
1198
+ name: "anthropic",
1199
+ status: "unconfigured",
1200
+ provider: "anthropic",
1201
+ error: "API key not configured"
1202
+ });
1203
+ }
1204
+
929
1205
  // Check Ollama
930
1206
  try {
931
1207
  console.log(\`Checking Ollama health at: \${OLLAMA_BASE_URL}/api/tags\`);
@@ -963,6 +1239,495 @@ app.get("/api/models", (_req, res) => {
963
1239
  res.json({ models: toGatewayModels() });
964
1240
  });
965
1241
 
1242
+ // ============================================================================
1243
+ // ANTHROPIC ROUTES
1244
+ // ============================================================================
1245
+
1246
+ app.get("/api/anthropic/health", async (_req, res) => {
1247
+ try {
1248
+ requireAnthropicKey();
1249
+ const response = await fetch(buildAnthropicUrl("models"), {
1250
+ method: "GET",
1251
+ headers: buildAnthropicHeaders()
1252
+ });
1253
+ const isHealthy = response.ok;
1254
+ res.json({
1255
+ status: isHealthy ? "healthy" : "unhealthy",
1256
+ anthropic_status: isHealthy,
1257
+ provider: "anthropic",
1258
+ endpoint: ANTHROPIC_BASE_URL
1259
+ });
1260
+ } catch (error) {
1261
+ const message = error instanceof Error ? error.message : String(error);
1262
+ res.status(503).json({
1263
+ status: "unhealthy",
1264
+ anthropic_status: false,
1265
+ provider: "anthropic",
1266
+ error: message,
1267
+ endpoint: ANTHROPIC_BASE_URL
1268
+ });
1269
+ }
1270
+ });
1271
+
1272
+ app.post("/api/anthropic/chat/completions", async (req, res) => {
1273
+ try {
1274
+ requireAnthropicKey();
1275
+ const rawBody = req.body ?? {};
1276
+ const isStreaming = rawBody.stream === true;
1277
+ const requestedModel =
1278
+ stripAnthropicModelPrefix(rawBody.model) ??
1279
+ stripAnthropicModelPrefix("${ctx.defaultModelId}") ??
1280
+ "claude-3-5-sonnet-latest";
1281
+
1282
+ const stopSequences = Array.isArray(rawBody.stop)
1283
+ ? rawBody.stop
1284
+ : Array.isArray(rawBody.stop_sequences)
1285
+ ? rawBody.stop_sequences
1286
+ : rawBody.stop
1287
+ ? [rawBody.stop]
1288
+ : undefined;
1289
+
1290
+ const { messages: anthropicMessages, system } = toAnthropicMessages(
1291
+ Array.isArray(rawBody.messages) ? rawBody.messages : []
1292
+ );
1293
+
1294
+ const fallbackText =
1295
+ typeof rawBody.prompt === "string" && rawBody.prompt.trim().length > 0
1296
+ ? rawBody.prompt
1297
+ : "Hello from Bandit quickstart gateway";
1298
+
1299
+ const requestBody = {
1300
+ model: requestedModel,
1301
+ messages:
1302
+ anthropicMessages.length > 0
1303
+ ? anthropicMessages
1304
+ : [
1305
+ {
1306
+ role: "user",
1307
+ content: [{ type: "text", text: fallbackText }],
1308
+ },
1309
+ ],
1310
+ stream: isStreaming,
1311
+ max_tokens:
1312
+ typeof rawBody.max_tokens === "number" && rawBody.max_tokens > 0
1313
+ ? rawBody.max_tokens
1314
+ : ANTHROPIC_MAX_TOKENS,
1315
+ };
1316
+
1317
+ if (system) {
1318
+ requestBody.system = system;
1319
+ }
1320
+ if (typeof rawBody.temperature === "number") {
1321
+ requestBody.temperature = rawBody.temperature;
1322
+ }
1323
+ if (typeof rawBody.top_p === "number") {
1324
+ requestBody.top_p = rawBody.top_p;
1325
+ }
1326
+ if (typeof rawBody.top_k === "number") {
1327
+ requestBody.top_k = rawBody.top_k;
1328
+ }
1329
+ if (stopSequences) {
1330
+ requestBody.stop_sequences = stopSequences;
1331
+ }
1332
+ if (rawBody.metadata) {
1333
+ requestBody.metadata = rawBody.metadata;
1334
+ }
1335
+ if (rawBody.tools) {
1336
+ requestBody.tools = rawBody.tools;
1337
+ }
1338
+ if (rawBody.tool_choice) {
1339
+ requestBody.tool_choice = rawBody.tool_choice;
1340
+ }
1341
+ if (rawBody.thinking) {
1342
+ requestBody.thinking = rawBody.thinking;
1343
+ }
1344
+ if (rawBody.extra_headers) {
1345
+ requestBody.extra_headers = rawBody.extra_headers;
1346
+ }
1347
+
1348
+ const response = await fetch(buildAnthropicUrl("messages"), {
1349
+ method: "POST",
1350
+ headers: buildAnthropicHeaders(),
1351
+ body: JSON.stringify(requestBody),
1352
+ });
1353
+
1354
+ if (!response.ok) {
1355
+ const errorText = await response.text();
1356
+ return res.status(response.status).json({
1357
+ error: \`Anthropic chat failed: \${response.status}\`,
1358
+ details: errorText,
1359
+ });
1360
+ }
1361
+
1362
+ if (isStreaming) {
1363
+ await handleStreamingResponse(response, res);
1364
+ } else {
1365
+ const data = await response.json();
1366
+ const normalized = convertAnthropicResponseToGateway(data, requestedModel);
1367
+ res.json(normalized);
1368
+ }
1369
+ } catch (error) {
1370
+ const message = error instanceof Error ? error.message : String(error);
1371
+ const status = message.startsWith("Missing ANTHROPIC_API_KEY") ? 400 : 500;
1372
+ res.status(status).json({ error: message });
1373
+ }
1374
+ });
1375
+
1376
+ app.post("/api/anthropic/chat", async (req, res) => {
1377
+ req.url = "/api/anthropic/chat/completions";
1378
+ return app._router.handle(req, res);
1379
+ });
1380
+
1381
+ app.post("/api/anthropic/completions", async (req, res) => {
1382
+ try {
1383
+ requireAnthropicKey();
1384
+ const rawBody = req.body ?? {};
1385
+ const isStreaming = rawBody.stream === true;
1386
+ const requestedModel =
1387
+ stripAnthropicModelPrefix(rawBody.model) ??
1388
+ stripAnthropicModelPrefix("${ctx.defaultModelId}") ??
1389
+ "claude-3-5-sonnet-latest";
1390
+
1391
+ const stopSequences = Array.isArray(rawBody.stop)
1392
+ ? rawBody.stop
1393
+ : Array.isArray(rawBody.stop_sequences)
1394
+ ? rawBody.stop_sequences
1395
+ : rawBody.stop
1396
+ ? [rawBody.stop]
1397
+ : undefined;
1398
+
1399
+ const prompt =
1400
+ typeof rawBody.prompt === "string" && rawBody.prompt.trim().length > 0
1401
+ ? rawBody.prompt
1402
+ : "Hello from Bandit quickstart gateway";
1403
+
1404
+ const { messages, system } = toAnthropicMessages([
1405
+ { role: "user", content: prompt },
1406
+ ]);
1407
+
1408
+ const requestBody = {
1409
+ model: requestedModel,
1410
+ messages,
1411
+ stream: isStreaming,
1412
+ max_tokens:
1413
+ typeof rawBody.max_tokens === "number" && rawBody.max_tokens > 0
1414
+ ? rawBody.max_tokens
1415
+ : ANTHROPIC_MAX_TOKENS,
1416
+ };
1417
+
1418
+ if (system) {
1419
+ requestBody.system = system;
1420
+ }
1421
+ if (typeof rawBody.temperature === "number") {
1422
+ requestBody.temperature = rawBody.temperature;
1423
+ }
1424
+ if (typeof rawBody.top_p === "number") {
1425
+ requestBody.top_p = rawBody.top_p;
1426
+ }
1427
+ if (typeof rawBody.top_k === "number") {
1428
+ requestBody.top_k = rawBody.top_k;
1429
+ }
1430
+ if (stopSequences) {
1431
+ requestBody.stop_sequences = stopSequences;
1432
+ }
1433
+ if (rawBody.metadata) {
1434
+ requestBody.metadata = rawBody.metadata;
1435
+ }
1436
+ if (rawBody.tools) {
1437
+ requestBody.tools = rawBody.tools;
1438
+ }
1439
+ if (rawBody.tool_choice) {
1440
+ requestBody.tool_choice = rawBody.tool_choice;
1441
+ }
1442
+
1443
+ const response = await fetch(buildAnthropicUrl("messages"), {
1444
+ method: "POST",
1445
+ headers: buildAnthropicHeaders(),
1446
+ body: JSON.stringify(requestBody),
1447
+ });
1448
+
1449
+ if (!response.ok) {
1450
+ const errorText = await response.text();
1451
+ return res.status(response.status).json({
1452
+ error: \`Anthropic completions failed: \${response.status}\`,
1453
+ details: errorText,
1454
+ });
1455
+ }
1456
+
1457
+ if (isStreaming) {
1458
+ await handleStreamingResponse(response, res);
1459
+ } else {
1460
+ const data = await response.json();
1461
+ const formatted = convertAnthropicResponseToGenerate(data, requestedModel);
1462
+ res.json(formatted);
1463
+ }
1464
+ } catch (error) {
1465
+ const message = error instanceof Error ? error.message : String(error);
1466
+ const status = message.startsWith("Missing ANTHROPIC_API_KEY") ? 400 : 500;
1467
+ res.status(status).json({ error: message });
1468
+ }
1469
+ });
1470
+
1471
+ app.post("/api/anthropic/generate", async (req, res) => {
1472
+ req.url = "/api/anthropic/completions";
1473
+ return app._router.handle(req, res);
1474
+ });
1475
+
1476
+ app.get("/api/anthropic/models", async (_req, res) => {
1477
+ try {
1478
+ requireAnthropicKey();
1479
+ const response = await fetch(buildAnthropicUrl("models"), {
1480
+ method: "GET",
1481
+ headers: buildAnthropicHeaders(),
1482
+ });
1483
+
1484
+ if (!response.ok) {
1485
+ const errorText = await response.text();
1486
+ return res.status(response.status).json({
1487
+ error: \`Anthropic models failed: \${response.status}\`,
1488
+ details: errorText,
1489
+ });
1490
+ }
1491
+
1492
+ const text = await response.text();
1493
+ res.setHeader('Content-Type', 'application/json');
1494
+ res.send(text);
1495
+ } catch (error) {
1496
+ const message = error instanceof Error ? error.message : String(error);
1497
+ const status = message.startsWith("Missing ANTHROPIC_API_KEY") ? 400 : 500;
1498
+ res.status(status).json({ error: message });
1499
+ }
1500
+ });
1501
+
1502
+ app.post("/api/anthropic/embed", async (_req, res) => {
1503
+ res.status(501).json({
1504
+ error: "Anthropic embeddings not implemented",
1505
+ message: "Add support for the Anthropic embeddings endpoint if your use case requires it."
1506
+ });
1507
+ });
1508
+
1509
+ // ============================================================================
1510
+ // AZURE OPENAI ROUTES
1511
+ // ============================================================================
1512
+
1513
+ app.get("/api/azure/health", async (_req, res) => {
1514
+ try {
1515
+ const { endpoint } = requireAzureBaseConfig();
1516
+ const deploymentsUrl = buildAzurePath("deployments");
1517
+ const response = await fetch(deploymentsUrl, {
1518
+ headers: { "api-key": AZURE_OPENAI_API_KEY }
1519
+ });
1520
+ const isHealthy = response.ok;
1521
+ res.json({
1522
+ status: isHealthy ? "healthy" : "unhealthy",
1523
+ azure_status: isHealthy,
1524
+ provider: "azure",
1525
+ endpoint
1526
+ });
1527
+ } catch (error) {
1528
+ res.status(503).json({
1529
+ status: "unhealthy",
1530
+ azure_status: false,
1531
+ provider: "azure",
1532
+ error: error instanceof Error ? error.message : String(error),
1533
+ endpoint: AZURE_OPENAI_ENDPOINT
1534
+ });
1535
+ }
1536
+ });
1537
+
1538
+ app.post("/api/azure/chat/completions", async (req, res) => {
1539
+ try {
1540
+ const { apiKey } = requireAzureBaseConfig();
1541
+ const deployment = resolveAzureDeployment(req.body?.model, AZURE_OPENAI_CHAT_DEPLOYMENT, "chat");
1542
+ const isStreaming = req.body?.stream === true;
1543
+ const { provider, model, ...cleanBody } = req.body ?? {};
1544
+ const requestBody = { ...cleanBody };
1545
+
1546
+ const response = await fetch(buildAzureDeploymentUrl(deployment, "chat/completions"), {
1547
+ method: "POST",
1548
+ headers: {
1549
+ "Content-Type": "application/json",
1550
+ "api-key": apiKey
1551
+ },
1552
+ body: JSON.stringify(requestBody)
1553
+ });
1554
+
1555
+ if (!response.ok) {
1556
+ const errorText = await response.text();
1557
+ return res.status(response.status).json({
1558
+ error: \`Azure OpenAI chat failed: \${response.status}\`,
1559
+ details: errorText
1560
+ });
1561
+ }
1562
+
1563
+ if (isStreaming) {
1564
+ await handleStreamingResponse(response, res);
1565
+ } else {
1566
+ const text = await response.text();
1567
+ res.setHeader('Content-Type', 'application/json');
1568
+ res.send(text);
1569
+ }
1570
+ } catch (error) {
1571
+ const message = error instanceof Error ? error.message : String(error);
1572
+ const status = message.startsWith("Missing Azure OpenAI") ? 400 : 500;
1573
+ res.status(status).json({ error: message });
1574
+ }
1575
+ });
1576
+
1577
+ app.post("/api/azure/chat", async (req, res) => {
1578
+ req.url = "/api/azure/chat/completions";
1579
+ return app._router.handle(req, res);
1580
+ });
1581
+
1582
+ app.post("/api/azure/completions", async (req, res) => {
1583
+ try {
1584
+ const { apiKey } = requireAzureBaseConfig();
1585
+ const deployment = resolveAzureDeployment(req.body?.model, AZURE_OPENAI_COMPLETIONS_DEPLOYMENT, "completions");
1586
+ const isStreaming = req.body?.stream === true;
1587
+ const { provider, model, ...cleanBody } = req.body ?? {};
1588
+ const requestBody = { ...cleanBody };
1589
+
1590
+ const response = await fetch(buildAzureDeploymentUrl(deployment, "completions"), {
1591
+ method: "POST",
1592
+ headers: {
1593
+ "Content-Type": "application/json",
1594
+ "api-key": apiKey
1595
+ },
1596
+ body: JSON.stringify(requestBody)
1597
+ });
1598
+
1599
+ if (!response.ok) {
1600
+ const errorText = await response.text();
1601
+ return res.status(response.status).json({
1602
+ error: \`Azure OpenAI completions failed: \${response.status}\`,
1603
+ details: errorText
1604
+ });
1605
+ }
1606
+
1607
+ if (isStreaming) {
1608
+ await handleStreamingResponse(response, res);
1609
+ } else {
1610
+ const text = await response.text();
1611
+ res.setHeader('Content-Type', 'application/json');
1612
+ res.send(text);
1613
+ }
1614
+ } catch (error) {
1615
+ const message = error instanceof Error ? error.message : String(error);
1616
+ const status = message.startsWith("Missing Azure OpenAI") ? 400 : 500;
1617
+ res.status(status).json({ error: message });
1618
+ }
1619
+ });
1620
+
1621
+ app.post("/api/azure/generate", async (req, res) => {
1622
+ try {
1623
+ const { apiKey } = requireAzureBaseConfig();
1624
+ const deployment = resolveAzureDeployment(req.body?.model, AZURE_OPENAI_CHAT_DEPLOYMENT, "chat");
1625
+ const prompt = req.body?.prompt || "";
1626
+ const isStreaming = req.body?.stream === true;
1627
+
1628
+ const chatBody = {
1629
+ messages: [
1630
+ {
1631
+ role: "user",
1632
+ content: prompt
1633
+ }
1634
+ ],
1635
+ stream: isStreaming,
1636
+ max_tokens: req.body?.max_tokens ?? 150,
1637
+ temperature: req.body?.temperature ?? 0.7
1638
+ };
1639
+
1640
+ const response = await fetch(buildAzureDeploymentUrl(deployment, "chat/completions"), {
1641
+ method: "POST",
1642
+ headers: {
1643
+ "Content-Type": "application/json",
1644
+ "api-key": apiKey
1645
+ },
1646
+ body: JSON.stringify(chatBody)
1647
+ });
1648
+
1649
+ if (!response.ok) {
1650
+ const errorText = await response.text();
1651
+ return res.status(response.status).json({
1652
+ error: \`Azure OpenAI generate failed: \${response.status}\`,
1653
+ details: errorText
1654
+ });
1655
+ }
1656
+
1657
+ if (isStreaming) {
1658
+ await handleStreamingResponse(response, res);
1659
+ } else {
1660
+ const text = await response.text();
1661
+ res.setHeader('Content-Type', 'application/json');
1662
+ res.send(text);
1663
+ }
1664
+ } catch (error) {
1665
+ const message = error instanceof Error ? error.message : String(error);
1666
+ const status = message.startsWith("Missing Azure OpenAI") ? 400 : 500;
1667
+ res.status(status).json({ error: message });
1668
+ }
1669
+ });
1670
+
1671
+ app.get("/api/azure/models", async (_req, res) => {
1672
+ try {
1673
+ requireAzureBaseConfig();
1674
+
1675
+ const response = await fetch(buildAzurePath("deployments"), {
1676
+ headers: { "api-key": AZURE_OPENAI_API_KEY }
1677
+ });
1678
+
1679
+ if (!response.ok) {
1680
+ const errorText = await response.text();
1681
+ return res.status(response.status).json({
1682
+ error: \`Azure OpenAI models failed: \${response.status}\`,
1683
+ details: errorText
1684
+ });
1685
+ }
1686
+
1687
+ const text = await response.text();
1688
+ res.setHeader('Content-Type', 'application/json');
1689
+ res.send(text);
1690
+ } catch (error) {
1691
+ const message = error instanceof Error ? error.message : String(error);
1692
+ const status = message.startsWith("Missing Azure OpenAI") ? 400 : 500;
1693
+ res.status(status).json({ error: message });
1694
+ }
1695
+ });
1696
+
1697
+ app.post("/api/azure/embed", async (req, res) => {
1698
+ try {
1699
+ const { apiKey } = requireAzureBaseConfig();
1700
+ const deployment = resolveAzureDeployment(req.body?.model, AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT, "embeddings");
1701
+ const { provider, model, ...cleanBody } = req.body ?? {};
1702
+ const requestBody = { ...cleanBody };
1703
+
1704
+ const response = await fetch(buildAzureDeploymentUrl(deployment, "embeddings"), {
1705
+ method: "POST",
1706
+ headers: {
1707
+ "Content-Type": "application/json",
1708
+ "api-key": apiKey
1709
+ },
1710
+ body: JSON.stringify(requestBody)
1711
+ });
1712
+
1713
+ if (!response.ok) {
1714
+ const errorText = await response.text();
1715
+ return res.status(response.status).json({
1716
+ error: \`Azure OpenAI embed failed: \${response.status}\`,
1717
+ details: errorText
1718
+ });
1719
+ }
1720
+
1721
+ const text = await response.text();
1722
+ res.setHeader('Content-Type', 'application/json');
1723
+ res.send(text);
1724
+ } catch (error) {
1725
+ const message = error instanceof Error ? error.message : String(error);
1726
+ const status = message.startsWith("Missing Azure OpenAI") ? 400 : 500;
1727
+ res.status(status).json({ error: message });
1728
+ }
1729
+ });
1730
+
966
1731
  // ============================================================================
967
1732
  // OPENAI ROUTES
968
1733
  // ============================================================================
@@ -1477,24 +2242,19 @@ app.post("/api/mcp/generate-image", async (req, res) => {
1477
2242
 
1478
2243
  app.all("/api/anthropic/*", (_req, res) => {
1479
2244
  res.status(501).json({
1480
- error: "Anthropic integration not implemented",
1481
- message: "This quickstart gateway only supports OpenAI and Ollama providers"
1482
- });
1483
- });
1484
-
1485
- app.all("/api/azure/*", (_req, res) => {
1486
- res.status(501).json({
1487
- error: "Azure OpenAI integration not implemented",
1488
- message: "This quickstart gateway only supports OpenAI and Ollama providers"
2245
+ error: "Anthropic route not implemented",
2246
+ message: "Extend the quickstart gateway if you need additional Anthropic endpoints beyond the defaults."
1489
2247
  });
1490
2248
  });
1491
2249
 
1492
2250
  const port = Number(process.env.PORT ?? ${ctx.gatewayPort});
1493
2251
  app.listen(port, () => {
1494
2252
  console.log("\u26A1 Bandit quickstart gateway ready on http://localhost:" + port);
1495
- console.log("\u{1F4E1} Supported providers: OpenAI, Ollama");
2253
+ console.log("\u{1F4E1} Supported providers: OpenAI, Azure OpenAI, Anthropic, Ollama");
1496
2254
  console.log("\u{1F517} Provider-specific routes:");
1497
2255
  console.log(" \u2022 /api/openai/* - OpenAI endpoints");
2256
+ console.log(" \u2022 /api/azure/* - Azure OpenAI endpoints");
2257
+ console.log(" \u2022 /api/anthropic/* - Anthropic endpoints");
1498
2258
  console.log(" \u2022 /api/ollama/* - Ollama endpoints");
1499
2259
  console.log(" \u2022 /api/health - Overall health check");
1500
2260
  });
@@ -1526,7 +2286,7 @@ This project was generated by the Bandit Engine CLI. It ships with a React + Vit
1526
2286
  ## \u{1F680} Next steps
1527
2287
  - \`npm install\`
1528
2288
  - \`cp .env.example .env\`
1529
- - Fill in \`OPENAI_API_KEY\` (or point \`OLLAMA_URL\` at your local server)
2289
+ - Fill in your OpenAI, Azure OpenAI, or Anthropic credentials (or point \`OLLAMA_URL\` at your local server)
1530
2290
  - \`npm run dev\`
1531
2291
 
1532
2292
  The command runs the gateway and the frontend together. Visit http://localhost:${ctx.frontendPort} to see the chat and modal in action.
@@ -1539,7 +2299,7 @@ The command runs the gateway and the frontend together. Visit http://localhost:$
1539
2299
  ## \u{1F4E6} What\u2019s inside
1540
2300
  - React + Vite 5 with Material UI theming
1541
2301
  - Bandit chat surface + modal wired via \`ChatProvider\`
1542
- - Express gateway proxying OpenAI or Ollama to keep API keys server-side
2302
+ - Express gateway proxying OpenAI, Azure OpenAI, Anthropic, or Ollama to keep API keys server-side
1543
2303
  - Friendly defaults you can evolve into your production stack
1544
2304
 
1545
2305
  Need more? Run \`npx @burtson-labs/bandit-engine create --help\` to explore additional options.
@@ -1554,8 +2314,9 @@ var createQuickstartProject = async (options) => {
1554
2314
  const packageName = normalizePackageName(rawProjectName);
1555
2315
  const projectTitle = toTitleCase(rawProjectName) || "Bandit Quickstart";
1556
2316
  await ensureWritableDirectory(resolvedDir, Boolean(options.force));
1557
- const provider = normalizeProvider(options.provider);
1558
- const promptAnswers = options.skipPrompts ? {} : await promptForMissingData({
2317
+ const skipPrompts = Boolean(options.skipPrompts);
2318
+ const provider = options.provider ? normalizeProvider(options.provider) : skipPrompts ? "openai" : await promptForProvider();
2319
+ const promptAnswers = skipPrompts ? {} : await promptForMissingData({
1559
2320
  brandingText: options.brandingText,
1560
2321
  provider
1561
2322
  });
@@ -1620,17 +2381,74 @@ var ensureWritableDirectory = async (dir, force) => {
1620
2381
  };
1621
2382
  var normalizeProvider = (value) => {
1622
2383
  const normalized = (value ?? "openai").toLowerCase();
1623
- return normalized === "ollama" ? "ollama" : "openai";
2384
+ if (normalized === "ollama") {
2385
+ return "ollama";
2386
+ }
2387
+ if (normalized === "azure" || normalized === "azure-openai" || normalized === "azureopenai") {
2388
+ return "azure";
2389
+ }
2390
+ if (normalized === "anthropic") {
2391
+ return "anthropic";
2392
+ }
2393
+ return "openai";
1624
2394
  };
1625
2395
  var inferDefaultModelId = (provider) => {
1626
- return provider === "ollama" ? "ollama:llama3.1" : "openai:gpt-4o-mini";
2396
+ if (provider === "ollama") {
2397
+ return "ollama:llama3.1";
2398
+ }
2399
+ if (provider === "azure") {
2400
+ return "azure:gpt-4o";
2401
+ }
2402
+ if (provider === "anthropic") {
2403
+ return "anthropic:claude-3-5-sonnet-latest";
2404
+ }
2405
+ return "openai:gpt-4o-mini";
1627
2406
  };
1628
2407
  var inferFallbackModelId = (provider, defaultId) => {
1629
2408
  if (provider === "ollama") {
1630
2409
  return defaultId === "ollama:llama3" ? "ollama:llama2" : "ollama:llama3";
1631
2410
  }
2411
+ if (provider === "azure") {
2412
+ return defaultId === "azure:gpt-4o-mini" ? "azure:gpt-4o" : "azure:gpt-4o-mini";
2413
+ }
2414
+ if (provider === "anthropic") {
2415
+ return defaultId === "anthropic:claude-3-5-haiku-latest" ? "anthropic:claude-3-5-sonnet-latest" : "anthropic:claude-3-5-haiku-latest";
2416
+ }
1632
2417
  return defaultId === "openai:gpt-4.1-mini" ? "openai:gpt-4o-mini" : "openai:gpt-4.1-mini";
1633
2418
  };
2419
+ var promptForProvider = async () => {
2420
+ const providerOptions = [
2421
+ { label: "OpenAI (default)", value: "openai" },
2422
+ { label: "Azure OpenAI", value: "azure" },
2423
+ { label: "Anthropic", value: "anthropic" },
2424
+ { label: "Ollama (self-hosted)", value: "ollama" }
2425
+ ];
2426
+ const messageLines = [
2427
+ "Which provider should we configure for the gateway?",
2428
+ ...providerOptions.map((option, index) => ` ${index + 1}. ${option.label}`),
2429
+ "Enter a number:"
2430
+ ];
2431
+ const onCancel = () => {
2432
+ throw new Error("Command cancelled.");
2433
+ };
2434
+ const answers = await (0, import_prompts.default)(
2435
+ {
2436
+ type: "number",
2437
+ name: "providerIndex",
2438
+ message: messageLines.join("\n"),
2439
+ initial: 1,
2440
+ validate: (input) => {
2441
+ if (!Number.isInteger(input)) {
2442
+ return "Enter a whole number.";
2443
+ }
2444
+ return input >= 1 && input <= providerOptions.length ? true : `Enter a number between 1 and ${providerOptions.length}.`;
2445
+ }
2446
+ },
2447
+ { onCancel }
2448
+ );
2449
+ const selectedIndex = typeof answers.providerIndex === "number" && answers.providerIndex >= 1 ? answers.providerIndex - 1 : 0;
2450
+ return providerOptions[selectedIndex]?.value ?? "openai";
2451
+ };
1634
2452
  var sanitizePort = (value) => {
1635
2453
  const port = Number(value);
1636
2454
  if (Number.isNaN(port) || port <= 0 || port >= 65535) {
@@ -1766,6 +2584,9 @@ var logIntro = () => {
1766
2584
  var program = new import_commander.Command();
1767
2585
  program.name("bandit").description("Bandit Engine developer utilities").version(package_default.version).showHelpAfterError();
1768
2586
  program.command("create").description("Scaffold a Bandit quickstart project with a frontend and gateway").argument("[directory]", "Relative path for your new project", "bandit-quickstart").option("-f, --force", "Overwrite the target directory if it already contains files", false).option("--branding-text <text>", "Assistant display name shown in the UI").option(
2587
+ "--provider <provider>",
2588
+ "Default gateway provider (openai, azure, anthropic, ollama)"
2589
+ ).option(
1769
2590
  "--frontend-port <port>",
1770
2591
  "Frontend dev server port (default: 5183)",
1771
2592
  (value) => parseInt(value, 10)
@@ -1780,6 +2601,7 @@ program.command("create").description("Scaffold a Bandit quickstart project with
1780
2601
  projectName,
1781
2602
  force: Boolean(cmdOptions.force),
1782
2603
  brandingText: cmdOptions.brandingText,
2604
+ provider: typeof cmdOptions.provider === "string" ? cmdOptions.provider : void 0,
1783
2605
  frontendPort: Number.isFinite(cmdOptions.frontendPort) ? cmdOptions.frontendPort : void 0,
1784
2606
  gatewayPort: Number.isFinite(cmdOptions.gatewayPort) ? cmdOptions.gatewayPort : void 0,
1785
2607
  skipPrompts