@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.
- package/dist/cli/cli.js +843 -21
- package/dist/cli/cli.js.map +1 -1
- package/docs/api_reference/classes/DebugLogger.html +11 -11
- package/docs/api_reference/classes/FeatureFlagService.html +13 -13
- package/docs/api_reference/classes/NotificationService.html +10 -10
- package/docs/api_reference/classes/StreamingTTSClient.html +9 -9
- package/docs/api_reference/classes/VectorDatabaseService.html +24 -24
- package/docs/api_reference/classes/VectorMigrationService.html +8 -8
- package/docs/api_reference/classes/VoiceService.html +2 -2
- package/docs/api_reference/enums/TTSState.html +2 -2
- package/docs/api_reference/functions/Chat.html +1 -1
- package/docs/api_reference/functions/ChatModal.html +1 -1
- package/docs/api_reference/functions/ChatProvider.html +1 -1
- package/docs/api_reference/functions/FeatureFlagProvider.html +1 -1
- package/docs/api_reference/functions/FeedbackButton.html +1 -1
- package/docs/api_reference/functions/FeedbackModal.html +1 -1
- package/docs/api_reference/functions/Management.html +1 -1
- package/docs/api_reference/functions/NotificationProvider.html +1 -1
- package/docs/api_reference/functions/SubscriptionExpiredGuard.html +1 -1
- package/docs/api_reference/functions/SubscriptionExpiredModal.html +1 -1
- package/docs/api_reference/functions/defineCustomElement.html +1 -1
- package/docs/api_reference/functions/getCriticalConfig.html +1 -1
- package/docs/api_reference/functions/getFeatureMatrix.html +1 -1
- package/docs/api_reference/functions/getStreamingTTSClient.html +1 -1
- package/docs/api_reference/functions/getSystemConstants.html +1 -1
- package/docs/api_reference/functions/getTTSState.html +1 -1
- package/docs/api_reference/functions/handleHttpError.html +1 -1
- package/docs/api_reference/functions/handleSubscriptionUpgrade.html +1 -1
- package/docs/api_reference/functions/handleValidationError.html +1 -1
- package/docs/api_reference/functions/initializeCoreSystem.html +1 -1
- package/docs/api_reference/functions/pauseTTS.html +1 -1
- package/docs/api_reference/functions/previewTierUpgrade.html +1 -1
- package/docs/api_reference/functions/resumeTTS.html +1 -1
- package/docs/api_reference/functions/showInfoNotification.html +1 -1
- package/docs/api_reference/functions/showSuccessNotification.html +1 -1
- package/docs/api_reference/functions/speakWithStreaming.html +1 -1
- package/docs/api_reference/functions/stopTTS.html +1 -1
- package/docs/api_reference/functions/syncSubscriptionWithAPI.html +1 -1
- package/docs/api_reference/functions/updateSubscriptionTier.html +1 -1
- package/docs/api_reference/functions/useFeatureFlag.html +1 -1
- package/docs/api_reference/functions/useFeatureVisibility.html +1 -1
- package/docs/api_reference/functions/useFeatures.html +1 -1
- package/docs/api_reference/functions/useGatewayHealth.html +1 -1
- package/docs/api_reference/functions/useGatewayMemory.html +1 -1
- package/docs/api_reference/functions/useGatewayModels.html +1 -1
- package/docs/api_reference/functions/useGlobalTTS.html +1 -1
- package/docs/api_reference/functions/useNotification.html +1 -1
- package/docs/api_reference/functions/useNotificationService.html +1 -1
- package/docs/api_reference/functions/useTTS.html +1 -1
- package/docs/api_reference/functions/useVectorStore.html +1 -1
- package/docs/api_reference/functions/useVoiceStore.html +2 -2
- package/docs/api_reference/functions/useVoices.html +1 -1
- package/docs/api_reference/functions/validateEnvironment.html +1 -1
- package/docs/api_reference/functions/validateSystemIntegrity.html +1 -1
- package/docs/api_reference/interfaces/AIChatRequest.html +2 -2
- package/docs/api_reference/interfaces/AIChatResponse.html +2 -2
- package/docs/api_reference/interfaces/AIGenerateRequest.html +2 -2
- package/docs/api_reference/interfaces/AIGenerateResponse.html +2 -2
- package/docs/api_reference/interfaces/AIMessage.html +2 -2
- package/docs/api_reference/interfaces/AIModel.html +2 -2
- package/docs/api_reference/interfaces/AIProviderConfig.html +2 -2
- package/docs/api_reference/interfaces/ChatConfig.html +3 -3
- package/docs/api_reference/interfaces/ChatModalProps.html +3 -3
- package/docs/api_reference/interfaces/CreateMemoryOptions.html +2 -2
- package/docs/api_reference/interfaces/FeatureEvaluation.html +7 -7
- package/docs/api_reference/interfaces/FeatureFlagConfig.html +9 -9
- package/docs/api_reference/interfaces/FeatureFlagContextValue.html +8 -8
- package/docs/api_reference/interfaces/FeatureFlagProviderProps.html +2 -2
- package/docs/api_reference/interfaces/FeedbackButtonProps.html +10 -10
- package/docs/api_reference/interfaces/FeedbackCategories.html +2 -2
- package/docs/api_reference/interfaces/FeedbackModalProps.html +2 -2
- package/docs/api_reference/interfaces/FeedbackPriorities.html +2 -2
- package/docs/api_reference/interfaces/FeedbackRequest.html +2 -2
- package/docs/api_reference/interfaces/FeedbackResponse.html +2 -2
- package/docs/api_reference/interfaces/FileUploadResult.html +2 -2
- package/docs/api_reference/interfaces/GatewayChatRequest.html +2 -2
- package/docs/api_reference/interfaces/GatewayChatResponse.html +2 -2
- package/docs/api_reference/interfaces/GatewayContract.html +2 -2
- package/docs/api_reference/interfaces/GatewayGenerateRequest.html +2 -2
- package/docs/api_reference/interfaces/GatewayGenerateResponse.html +2 -2
- package/docs/api_reference/interfaces/GatewayHealthResponse.html +2 -2
- package/docs/api_reference/interfaces/GatewayMemoryRecord.html +2 -2
- package/docs/api_reference/interfaces/GatewayMemoryResponse.html +2 -2
- package/docs/api_reference/interfaces/GatewayMessage.html +2 -2
- package/docs/api_reference/interfaces/GatewayMessageContent.html +2 -2
- package/docs/api_reference/interfaces/GatewayModel.html +2 -2
- package/docs/api_reference/interfaces/GatewayModelsResponse.html +2 -2
- package/docs/api_reference/interfaces/MemorySearchFilters.html +2 -2
- package/docs/api_reference/interfaces/MigrationProgress.html +2 -2
- package/docs/api_reference/interfaces/MigrationStatus.html +2 -2
- package/docs/api_reference/interfaces/NotificationConfig.html +2 -2
- package/docs/api_reference/interfaces/NotificationContextType.html +2 -2
- package/docs/api_reference/interfaces/NotificationProviderProps.html +2 -2
- package/docs/api_reference/interfaces/PackageSettings.html +2 -2
- package/docs/api_reference/interfaces/SearchOptions.html +2 -2
- package/docs/api_reference/interfaces/SearchResult.html +2 -2
- package/docs/api_reference/interfaces/SubscriptionExpiredGuardProps.html +2 -2
- package/docs/api_reference/interfaces/SubscriptionExpiredModalProps.html +2 -2
- package/docs/api_reference/interfaces/TTSOptions.html +2 -2
- package/docs/api_reference/interfaces/TTSProgress.html +2 -2
- package/docs/api_reference/interfaces/TrialUsage.html +2 -2
- package/docs/api_reference/interfaces/UploadRequest.html +3 -3
- package/docs/api_reference/interfaces/UseTTSReturn.html +2 -2
- package/docs/api_reference/interfaces/VectorDocument.html +2 -2
- package/docs/api_reference/interfaces/VectorMemory.html +2 -2
- package/docs/api_reference/interfaces/VectorMemoryMetadata.html +2 -2
- package/docs/api_reference/interfaces/VectorStoreStatus.html +2 -2
- package/docs/api_reference/interfaces/VoiceModelsResponse.html +2 -2
- package/docs/api_reference/interfaces/VoiceState.html +2 -2
- package/docs/api_reference/types/FeatureKey.html +1 -1
- package/docs/api_reference/types/FeatureMatrix.html +1 -1
- package/docs/api_reference/types/GatewayQueryOptions.html +1 -1
- package/docs/api_reference/types/LogContext.html +1 -1
- package/docs/api_reference/types/SubscriptionTier.html +1 -1
- package/docs/api_reference/variables/DEFAULT_TIER_FEATURES.html +1 -1
- package/docs/api_reference/variables/FeatureFlagContext.html +1 -1
- package/docs/api_reference/variables/OSS_DEFAULT_FEATURES.html +1 -1
- package/docs/api_reference/variables/SYSTEM_FLAGS.html +1 -1
- package/docs/api_reference/variables/authenticationService.html +1 -1
- package/docs/api_reference/variables/debugLogger-1.html +1 -1
- package/docs/api_reference/variables/featureFlagService-1.html +1 -1
- package/docs/api_reference/variables/notificationService-1.html +1 -1
- package/docs/api_reference/variables/vectorDatabaseService-1.html +1 -1
- package/docs/api_reference/variables/vectorMigrationService-1.html +1 -1
- package/docs/api_reference/variables/voiceService-1.html +1 -1
- 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
|
+
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/
|
|
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
|
|
1481
|
-
message: "
|
|
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
|
|
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
|
|
1558
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|