@burtson-labs/bandit-engine 2.0.8
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/LICENSE +43 -0
- package/README.md +1235 -0
- package/dist/aiProviderStore-YWJHSWFA.mjs +9 -0
- package/dist/aiProviderStore-YWJHSWFA.mjs.map +1 -0
- package/dist/chat-QXB526NZ.mjs +11 -0
- package/dist/chat-QXB526NZ.mjs.map +1 -0
- package/dist/chunk-AVC6IZJQ.mjs +2157 -0
- package/dist/chunk-AVC6IZJQ.mjs.map +1 -0
- package/dist/chunk-BIPELT57.mjs +24183 -0
- package/dist/chunk-BIPELT57.mjs.map +1 -0
- package/dist/chunk-BJTO5JO5.mjs +11 -0
- package/dist/chunk-BJTO5JO5.mjs.map +1 -0
- package/dist/chunk-IXIM7BNO.mjs +39 -0
- package/dist/chunk-IXIM7BNO.mjs.map +1 -0
- package/dist/chunk-KCI46M23.mjs +106 -0
- package/dist/chunk-KCI46M23.mjs.map +1 -0
- package/dist/chunk-WYS5CZVG.mjs +843 -0
- package/dist/chunk-WYS5CZVG.mjs.map +1 -0
- package/dist/cli/cli.js +1808 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/index.d.mts +1636 -0
- package/dist/index.d.ts +1636 -0
- package/dist/index.js +40601 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +12477 -0
- package/dist/index.mjs.map +1 -0
- package/dist/memoryUtils-33TZKHSQ.mjs +223 -0
- package/dist/memoryUtils-33TZKHSQ.mjs.map +1 -0
- package/dist/modelStore-Y3LZWRQC.mjs +9 -0
- package/dist/modelStore-Y3LZWRQC.mjs.map +1 -0
- package/dist/shared/custom-element.js +73 -0
- package/dist/shared/custom-element.js.map +1 -0
- package/dist/shared/custom-element.mjs +8 -0
- package/dist/shared/custom-element.mjs.map +1 -0
- package/docs/00_intro.md +26 -0
- package/docs/01_quickstart.md +54 -0
- package/docs/02_gateway_api.md +64 -0
- package/docs/03_provider_integration.md +38 -0
- package/docs/04_local_dev.md +43 -0
- package/docs/05_cli_quickstart.md +89 -0
- package/docs/05_contributing.md +33 -0
- package/docs/06_busl_licensing.md +25 -0
- package/docs/README.md +9 -0
- package/docs/api_reference/.nojekyll +1 -0
- package/docs/api_reference/assets/highlight.css +141 -0
- package/docs/api_reference/assets/icons.js +18 -0
- package/docs/api_reference/assets/icons.svg +1 -0
- package/docs/api_reference/assets/main.js +60 -0
- package/docs/api_reference/assets/navigation.js +1 -0
- package/docs/api_reference/assets/search.js +1 -0
- package/docs/api_reference/assets/style.css +1493 -0
- package/docs/api_reference/classes/DebugLogger.html +29 -0
- package/docs/api_reference/classes/FeatureFlagService.html +28 -0
- package/docs/api_reference/classes/NotificationService.html +21 -0
- package/docs/api_reference/classes/StreamingTTSClient.html +19 -0
- package/docs/api_reference/classes/VectorDatabaseService.html +63 -0
- package/docs/api_reference/classes/VectorMigrationService.html +27 -0
- package/docs/api_reference/classes/VoiceService.html +4 -0
- package/docs/api_reference/enums/TTSState.html +6 -0
- package/docs/api_reference/functions/Chat.html +1 -0
- package/docs/api_reference/functions/ChatModal.html +29 -0
- package/docs/api_reference/functions/ChatProvider.html +29 -0
- package/docs/api_reference/functions/FeatureFlagProvider.html +30 -0
- package/docs/api_reference/functions/FeedbackButton.html +29 -0
- package/docs/api_reference/functions/FeedbackModal.html +29 -0
- package/docs/api_reference/functions/Management.html +1 -0
- package/docs/api_reference/functions/NotificationProvider.html +29 -0
- package/docs/api_reference/functions/SubscriptionExpiredGuard.html +31 -0
- package/docs/api_reference/functions/SubscriptionExpiredModal.html +31 -0
- package/docs/api_reference/functions/defineCustomElement.html +1 -0
- package/docs/api_reference/functions/getCriticalConfig.html +1 -0
- package/docs/api_reference/functions/getFeatureMatrix.html +1 -0
- package/docs/api_reference/functions/getStreamingTTSClient.html +1 -0
- package/docs/api_reference/functions/getSystemConstants.html +1 -0
- package/docs/api_reference/functions/getTTSState.html +1 -0
- package/docs/api_reference/functions/handleHttpError.html +2 -0
- package/docs/api_reference/functions/handleSubscriptionUpgrade.html +1 -0
- package/docs/api_reference/functions/handleValidationError.html +2 -0
- package/docs/api_reference/functions/initializeCoreSystem.html +1 -0
- package/docs/api_reference/functions/pauseTTS.html +1 -0
- package/docs/api_reference/functions/previewTierUpgrade.html +1 -0
- package/docs/api_reference/functions/resumeTTS.html +1 -0
- package/docs/api_reference/functions/showInfoNotification.html +2 -0
- package/docs/api_reference/functions/showSuccessNotification.html +2 -0
- package/docs/api_reference/functions/speakWithStreaming.html +1 -0
- package/docs/api_reference/functions/stopTTS.html +1 -0
- package/docs/api_reference/functions/syncSubscriptionWithAPI.html +1 -0
- package/docs/api_reference/functions/updateSubscriptionTier.html +2 -0
- package/docs/api_reference/functions/useFeatureFlag.html +2 -0
- package/docs/api_reference/functions/useFeatureVisibility.html +2 -0
- package/docs/api_reference/functions/useFeatures.html +2 -0
- package/docs/api_reference/functions/useGatewayHealth.html +1 -0
- package/docs/api_reference/functions/useGatewayMemory.html +1 -0
- package/docs/api_reference/functions/useGatewayModels.html +1 -0
- package/docs/api_reference/functions/useGlobalTTS.html +2 -0
- package/docs/api_reference/functions/useNotification.html +1 -0
- package/docs/api_reference/functions/useNotificationService.html +6 -0
- package/docs/api_reference/functions/useTTS.html +2 -0
- package/docs/api_reference/functions/useVectorStore.html +13 -0
- package/docs/api_reference/functions/useVoiceStore.html +8 -0
- package/docs/api_reference/functions/useVoices.html +3 -0
- package/docs/api_reference/functions/validateEnvironment.html +1 -0
- package/docs/api_reference/functions/validateSystemIntegrity.html +1 -0
- package/docs/api_reference/index.html +756 -0
- package/docs/api_reference/interfaces/AIChatRequest.html +8 -0
- package/docs/api_reference/interfaces/AIChatResponse.html +3 -0
- package/docs/api_reference/interfaces/AIGenerateRequest.html +5 -0
- package/docs/api_reference/interfaces/AIGenerateResponse.html +3 -0
- package/docs/api_reference/interfaces/AIMessage.html +4 -0
- package/docs/api_reference/interfaces/AIModel.html +6 -0
- package/docs/api_reference/interfaces/AIProviderConfig.html +9 -0
- package/docs/api_reference/interfaces/ChatConfig.html +5 -0
- package/docs/api_reference/interfaces/CreateMemoryOptions.html +7 -0
- package/docs/api_reference/interfaces/FeatureEvaluation.html +14 -0
- package/docs/api_reference/interfaces/FeatureFlagConfig.html +18 -0
- package/docs/api_reference/interfaces/FeatureFlagContextValue.html +16 -0
- package/docs/api_reference/interfaces/FeatureFlagProviderProps.html +4 -0
- package/docs/api_reference/interfaces/FeedbackButtonProps.html +19 -0
- package/docs/api_reference/interfaces/FeedbackCategories.html +6 -0
- package/docs/api_reference/interfaces/FeedbackModalProps.html +4 -0
- package/docs/api_reference/interfaces/FeedbackPriorities.html +5 -0
- package/docs/api_reference/interfaces/FeedbackRequest.html +12 -0
- package/docs/api_reference/interfaces/FeedbackResponse.html +7 -0
- package/docs/api_reference/interfaces/FileUploadResult.html +4 -0
- package/docs/api_reference/interfaces/GatewayChatRequest.html +12 -0
- package/docs/api_reference/interfaces/GatewayChatResponse.html +7 -0
- package/docs/api_reference/interfaces/GatewayContract.html +4 -0
- package/docs/api_reference/interfaces/GatewayGenerateRequest.html +8 -0
- package/docs/api_reference/interfaces/GatewayGenerateResponse.html +12 -0
- package/docs/api_reference/interfaces/GatewayHealthResponse.html +5 -0
- package/docs/api_reference/interfaces/GatewayMemoryRecord.html +7 -0
- package/docs/api_reference/interfaces/GatewayMemoryResponse.html +4 -0
- package/docs/api_reference/interfaces/GatewayMessage.html +5 -0
- package/docs/api_reference/interfaces/GatewayMessageContent.html +4 -0
- package/docs/api_reference/interfaces/GatewayModel.html +9 -0
- package/docs/api_reference/interfaces/GatewayModelsResponse.html +2 -0
- package/docs/api_reference/interfaces/MemorySearchFilters.html +5 -0
- package/docs/api_reference/interfaces/MigrationProgress.html +6 -0
- package/docs/api_reference/interfaces/MigrationStatus.html +7 -0
- package/docs/api_reference/interfaces/NotificationConfig.html +5 -0
- package/docs/api_reference/interfaces/NotificationContextType.html +6 -0
- package/docs/api_reference/interfaces/NotificationProviderProps.html +4 -0
- package/docs/api_reference/interfaces/PackageSettings.html +11 -0
- package/docs/api_reference/interfaces/SearchOptions.html +6 -0
- package/docs/api_reference/interfaces/SearchResult.html +5 -0
- package/docs/api_reference/interfaces/SubscriptionExpiredGuardProps.html +7 -0
- package/docs/api_reference/interfaces/SubscriptionExpiredModalProps.html +6 -0
- package/docs/api_reference/interfaces/TTSOptions.html +5 -0
- package/docs/api_reference/interfaces/TTSProgress.html +6 -0
- package/docs/api_reference/interfaces/TrialUsage.html +6 -0
- package/docs/api_reference/interfaces/UploadRequest.html +9 -0
- package/docs/api_reference/interfaces/UseTTSReturn.html +14 -0
- package/docs/api_reference/interfaces/VectorDocument.html +11 -0
- package/docs/api_reference/interfaces/VectorMemory.html +12 -0
- package/docs/api_reference/interfaces/VectorMemoryMetadata.html +7 -0
- package/docs/api_reference/interfaces/VectorStoreStatus.html +7 -0
- package/docs/api_reference/interfaces/VoiceModelsResponse.html +4 -0
- package/docs/api_reference/interfaces/VoiceState.html +12 -0
- package/docs/api_reference/media/02_gateway_api.md +64 -0
- package/docs/api_reference/media/05_cli_quickstart.md +89 -0
- package/docs/api_reference/media/LICENSE +43 -0
- package/docs/api_reference/media/PRE-PUSH-CHECKLIST.md +10 -0
- package/docs/api_reference/media/PROTECTION-NOTICE.md +60 -0
- package/docs/api_reference/media/PROTECTION-README.md +41 -0
- package/docs/api_reference/media/README-1.md +23 -0
- package/docs/api_reference/media/README.md +9 -0
- package/docs/api_reference/modules.html +123 -0
- package/docs/api_reference/types/FeatureKey.html +2 -0
- package/docs/api_reference/types/FeatureMatrix.html +2 -0
- package/docs/api_reference/types/GatewayQueryOptions.html +1 -0
- package/docs/api_reference/types/LogContext.html +14 -0
- package/docs/api_reference/types/SubscriptionTier.html +2 -0
- package/docs/api_reference/variables/DEFAULT_TIER_FEATURES.html +2 -0
- package/docs/api_reference/variables/FeatureFlagContext.html +1 -0
- package/docs/api_reference/variables/OSS_DEFAULT_FEATURES.html +2 -0
- package/docs/api_reference/variables/SYSTEM_FLAGS.html +1 -0
- package/docs/api_reference/variables/authenticationService.html +1 -0
- package/docs/api_reference/variables/debugLogger-1.html +1 -0
- package/docs/api_reference/variables/featureFlagService-1.html +2 -0
- package/docs/api_reference/variables/notificationService-1.html +1 -0
- package/docs/api_reference/variables/vectorDatabaseService-1.html +1 -0
- package/docs/api_reference/variables/vectorMigrationService-1.html +1 -0
- package/docs/api_reference/variables/voiceService-1.html +1 -0
- package/package.json +103 -0
|
@@ -0,0 +1,2157 @@
|
|
|
1
|
+
import {
|
|
2
|
+
debugLogger
|
|
3
|
+
} from "./chunk-KCI46M23.mjs";
|
|
4
|
+
|
|
5
|
+
// src/store/aiProviderStore.ts
|
|
6
|
+
import { create } from "zustand";
|
|
7
|
+
|
|
8
|
+
// src/services/ai-provider/providers/ollama.provider.ts
|
|
9
|
+
import { map as map2 } from "rxjs";
|
|
10
|
+
|
|
11
|
+
// src/services/ollama/ollama.service.ts
|
|
12
|
+
import { catchError, from, lastValueFrom, map, Observable, of, shareReplay, switchMap, throwError, timeout } from "rxjs";
|
|
13
|
+
var handleError = () => (obs) => obs.pipe(
|
|
14
|
+
switchMap(
|
|
15
|
+
(response) => response.ok ? of(response) : throwError(() => new Error(`Request failed: ${response.status} ${response.statusText}`))
|
|
16
|
+
)
|
|
17
|
+
);
|
|
18
|
+
var parseResponseBody = async (response, responseType) => {
|
|
19
|
+
if (responseType === "text") {
|
|
20
|
+
return await response.text();
|
|
21
|
+
}
|
|
22
|
+
return await response.json();
|
|
23
|
+
};
|
|
24
|
+
var OllamaService = class {
|
|
25
|
+
constructor(_baseUrl, _tokenFactory) {
|
|
26
|
+
this._baseUrl = _baseUrl;
|
|
27
|
+
this._tokenFactory = _tokenFactory;
|
|
28
|
+
if (!this._baseUrl) {
|
|
29
|
+
this._baseUrl = "http://localhost:11434";
|
|
30
|
+
debugLogger.warn(`No base URL provided, using default: ${this._baseUrl}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
_tryGatewayWithTimeout(args) {
|
|
34
|
+
const { url, responseType, timeoutMs } = args;
|
|
35
|
+
const source = this._get(url, responseType);
|
|
36
|
+
const mapped = source.pipe(
|
|
37
|
+
catchError((e) => e?.message.includes("401") ? of(true) : of(false)),
|
|
38
|
+
map(() => true),
|
|
39
|
+
timeout(timeoutMs)
|
|
40
|
+
);
|
|
41
|
+
return mapped;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validates the availability of the service at the given base URL.
|
|
45
|
+
* @param fallbackUrl The fallback URL to try if the base URL is not available.
|
|
46
|
+
* @returns An object containing the URL and availability status.
|
|
47
|
+
*/
|
|
48
|
+
async validateServiceAvailability(args) {
|
|
49
|
+
const { fallbackUrl, timeoutMs } = args;
|
|
50
|
+
const responseType = "text";
|
|
51
|
+
const availablility = {
|
|
52
|
+
url: "",
|
|
53
|
+
isAvailable: false
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
debugLogger.debug(`Validating service availability at ${this._baseUrl}`);
|
|
57
|
+
availablility.url = this._baseUrl;
|
|
58
|
+
availablility.isAvailable = await lastValueFrom(
|
|
59
|
+
this._tryGatewayWithTimeout({
|
|
60
|
+
url: availablility.url,
|
|
61
|
+
responseType,
|
|
62
|
+
timeoutMs
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
if (!availablility.isAvailable) {
|
|
66
|
+
throw new Error(`Service not available at ${this._baseUrl}`);
|
|
67
|
+
}
|
|
68
|
+
return availablility;
|
|
69
|
+
} catch (e) {
|
|
70
|
+
debugLogger.warn(`Service not available at ${this._baseUrl}, trying fallback URL: ${fallbackUrl}`);
|
|
71
|
+
try {
|
|
72
|
+
availablility.url = fallbackUrl;
|
|
73
|
+
availablility.isAvailable = await lastValueFrom(
|
|
74
|
+
this._tryGatewayWithTimeout({
|
|
75
|
+
url: availablility.url,
|
|
76
|
+
responseType,
|
|
77
|
+
timeoutMs
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
if (!availablility.isAvailable) {
|
|
81
|
+
throw new Error(`Service not available at ${fallbackUrl}`);
|
|
82
|
+
}
|
|
83
|
+
this._baseUrl = fallbackUrl;
|
|
84
|
+
return availablility;
|
|
85
|
+
} catch (e2) {
|
|
86
|
+
debugLogger.error(`Service not available at fallback URL: ${fallbackUrl}`);
|
|
87
|
+
throw e2;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
generate(request) {
|
|
92
|
+
const url = `${this._baseUrl}/api/generate`;
|
|
93
|
+
return new Observable((observer) => {
|
|
94
|
+
const task = fetch(url, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: this._getHeaders(),
|
|
97
|
+
body: JSON.stringify({ ...request, stream: request.stream === false ? false : true })
|
|
98
|
+
});
|
|
99
|
+
task.then((response) => {
|
|
100
|
+
this._throwErrorIfNotOk(url, response);
|
|
101
|
+
const reader = response.body?.getReader();
|
|
102
|
+
const decoder = new TextDecoder();
|
|
103
|
+
let buffer = "";
|
|
104
|
+
const read = () => {
|
|
105
|
+
reader?.read().then(({ done, value }) => {
|
|
106
|
+
if (done) {
|
|
107
|
+
if (buffer.trim() !== "") {
|
|
108
|
+
try {
|
|
109
|
+
observer.next(JSON.parse(buffer));
|
|
110
|
+
} catch (err) {
|
|
111
|
+
observer.error(err);
|
|
112
|
+
debugLogger.error("Final chunk parsing error (generate):", { buffer });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
observer.complete();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
buffer += decoder.decode(value, { stream: true });
|
|
119
|
+
const lines = buffer.split("\n");
|
|
120
|
+
buffer = lines.pop() ?? "";
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
if (line.trim()) {
|
|
123
|
+
try {
|
|
124
|
+
observer.next(JSON.parse(line));
|
|
125
|
+
} catch (err) {
|
|
126
|
+
observer.error(err);
|
|
127
|
+
debugLogger.error("Error parsing JSON line (generate):", { line });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
read();
|
|
132
|
+
}).catch((err) => observer.error(err));
|
|
133
|
+
};
|
|
134
|
+
read();
|
|
135
|
+
}).catch((err) => observer.error(err));
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// generate(request: GenerateRequest): Observable<GenerateResponse> {
|
|
139
|
+
// const url = `${this._baseUrl}/api/generate`;
|
|
140
|
+
// return this._post<GenerateRequest, GenerateResponse>(url, request);
|
|
141
|
+
// }
|
|
142
|
+
chat(request) {
|
|
143
|
+
const url = `${this._baseUrl}/api/chat`;
|
|
144
|
+
return new Observable((observer) => {
|
|
145
|
+
const task = fetch(url, {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: this._getHeaders(),
|
|
148
|
+
body: JSON.stringify({ ...request, stream: true })
|
|
149
|
+
});
|
|
150
|
+
task.then((response) => {
|
|
151
|
+
this._throwErrorIfNotOk(url, response);
|
|
152
|
+
const reader = response.body?.getReader();
|
|
153
|
+
const decoder = new TextDecoder();
|
|
154
|
+
let buffer = "";
|
|
155
|
+
const read = () => {
|
|
156
|
+
reader?.read().then(({ done, value }) => {
|
|
157
|
+
if (done) {
|
|
158
|
+
if (buffer.trim() !== "") {
|
|
159
|
+
try {
|
|
160
|
+
observer.next(JSON.parse(buffer));
|
|
161
|
+
} catch (err) {
|
|
162
|
+
observer.error(err);
|
|
163
|
+
debugLogger.error("Final chunk parsing error (chat):", { buffer });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
observer.complete();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
buffer += decoder.decode(value, { stream: true });
|
|
170
|
+
const lines = buffer.split("\n");
|
|
171
|
+
buffer = lines.pop() ?? "";
|
|
172
|
+
for (const line of lines) {
|
|
173
|
+
if (line.trim()) {
|
|
174
|
+
try {
|
|
175
|
+
observer.next(JSON.parse(line));
|
|
176
|
+
} catch (err) {
|
|
177
|
+
observer.error(err);
|
|
178
|
+
debugLogger.error("Error parsing JSON line (chat):", { line });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
read();
|
|
183
|
+
}).catch((err) => observer.error(err));
|
|
184
|
+
};
|
|
185
|
+
read();
|
|
186
|
+
}).catch((err) => observer.error(err));
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
listModels() {
|
|
190
|
+
const url = `${this._baseUrl}/api/tags`;
|
|
191
|
+
const response = this._get(url);
|
|
192
|
+
const result = response.pipe(
|
|
193
|
+
map((data) => data.models),
|
|
194
|
+
shareReplay(1)
|
|
195
|
+
);
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
_get(url, responseType = "json") {
|
|
199
|
+
const requestInit = {
|
|
200
|
+
method: "GET",
|
|
201
|
+
headers: this._getHeaders()
|
|
202
|
+
};
|
|
203
|
+
const response = from(fetch(url, requestInit));
|
|
204
|
+
const handleFetchError = response.pipe(handleError());
|
|
205
|
+
const data = handleFetchError.pipe(switchMap((res) => from(parseResponseBody(res, responseType))));
|
|
206
|
+
const result = data.pipe(shareReplay(1));
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
_post(url, body) {
|
|
210
|
+
const response = from(fetch(url, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
headers: this._getHeaders(),
|
|
213
|
+
body: JSON.stringify(body)
|
|
214
|
+
}));
|
|
215
|
+
const handleFetchError = response.pipe(handleError());
|
|
216
|
+
const json = handleFetchError.pipe(switchMap((res) => from(parseResponseBody(res, "json"))));
|
|
217
|
+
const result = json.pipe(shareReplay(1));
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
_throwErrorIfNotOk(url, response) {
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
throw new Error(`POST ${url} failed: ${response.status} ${response.statusText}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
_getHeaders() {
|
|
226
|
+
const token = this._tokenFactory();
|
|
227
|
+
if (!token) {
|
|
228
|
+
debugLogger.warn("OllamaService: No token found, using empty string for Authorization header");
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
"Content-Type": "application/json",
|
|
232
|
+
"Authorization": `Bearer ${token || ""}`
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// src/services/ai-provider/providers/deprecated.ts
|
|
238
|
+
function deprecatedOpenAIProvider() {
|
|
239
|
+
debugLogger.warn('\u26A0\uFE0F DEPRECATION WARNING: Direct OpenAI provider is deprecated. Please migrate to GatewayProvider with provider: "openai". See GATEWAY_MIGRATION_GUIDE.md for details.');
|
|
240
|
+
}
|
|
241
|
+
function deprecatedAzureOpenAIProvider() {
|
|
242
|
+
debugLogger.warn('\u26A0\uFE0F DEPRECATION WARNING: Direct Azure OpenAI provider is deprecated. Please migrate to GatewayProvider with provider: "azure-openai". See GATEWAY_MIGRATION_GUIDE.md for details.');
|
|
243
|
+
}
|
|
244
|
+
function deprecatedAnthropicProvider() {
|
|
245
|
+
debugLogger.warn('\u26A0\uFE0F DEPRECATION WARNING: Direct Anthropic provider is deprecated. Please migrate to GatewayProvider with provider: "anthropic". See GATEWAY_MIGRATION_GUIDE.md for details.');
|
|
246
|
+
}
|
|
247
|
+
function deprecatedOllamaProvider() {
|
|
248
|
+
debugLogger.info('\u2139\uFE0F INFO: Direct Ollama provider is available for local development. For production environments, consider using GatewayProvider with provider: "ollama" for enhanced management. See GATEWAY_MIGRATION_GUIDE.md for details.');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/services/ai-provider/providers/ollama.provider.ts
|
|
252
|
+
var OllamaProvider = class {
|
|
253
|
+
ollamaService;
|
|
254
|
+
config;
|
|
255
|
+
constructor(config) {
|
|
256
|
+
deprecatedOllamaProvider();
|
|
257
|
+
this.config = config;
|
|
258
|
+
const baseUrl = config.baseUrl || "http://localhost:11434";
|
|
259
|
+
debugLogger.info("OllamaProvider: Constructor", {
|
|
260
|
+
configBaseUrl: config.baseUrl,
|
|
261
|
+
finalBaseUrl: baseUrl,
|
|
262
|
+
hasTokenFactory: !!config.tokenFactory
|
|
263
|
+
});
|
|
264
|
+
this.ollamaService = new OllamaService(
|
|
265
|
+
baseUrl,
|
|
266
|
+
config.tokenFactory || (() => null)
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
chat(request) {
|
|
270
|
+
const ollamaRequest = {
|
|
271
|
+
model: request.model,
|
|
272
|
+
messages: request.messages.map((msg) => ({
|
|
273
|
+
role: msg.role,
|
|
274
|
+
content: msg.content
|
|
275
|
+
})),
|
|
276
|
+
stream: request.stream,
|
|
277
|
+
options: request.options,
|
|
278
|
+
images: request.images
|
|
279
|
+
};
|
|
280
|
+
return this.ollamaService.chat(ollamaRequest).pipe(
|
|
281
|
+
map2((response) => ({
|
|
282
|
+
message: {
|
|
283
|
+
content: response.message.content,
|
|
284
|
+
role: "assistant"
|
|
285
|
+
},
|
|
286
|
+
done: response.done
|
|
287
|
+
}))
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
generate(request) {
|
|
291
|
+
const ollamaRequest = {
|
|
292
|
+
model: request.model,
|
|
293
|
+
prompt: request.prompt,
|
|
294
|
+
stream: request.stream,
|
|
295
|
+
options: request.options
|
|
296
|
+
};
|
|
297
|
+
return this.ollamaService.generate(ollamaRequest).pipe(
|
|
298
|
+
map2((response) => ({
|
|
299
|
+
response: response.response,
|
|
300
|
+
done: response.done
|
|
301
|
+
}))
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
listModels() {
|
|
305
|
+
return this.ollamaService.listModels().pipe(
|
|
306
|
+
map2((models) => models.map((model) => ({
|
|
307
|
+
name: model.name,
|
|
308
|
+
size: model.size,
|
|
309
|
+
details: model.details,
|
|
310
|
+
digest: model.digest,
|
|
311
|
+
modified_at: model.modified_at
|
|
312
|
+
})))
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
async validateServiceAvailability(args) {
|
|
316
|
+
return this.ollamaService.validateServiceAvailability({
|
|
317
|
+
fallbackUrl: args.fallbackUrl || "",
|
|
318
|
+
timeoutMs: args.timeoutMs
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
getProviderType() {
|
|
322
|
+
return "ollama" /* OLLAMA */;
|
|
323
|
+
}
|
|
324
|
+
getConfig() {
|
|
325
|
+
return this.config;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// src/services/ai-provider/providers/openai.provider.ts
|
|
330
|
+
import { Observable as Observable3, from as from2, switchMap as switchMap2, map as map3, throwError as throwError2 } from "rxjs";
|
|
331
|
+
var OpenAIProvider = class {
|
|
332
|
+
config;
|
|
333
|
+
baseUrl;
|
|
334
|
+
constructor(config) {
|
|
335
|
+
deprecatedOpenAIProvider();
|
|
336
|
+
this.config = config;
|
|
337
|
+
this.baseUrl = config.baseUrl || "https://api.openai.com/v1";
|
|
338
|
+
}
|
|
339
|
+
chat(request) {
|
|
340
|
+
const url = `${this.baseUrl}/chat/completions`;
|
|
341
|
+
const payload = {
|
|
342
|
+
model: request.model,
|
|
343
|
+
messages: request.messages,
|
|
344
|
+
stream: Boolean(request.stream),
|
|
345
|
+
temperature: request.temperature,
|
|
346
|
+
max_tokens: request.maxTokens
|
|
347
|
+
};
|
|
348
|
+
if (request.stream) {
|
|
349
|
+
return this.streamChatRequest(url, payload);
|
|
350
|
+
} else {
|
|
351
|
+
return this.nonStreamChatRequest(url, payload);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
generate(request) {
|
|
355
|
+
const chatRequest = {
|
|
356
|
+
model: request.model,
|
|
357
|
+
messages: [{ role: "user", content: request.prompt }],
|
|
358
|
+
stream: request.stream,
|
|
359
|
+
options: request.options
|
|
360
|
+
};
|
|
361
|
+
return this.chat(chatRequest).pipe(
|
|
362
|
+
map3((response) => ({
|
|
363
|
+
response: response.message.content,
|
|
364
|
+
done: response.done
|
|
365
|
+
}))
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
listModels() {
|
|
369
|
+
const url = `${this.baseUrl}/models`;
|
|
370
|
+
return from2(fetch(url, {
|
|
371
|
+
headers: this.getHeaders()
|
|
372
|
+
})).pipe(
|
|
373
|
+
switchMap2((response) => {
|
|
374
|
+
if (!response.ok) {
|
|
375
|
+
return throwError2(() => new Error(`Failed to list models: ${response.status}`));
|
|
376
|
+
}
|
|
377
|
+
return from2(response.json());
|
|
378
|
+
}),
|
|
379
|
+
map3(
|
|
380
|
+
(data) => data.data.map((model) => ({
|
|
381
|
+
name: model.id,
|
|
382
|
+
details: {
|
|
383
|
+
format: "openai",
|
|
384
|
+
family: model.object
|
|
385
|
+
}
|
|
386
|
+
}))
|
|
387
|
+
)
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
async validateServiceAvailability(args) {
|
|
391
|
+
try {
|
|
392
|
+
const controller = new AbortController();
|
|
393
|
+
const timeoutId = setTimeout(() => controller.abort(), args.timeoutMs);
|
|
394
|
+
const response = await fetch(`${this.baseUrl}/models`, {
|
|
395
|
+
headers: this.getHeaders(),
|
|
396
|
+
signal: controller.signal
|
|
397
|
+
});
|
|
398
|
+
clearTimeout(timeoutId);
|
|
399
|
+
return {
|
|
400
|
+
url: this.baseUrl,
|
|
401
|
+
isAvailable: response.ok
|
|
402
|
+
};
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (args.fallbackUrl) {
|
|
405
|
+
try {
|
|
406
|
+
const controller = new AbortController();
|
|
407
|
+
const timeoutId = setTimeout(() => controller.abort(), args.timeoutMs);
|
|
408
|
+
const response = await fetch(`${args.fallbackUrl}/models`, {
|
|
409
|
+
headers: this.getHeaders(),
|
|
410
|
+
signal: controller.signal
|
|
411
|
+
});
|
|
412
|
+
clearTimeout(timeoutId);
|
|
413
|
+
if (response.ok) {
|
|
414
|
+
this.baseUrl = args.fallbackUrl;
|
|
415
|
+
return {
|
|
416
|
+
url: args.fallbackUrl,
|
|
417
|
+
isAvailable: true
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
} catch (fallbackError) {
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
url: this.baseUrl,
|
|
425
|
+
isAvailable: false
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
getProviderType() {
|
|
430
|
+
return "openai" /* OPENAI */;
|
|
431
|
+
}
|
|
432
|
+
getConfig() {
|
|
433
|
+
return this.config;
|
|
434
|
+
}
|
|
435
|
+
streamChatRequest(url, payload) {
|
|
436
|
+
return new Observable3((observer) => {
|
|
437
|
+
const task = fetch(url, {
|
|
438
|
+
method: "POST",
|
|
439
|
+
headers: {
|
|
440
|
+
...this.getHeaders(),
|
|
441
|
+
"Content-Type": "application/json"
|
|
442
|
+
},
|
|
443
|
+
body: JSON.stringify(payload)
|
|
444
|
+
});
|
|
445
|
+
task.then((response) => {
|
|
446
|
+
if (!response.ok) {
|
|
447
|
+
observer.error(new Error(`OpenAI request failed: ${response.status}`));
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const reader = response.body?.getReader();
|
|
451
|
+
const decoder = new TextDecoder();
|
|
452
|
+
let buffer = "";
|
|
453
|
+
const read = () => {
|
|
454
|
+
reader?.read().then(({ done, value }) => {
|
|
455
|
+
if (done) {
|
|
456
|
+
observer.next({
|
|
457
|
+
message: { content: "", role: "assistant" },
|
|
458
|
+
done: true
|
|
459
|
+
});
|
|
460
|
+
observer.complete();
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
buffer += decoder.decode(value, { stream: true });
|
|
464
|
+
const lines = buffer.split("\n");
|
|
465
|
+
buffer = lines.pop() ?? "";
|
|
466
|
+
for (const line of lines) {
|
|
467
|
+
if (line.trim() && line.startsWith("data: ")) {
|
|
468
|
+
const data = line.slice(6).trim();
|
|
469
|
+
if (data === "[DONE]") {
|
|
470
|
+
observer.next({
|
|
471
|
+
message: { content: "", role: "assistant" },
|
|
472
|
+
done: true
|
|
473
|
+
});
|
|
474
|
+
observer.complete();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const parsed = JSON.parse(data);
|
|
479
|
+
const content = parsed.choices?.[0]?.delta?.content ?? "";
|
|
480
|
+
if (content) {
|
|
481
|
+
observer.next({
|
|
482
|
+
message: { content, role: "assistant" },
|
|
483
|
+
done: false
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
} catch (err) {
|
|
487
|
+
debugLogger.error("Error parsing OpenAI stream data:", { data, error: err });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
read();
|
|
492
|
+
}).catch((err) => observer.error(err));
|
|
493
|
+
};
|
|
494
|
+
read();
|
|
495
|
+
}).catch((err) => observer.error(err));
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
nonStreamChatRequest(url, payload) {
|
|
499
|
+
return from2(fetch(url, {
|
|
500
|
+
method: "POST",
|
|
501
|
+
headers: {
|
|
502
|
+
...this.getHeaders(),
|
|
503
|
+
"Content-Type": "application/json"
|
|
504
|
+
},
|
|
505
|
+
body: JSON.stringify(payload)
|
|
506
|
+
})).pipe(
|
|
507
|
+
switchMap2((response) => {
|
|
508
|
+
if (!response.ok) {
|
|
509
|
+
return throwError2(() => new Error(`OpenAI request failed: ${response.status}`));
|
|
510
|
+
}
|
|
511
|
+
return from2(response.json());
|
|
512
|
+
}),
|
|
513
|
+
map3((data) => ({
|
|
514
|
+
message: {
|
|
515
|
+
content: data.choices?.[0]?.message?.content ?? "",
|
|
516
|
+
role: "assistant"
|
|
517
|
+
},
|
|
518
|
+
done: true
|
|
519
|
+
}))
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
getHeaders() {
|
|
523
|
+
const headers = {};
|
|
524
|
+
if (this.config.apiKey) {
|
|
525
|
+
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
526
|
+
}
|
|
527
|
+
return headers;
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
// src/services/ai-provider/providers/azure-openai.provider.ts
|
|
532
|
+
import { Observable as Observable4, from as from3, switchMap as switchMap3, map as map4, throwError as throwError3 } from "rxjs";
|
|
533
|
+
var AzureOpenAIProvider = class {
|
|
534
|
+
config;
|
|
535
|
+
baseUrl;
|
|
536
|
+
constructor(config) {
|
|
537
|
+
deprecatedAzureOpenAIProvider();
|
|
538
|
+
this.config = config;
|
|
539
|
+
if (!config.baseUrl || !config.deploymentName || !config.apiVersion) {
|
|
540
|
+
throw new Error("Azure OpenAI requires baseUrl, deploymentName, and apiVersion");
|
|
541
|
+
}
|
|
542
|
+
this.baseUrl = config.baseUrl;
|
|
543
|
+
}
|
|
544
|
+
chat(request) {
|
|
545
|
+
const url = `${this.baseUrl}/openai/deployments/${this.config.deploymentName}/chat/completions?api-version=${this.config.apiVersion}`;
|
|
546
|
+
const payload = {
|
|
547
|
+
messages: request.messages,
|
|
548
|
+
stream: Boolean(request.stream),
|
|
549
|
+
temperature: request.temperature,
|
|
550
|
+
max_tokens: request.maxTokens
|
|
551
|
+
};
|
|
552
|
+
if (request.stream) {
|
|
553
|
+
return this.streamChatRequest(url, payload);
|
|
554
|
+
} else {
|
|
555
|
+
return this.nonStreamChatRequest(url, payload);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
generate(request) {
|
|
559
|
+
const chatRequest = {
|
|
560
|
+
model: request.model,
|
|
561
|
+
messages: [{ role: "user", content: request.prompt }],
|
|
562
|
+
stream: request.stream,
|
|
563
|
+
options: request.options
|
|
564
|
+
};
|
|
565
|
+
return this.chat(chatRequest).pipe(
|
|
566
|
+
map4((response) => ({
|
|
567
|
+
response: response.message.content,
|
|
568
|
+
done: response.done
|
|
569
|
+
}))
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
listModels() {
|
|
573
|
+
const model = {
|
|
574
|
+
name: this.config.deploymentName || "azure-deployment",
|
|
575
|
+
details: {
|
|
576
|
+
format: "azure-openai",
|
|
577
|
+
family: "gpt"
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
return new Observable4((observer) => {
|
|
581
|
+
observer.next([model]);
|
|
582
|
+
observer.complete();
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
async validateServiceAvailability(args) {
|
|
586
|
+
try {
|
|
587
|
+
const controller = new AbortController();
|
|
588
|
+
const timeoutId = setTimeout(() => controller.abort(), args.timeoutMs);
|
|
589
|
+
const testUrl = `${this.baseUrl}/openai/deployments/${this.config.deploymentName}/chat/completions?api-version=${this.config.apiVersion}`;
|
|
590
|
+
const response = await fetch(testUrl, {
|
|
591
|
+
method: "POST",
|
|
592
|
+
headers: this.getHeaders(),
|
|
593
|
+
body: JSON.stringify({
|
|
594
|
+
messages: [{ role: "user", content: "test" }],
|
|
595
|
+
max_tokens: 1
|
|
596
|
+
}),
|
|
597
|
+
signal: controller.signal
|
|
598
|
+
});
|
|
599
|
+
clearTimeout(timeoutId);
|
|
600
|
+
return {
|
|
601
|
+
url: this.baseUrl,
|
|
602
|
+
isAvailable: response.ok || response.status === 400
|
|
603
|
+
// 400 might be expected for the test
|
|
604
|
+
};
|
|
605
|
+
} catch (error) {
|
|
606
|
+
if (args.fallbackUrl) {
|
|
607
|
+
try {
|
|
608
|
+
const controller = new AbortController();
|
|
609
|
+
const timeoutId = setTimeout(() => controller.abort(), args.timeoutMs);
|
|
610
|
+
const testUrl = `${args.fallbackUrl}/openai/deployments/${this.config.deploymentName}/chat/completions?api-version=${this.config.apiVersion}`;
|
|
611
|
+
const response = await fetch(testUrl, {
|
|
612
|
+
method: "POST",
|
|
613
|
+
headers: this.getHeaders(),
|
|
614
|
+
body: JSON.stringify({
|
|
615
|
+
messages: [{ role: "user", content: "test" }],
|
|
616
|
+
max_tokens: 1
|
|
617
|
+
}),
|
|
618
|
+
signal: controller.signal
|
|
619
|
+
});
|
|
620
|
+
clearTimeout(timeoutId);
|
|
621
|
+
if (response.ok || response.status === 400) {
|
|
622
|
+
this.baseUrl = args.fallbackUrl;
|
|
623
|
+
return {
|
|
624
|
+
url: args.fallbackUrl,
|
|
625
|
+
isAvailable: true
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
} catch (fallbackError) {
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
url: this.baseUrl,
|
|
633
|
+
isAvailable: false
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
getProviderType() {
|
|
638
|
+
return "azure-openai" /* AZURE_OPENAI */;
|
|
639
|
+
}
|
|
640
|
+
getConfig() {
|
|
641
|
+
return this.config;
|
|
642
|
+
}
|
|
643
|
+
streamChatRequest(url, payload) {
|
|
644
|
+
return new Observable4((observer) => {
|
|
645
|
+
const task = fetch(url, {
|
|
646
|
+
method: "POST",
|
|
647
|
+
headers: {
|
|
648
|
+
...this.getHeaders(),
|
|
649
|
+
"Content-Type": "application/json"
|
|
650
|
+
},
|
|
651
|
+
body: JSON.stringify(payload)
|
|
652
|
+
});
|
|
653
|
+
task.then((response) => {
|
|
654
|
+
if (!response.ok) {
|
|
655
|
+
observer.error(new Error(`Azure OpenAI request failed: ${response.status}`));
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const reader = response.body?.getReader();
|
|
659
|
+
const decoder = new TextDecoder();
|
|
660
|
+
let buffer = "";
|
|
661
|
+
const read = () => {
|
|
662
|
+
reader?.read().then(({ done, value }) => {
|
|
663
|
+
if (done) {
|
|
664
|
+
observer.next({
|
|
665
|
+
message: { content: "", role: "assistant" },
|
|
666
|
+
done: true
|
|
667
|
+
});
|
|
668
|
+
observer.complete();
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
buffer += decoder.decode(value, { stream: true });
|
|
672
|
+
const lines = buffer.split("\n");
|
|
673
|
+
buffer = lines.pop() ?? "";
|
|
674
|
+
for (const line of lines) {
|
|
675
|
+
if (line.trim() && line.startsWith("data: ")) {
|
|
676
|
+
const data = line.slice(6).trim();
|
|
677
|
+
if (data === "[DONE]") {
|
|
678
|
+
observer.next({
|
|
679
|
+
message: { content: "", role: "assistant" },
|
|
680
|
+
done: true
|
|
681
|
+
});
|
|
682
|
+
observer.complete();
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
const parsed = JSON.parse(data);
|
|
687
|
+
const content = parsed.choices?.[0]?.delta?.content ?? "";
|
|
688
|
+
if (content) {
|
|
689
|
+
observer.next({
|
|
690
|
+
message: { content, role: "assistant" },
|
|
691
|
+
done: false
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
} catch (err) {
|
|
695
|
+
debugLogger.error("Error parsing Azure OpenAI stream data:", { data, error: err });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
read();
|
|
700
|
+
}).catch((err) => observer.error(err));
|
|
701
|
+
};
|
|
702
|
+
read();
|
|
703
|
+
}).catch((err) => observer.error(err));
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
nonStreamChatRequest(url, payload) {
|
|
707
|
+
return from3(fetch(url, {
|
|
708
|
+
method: "POST",
|
|
709
|
+
headers: {
|
|
710
|
+
...this.getHeaders(),
|
|
711
|
+
"Content-Type": "application/json"
|
|
712
|
+
},
|
|
713
|
+
body: JSON.stringify(payload)
|
|
714
|
+
})).pipe(
|
|
715
|
+
switchMap3((response) => {
|
|
716
|
+
if (!response.ok) {
|
|
717
|
+
return throwError3(() => new Error(`Azure OpenAI request failed: ${response.status}`));
|
|
718
|
+
}
|
|
719
|
+
return from3(response.json());
|
|
720
|
+
}),
|
|
721
|
+
map4((data) => {
|
|
722
|
+
const content = data.choices?.[0]?.message?.content ?? "";
|
|
723
|
+
return {
|
|
724
|
+
message: {
|
|
725
|
+
content,
|
|
726
|
+
role: "assistant"
|
|
727
|
+
},
|
|
728
|
+
done: true
|
|
729
|
+
};
|
|
730
|
+
})
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
getHeaders() {
|
|
734
|
+
const headers = {};
|
|
735
|
+
if (this.config.apiKey) {
|
|
736
|
+
headers["api-key"] = this.config.apiKey;
|
|
737
|
+
}
|
|
738
|
+
return headers;
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
// src/services/ai-provider/providers/anthropic.provider.ts
|
|
743
|
+
import { Observable as Observable5, from as from4, switchMap as switchMap4, map as map5, throwError as throwError4 } from "rxjs";
|
|
744
|
+
var AnthropicProvider = class {
|
|
745
|
+
config;
|
|
746
|
+
baseUrl;
|
|
747
|
+
constructor(config) {
|
|
748
|
+
deprecatedAnthropicProvider();
|
|
749
|
+
this.config = config;
|
|
750
|
+
this.baseUrl = config.baseUrl || "https://api.anthropic.com/v1";
|
|
751
|
+
}
|
|
752
|
+
chat(request) {
|
|
753
|
+
const url = `${this.baseUrl}/messages`;
|
|
754
|
+
const systemMessage = request.messages.find((msg) => msg.role === "system");
|
|
755
|
+
const userMessages = request.messages.filter((msg) => msg.role !== "system");
|
|
756
|
+
const payload = {
|
|
757
|
+
model: request.model,
|
|
758
|
+
messages: userMessages.map((msg) => ({
|
|
759
|
+
role: msg.role === "user" ? "user" : "assistant",
|
|
760
|
+
content: msg.content
|
|
761
|
+
})),
|
|
762
|
+
system: systemMessage?.content,
|
|
763
|
+
stream: Boolean(request.stream),
|
|
764
|
+
temperature: request.temperature,
|
|
765
|
+
max_tokens: request.maxTokens ?? 1e3
|
|
766
|
+
};
|
|
767
|
+
if (request.stream) {
|
|
768
|
+
return this.streamChatRequest(url, payload);
|
|
769
|
+
} else {
|
|
770
|
+
return this.nonStreamChatRequest(url, payload);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
generate(request) {
|
|
774
|
+
const chatRequest = {
|
|
775
|
+
model: request.model,
|
|
776
|
+
messages: [{ role: "user", content: request.prompt }],
|
|
777
|
+
stream: request.stream,
|
|
778
|
+
options: request.options
|
|
779
|
+
};
|
|
780
|
+
return this.chat(chatRequest).pipe(
|
|
781
|
+
map5((response) => ({
|
|
782
|
+
response: response.message.content,
|
|
783
|
+
done: response.done
|
|
784
|
+
}))
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
listModels() {
|
|
788
|
+
const commonModels = [
|
|
789
|
+
{
|
|
790
|
+
name: "claude-3-5-sonnet-20241022",
|
|
791
|
+
details: {
|
|
792
|
+
format: "anthropic",
|
|
793
|
+
family: "claude-3.5"
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
name: "claude-3-5-haiku-20241022",
|
|
798
|
+
details: {
|
|
799
|
+
format: "anthropic",
|
|
800
|
+
family: "claude-3.5"
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
name: "claude-3-opus-20240229",
|
|
805
|
+
details: {
|
|
806
|
+
format: "anthropic",
|
|
807
|
+
family: "claude-3"
|
|
808
|
+
}
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
name: "claude-3-sonnet-20240229",
|
|
812
|
+
details: {
|
|
813
|
+
format: "anthropic",
|
|
814
|
+
family: "claude-3"
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
name: "claude-3-haiku-20240307",
|
|
819
|
+
details: {
|
|
820
|
+
format: "anthropic",
|
|
821
|
+
family: "claude-3"
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
];
|
|
825
|
+
return new Observable5((observer) => {
|
|
826
|
+
observer.next(commonModels);
|
|
827
|
+
observer.complete();
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
async validateServiceAvailability(args) {
|
|
831
|
+
try {
|
|
832
|
+
const controller = new AbortController();
|
|
833
|
+
const timeoutId = setTimeout(() => controller.abort(), args.timeoutMs);
|
|
834
|
+
const response = await fetch(`${this.baseUrl}/messages`, {
|
|
835
|
+
method: "POST",
|
|
836
|
+
headers: this.getHeaders(),
|
|
837
|
+
body: JSON.stringify({
|
|
838
|
+
model: "claude-3-haiku-20240307",
|
|
839
|
+
messages: [{ role: "user", content: "test" }],
|
|
840
|
+
max_tokens: 1
|
|
841
|
+
}),
|
|
842
|
+
signal: controller.signal
|
|
843
|
+
});
|
|
844
|
+
clearTimeout(timeoutId);
|
|
845
|
+
return {
|
|
846
|
+
url: this.baseUrl,
|
|
847
|
+
isAvailable: response.ok || response.status === 400
|
|
848
|
+
// 400 might be expected for the test
|
|
849
|
+
};
|
|
850
|
+
} catch (error) {
|
|
851
|
+
if (args.fallbackUrl) {
|
|
852
|
+
try {
|
|
853
|
+
const controller = new AbortController();
|
|
854
|
+
const timeoutId = setTimeout(() => controller.abort(), args.timeoutMs);
|
|
855
|
+
const response = await fetch(`${args.fallbackUrl}/messages`, {
|
|
856
|
+
method: "POST",
|
|
857
|
+
headers: this.getHeaders(),
|
|
858
|
+
body: JSON.stringify({
|
|
859
|
+
model: "claude-3-haiku-20240307",
|
|
860
|
+
messages: [{ role: "user", content: "test" }],
|
|
861
|
+
max_tokens: 1
|
|
862
|
+
}),
|
|
863
|
+
signal: controller.signal
|
|
864
|
+
});
|
|
865
|
+
clearTimeout(timeoutId);
|
|
866
|
+
if (response.ok || response.status === 400) {
|
|
867
|
+
this.baseUrl = args.fallbackUrl;
|
|
868
|
+
return {
|
|
869
|
+
url: args.fallbackUrl,
|
|
870
|
+
isAvailable: true
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
} catch (fallbackError) {
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return {
|
|
877
|
+
url: this.baseUrl,
|
|
878
|
+
isAvailable: false
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
getProviderType() {
|
|
883
|
+
return "anthropic" /* ANTHROPIC */;
|
|
884
|
+
}
|
|
885
|
+
getConfig() {
|
|
886
|
+
return this.config;
|
|
887
|
+
}
|
|
888
|
+
streamChatRequest(url, payload) {
|
|
889
|
+
return new Observable5((observer) => {
|
|
890
|
+
const task = fetch(url, {
|
|
891
|
+
method: "POST",
|
|
892
|
+
headers: {
|
|
893
|
+
...this.getHeaders(),
|
|
894
|
+
"Content-Type": "application/json"
|
|
895
|
+
},
|
|
896
|
+
body: JSON.stringify(payload)
|
|
897
|
+
});
|
|
898
|
+
task.then((response) => {
|
|
899
|
+
if (!response.ok) {
|
|
900
|
+
observer.error(new Error(`Anthropic request failed: ${response.status}`));
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const reader = response.body?.getReader();
|
|
904
|
+
const decoder = new TextDecoder();
|
|
905
|
+
let buffer = "";
|
|
906
|
+
const read = () => {
|
|
907
|
+
reader?.read().then(({ done, value }) => {
|
|
908
|
+
if (done) {
|
|
909
|
+
observer.next({
|
|
910
|
+
message: { content: "", role: "assistant" },
|
|
911
|
+
done: true
|
|
912
|
+
});
|
|
913
|
+
observer.complete();
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
buffer += decoder.decode(value, { stream: true });
|
|
917
|
+
const lines = buffer.split("\n");
|
|
918
|
+
buffer = lines.pop() ?? "";
|
|
919
|
+
for (const line of lines) {
|
|
920
|
+
if (line.trim() && line.startsWith("data: ")) {
|
|
921
|
+
const data = line.slice(6).trim();
|
|
922
|
+
if (data === "[DONE]") {
|
|
923
|
+
observer.next({
|
|
924
|
+
message: { content: "", role: "assistant" },
|
|
925
|
+
done: true
|
|
926
|
+
});
|
|
927
|
+
observer.complete();
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
try {
|
|
931
|
+
const parsed = JSON.parse(data);
|
|
932
|
+
const content = parsed.delta?.text || "";
|
|
933
|
+
if (content) {
|
|
934
|
+
observer.next({
|
|
935
|
+
message: { content, role: "assistant" },
|
|
936
|
+
done: false
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
} catch (err) {
|
|
940
|
+
debugLogger.error("Error parsing Anthropic stream data:", { data, error: err });
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
read();
|
|
945
|
+
}).catch((err) => observer.error(err));
|
|
946
|
+
};
|
|
947
|
+
read();
|
|
948
|
+
}).catch((err) => observer.error(err));
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
nonStreamChatRequest(url, payload) {
|
|
952
|
+
return from4(fetch(url, {
|
|
953
|
+
method: "POST",
|
|
954
|
+
headers: {
|
|
955
|
+
...this.getHeaders(),
|
|
956
|
+
"Content-Type": "application/json"
|
|
957
|
+
},
|
|
958
|
+
body: JSON.stringify(payload)
|
|
959
|
+
})).pipe(
|
|
960
|
+
switchMap4((response) => {
|
|
961
|
+
if (!response.ok) {
|
|
962
|
+
return throwError4(() => new Error(`Anthropic request failed: ${response.status}`));
|
|
963
|
+
}
|
|
964
|
+
return from4(response.json());
|
|
965
|
+
}),
|
|
966
|
+
map5((data) => ({
|
|
967
|
+
message: {
|
|
968
|
+
content: this.extractContentText(data),
|
|
969
|
+
role: "assistant"
|
|
970
|
+
},
|
|
971
|
+
done: true
|
|
972
|
+
}))
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
extractContentText(payload) {
|
|
976
|
+
const { content } = payload;
|
|
977
|
+
if (Array.isArray(content)) {
|
|
978
|
+
for (const entry of content) {
|
|
979
|
+
if (typeof entry === "string") {
|
|
980
|
+
return entry;
|
|
981
|
+
}
|
|
982
|
+
if (entry && typeof entry === "object" && "text" in entry) {
|
|
983
|
+
const text = entry.text;
|
|
984
|
+
if (typeof text === "string") {
|
|
985
|
+
return text;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
} else if (typeof content === "string") {
|
|
990
|
+
return content;
|
|
991
|
+
}
|
|
992
|
+
if (typeof payload.completion === "string") {
|
|
993
|
+
return payload.completion;
|
|
994
|
+
}
|
|
995
|
+
if (payload.message && typeof payload.message.content === "string") {
|
|
996
|
+
return payload.message.content;
|
|
997
|
+
}
|
|
998
|
+
return "";
|
|
999
|
+
}
|
|
1000
|
+
getHeaders() {
|
|
1001
|
+
const headers = {
|
|
1002
|
+
"anthropic-version": "2023-06-01"
|
|
1003
|
+
};
|
|
1004
|
+
if (this.config.apiKey) {
|
|
1005
|
+
headers["x-api-key"] = this.config.apiKey;
|
|
1006
|
+
}
|
|
1007
|
+
return headers;
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
// src/services/ai-provider/providers/gateway.provider.ts
|
|
1012
|
+
import { map as map11 } from "rxjs";
|
|
1013
|
+
|
|
1014
|
+
// src/services/gateway/gateway.service.ts
|
|
1015
|
+
import axios, { AxiosHeaders } from "axios";
|
|
1016
|
+
import { catchError as catchError2, from as from5, lastValueFrom as lastValueFrom2, map as map6, Observable as Observable6, of as of2, shareReplay as shareReplay2, timeout as timeout2 } from "rxjs";
|
|
1017
|
+
var GatewayService = class {
|
|
1018
|
+
constructor(_baseUrl, _tokenFactory, _feedbackEmail) {
|
|
1019
|
+
this._baseUrl = _baseUrl;
|
|
1020
|
+
this._tokenFactory = _tokenFactory;
|
|
1021
|
+
this._feedbackEmail = _feedbackEmail;
|
|
1022
|
+
if (!this._baseUrl) {
|
|
1023
|
+
this._baseUrl = "http://localhost:5000";
|
|
1024
|
+
debugLogger.warn(`No gateway URL provided, using default: ${this._baseUrl}`);
|
|
1025
|
+
}
|
|
1026
|
+
this._baseUrl = this._baseUrl.replace(/\/$/, "");
|
|
1027
|
+
if (this._baseUrl.endsWith("/api")) {
|
|
1028
|
+
this._baseUrl = this._baseUrl.slice(0, -4);
|
|
1029
|
+
debugLogger.info(`Removed /api suffix from gateway URL: ${this._baseUrl}`);
|
|
1030
|
+
}
|
|
1031
|
+
this._client = this._createAxiosClient();
|
|
1032
|
+
}
|
|
1033
|
+
_client;
|
|
1034
|
+
_createAxiosClient() {
|
|
1035
|
+
const instance = axios.create({
|
|
1036
|
+
baseURL: this._baseUrl,
|
|
1037
|
+
headers: {
|
|
1038
|
+
"Content-Type": "application/json"
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
instance.interceptors.request.use((config) => {
|
|
1042
|
+
const token = this._tokenFactory();
|
|
1043
|
+
const headers = AxiosHeaders.from(config.headers ?? {});
|
|
1044
|
+
if (!headers.has("Content-Type")) {
|
|
1045
|
+
headers.set("Content-Type", "application/json");
|
|
1046
|
+
}
|
|
1047
|
+
if (token && token.trim()) {
|
|
1048
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
1049
|
+
} else if (headers.has("Authorization")) {
|
|
1050
|
+
headers.delete("Authorization");
|
|
1051
|
+
}
|
|
1052
|
+
config.headers = headers;
|
|
1053
|
+
return config;
|
|
1054
|
+
});
|
|
1055
|
+
instance.interceptors.response.use(
|
|
1056
|
+
(response) => response,
|
|
1057
|
+
(error) => Promise.reject(this._normalizeAxiosError(error))
|
|
1058
|
+
);
|
|
1059
|
+
return instance;
|
|
1060
|
+
}
|
|
1061
|
+
_normalizeAxiosError(error) {
|
|
1062
|
+
if (error.response) {
|
|
1063
|
+
return this._createHttpError(
|
|
1064
|
+
`Request failed: ${error.response.status} ${error.response.statusText ?? ""}`,
|
|
1065
|
+
{
|
|
1066
|
+
status: error.response.status,
|
|
1067
|
+
statusText: error.response.statusText ?? "",
|
|
1068
|
+
data: error.response.data,
|
|
1069
|
+
url: error.config?.url ?? ""
|
|
1070
|
+
}
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
if (error.request) {
|
|
1074
|
+
return new Error(`No response received from gateway: ${error.message}`);
|
|
1075
|
+
}
|
|
1076
|
+
return new Error(error.message);
|
|
1077
|
+
}
|
|
1078
|
+
_createHttpError(message, response) {
|
|
1079
|
+
return Object.assign(new Error(message), { response });
|
|
1080
|
+
}
|
|
1081
|
+
_setBaseUrl(url) {
|
|
1082
|
+
this._baseUrl = url;
|
|
1083
|
+
this._client.defaults.baseURL = url;
|
|
1084
|
+
}
|
|
1085
|
+
_tryGatewayWithTimeout(args) {
|
|
1086
|
+
const { url, responseType, timeoutMs } = args;
|
|
1087
|
+
const source = this._get(url, responseType);
|
|
1088
|
+
const mapped = source.pipe(
|
|
1089
|
+
catchError2((e) => e?.message.includes("401") ? of2(true) : of2(false)),
|
|
1090
|
+
map6(() => true),
|
|
1091
|
+
timeout2(timeoutMs)
|
|
1092
|
+
);
|
|
1093
|
+
return mapped;
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Validates the availability of the gateway service.
|
|
1097
|
+
* @param fallbackUrl The fallback URL to try if the base URL is not available.
|
|
1098
|
+
* @returns An object containing the URL and availability status.
|
|
1099
|
+
*/
|
|
1100
|
+
async validateServiceAvailability(args) {
|
|
1101
|
+
const { fallbackUrl, timeoutMs } = args;
|
|
1102
|
+
const responseType = "json";
|
|
1103
|
+
const availability = {
|
|
1104
|
+
url: "",
|
|
1105
|
+
isAvailable: false
|
|
1106
|
+
};
|
|
1107
|
+
try {
|
|
1108
|
+
debugLogger.debug(`Validating gateway service availability at ${this._baseUrl}`);
|
|
1109
|
+
availability.url = this._baseUrl;
|
|
1110
|
+
availability.isAvailable = await lastValueFrom2(
|
|
1111
|
+
this._tryGatewayWithTimeout({
|
|
1112
|
+
url: `${availability.url}/api/health`,
|
|
1113
|
+
responseType,
|
|
1114
|
+
timeoutMs
|
|
1115
|
+
})
|
|
1116
|
+
);
|
|
1117
|
+
if (!availability.isAvailable) {
|
|
1118
|
+
throw new Error(`Gateway service not available at ${this._baseUrl}`);
|
|
1119
|
+
}
|
|
1120
|
+
return availability;
|
|
1121
|
+
} catch (e) {
|
|
1122
|
+
if (fallbackUrl) {
|
|
1123
|
+
debugLogger.warn(`Gateway service not available at ${this._baseUrl}, trying fallback URL: ${fallbackUrl}`);
|
|
1124
|
+
try {
|
|
1125
|
+
availability.url = fallbackUrl.replace(/\/$/, "");
|
|
1126
|
+
availability.isAvailable = await lastValueFrom2(
|
|
1127
|
+
this._tryGatewayWithTimeout({
|
|
1128
|
+
url: `${availability.url}/api/health`,
|
|
1129
|
+
responseType,
|
|
1130
|
+
timeoutMs
|
|
1131
|
+
})
|
|
1132
|
+
);
|
|
1133
|
+
if (!availability.isAvailable) {
|
|
1134
|
+
throw new Error(`Gateway service not available at ${fallbackUrl}`);
|
|
1135
|
+
}
|
|
1136
|
+
this._setBaseUrl(availability.url);
|
|
1137
|
+
return availability;
|
|
1138
|
+
} catch (e2) {
|
|
1139
|
+
debugLogger.error(`Gateway service not available at fallback URL: ${fallbackUrl}`);
|
|
1140
|
+
throw e2;
|
|
1141
|
+
}
|
|
1142
|
+
} else {
|
|
1143
|
+
debugLogger.error(`Gateway service not available and no fallback URL provided`);
|
|
1144
|
+
throw e;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Get gateway health status and available providers
|
|
1150
|
+
*/
|
|
1151
|
+
getHealth() {
|
|
1152
|
+
const url = `${this._baseUrl}/api/health`;
|
|
1153
|
+
return this._get(url);
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Chat completion using the gateway API
|
|
1157
|
+
*/
|
|
1158
|
+
chat(request) {
|
|
1159
|
+
const endpoint = request.provider === "ollama" ? `/api/${request.provider}/chat` : request.provider ? `/api/${request.provider}/chat/completions` : "/api/chat/completions";
|
|
1160
|
+
const url = `${this._baseUrl}${endpoint}`;
|
|
1161
|
+
debugLogger.debug(`Gateway chat request to ${url} with provider: ${request.provider || "default"}`, {
|
|
1162
|
+
model: request.model,
|
|
1163
|
+
messageCount: request.messages.length,
|
|
1164
|
+
hasImages: !!(request.images && request.images.length > 0),
|
|
1165
|
+
imageCount: request.images?.length || 0
|
|
1166
|
+
});
|
|
1167
|
+
const requestBody = { ...request, stream: request.stream !== false };
|
|
1168
|
+
return new Observable6((observer) => {
|
|
1169
|
+
const controller = new AbortController();
|
|
1170
|
+
const task = fetch(url, {
|
|
1171
|
+
method: "POST",
|
|
1172
|
+
headers: this._getHeaders(),
|
|
1173
|
+
body: JSON.stringify(requestBody),
|
|
1174
|
+
signal: controller.signal
|
|
1175
|
+
});
|
|
1176
|
+
task.then(async (response) => {
|
|
1177
|
+
debugLogger.debug(`Gateway chat response status: ${response.status} for provider: ${request.provider || "default"}`);
|
|
1178
|
+
if (!response.ok) {
|
|
1179
|
+
let errorText = "";
|
|
1180
|
+
let errorData = null;
|
|
1181
|
+
try {
|
|
1182
|
+
errorText = await response.text();
|
|
1183
|
+
debugLogger.error("GatewayService chat error response body", {
|
|
1184
|
+
status: response.status,
|
|
1185
|
+
statusText: response.statusText,
|
|
1186
|
+
url: response.url,
|
|
1187
|
+
body: errorText
|
|
1188
|
+
});
|
|
1189
|
+
} catch (readError) {
|
|
1190
|
+
debugLogger.error("GatewayService chat failed to read error response body", { error: readError });
|
|
1191
|
+
errorText = `Request failed with status ${response.status}`;
|
|
1192
|
+
}
|
|
1193
|
+
try {
|
|
1194
|
+
errorData = JSON.parse(errorText);
|
|
1195
|
+
debugLogger.error("GatewayService chat parsed error payload", errorData);
|
|
1196
|
+
} catch (parseError) {
|
|
1197
|
+
debugLogger.error("GatewayService chat error payload was not valid JSON");
|
|
1198
|
+
errorData = { message: errorText };
|
|
1199
|
+
}
|
|
1200
|
+
const error = this._createHttpError(
|
|
1201
|
+
`POST ${url} failed: ${response.status} ${response.statusText ?? ""}`,
|
|
1202
|
+
{
|
|
1203
|
+
status: response.status,
|
|
1204
|
+
statusText: response.statusText ?? "",
|
|
1205
|
+
data: errorData,
|
|
1206
|
+
url
|
|
1207
|
+
}
|
|
1208
|
+
);
|
|
1209
|
+
throw error;
|
|
1210
|
+
}
|
|
1211
|
+
const reader = response.body?.getReader();
|
|
1212
|
+
const decoder = new TextDecoder();
|
|
1213
|
+
let buffer = "";
|
|
1214
|
+
const read = () => {
|
|
1215
|
+
reader?.read().then(({ done, value }) => {
|
|
1216
|
+
if (done) {
|
|
1217
|
+
if (buffer.trim() !== "") {
|
|
1218
|
+
try {
|
|
1219
|
+
const finalResponse = JSON.parse(buffer);
|
|
1220
|
+
observer.next(finalResponse);
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
debugLogger.error("GatewayService chat final chunk parsing error", { buffer, error: err });
|
|
1223
|
+
observer.error(err);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
observer.complete();
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1230
|
+
const lines = buffer.split("\n");
|
|
1231
|
+
buffer = lines.pop() ?? "";
|
|
1232
|
+
for (const line of lines) {
|
|
1233
|
+
const trimmed = line.trim();
|
|
1234
|
+
if (trimmed) {
|
|
1235
|
+
let data = trimmed;
|
|
1236
|
+
if (trimmed.startsWith("data: ")) {
|
|
1237
|
+
data = trimmed.slice(6);
|
|
1238
|
+
if (data === "[DONE]") {
|
|
1239
|
+
observer.complete();
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
try {
|
|
1244
|
+
const parsed = JSON.parse(data);
|
|
1245
|
+
if (request.provider === "ollama" && parsed.message) {
|
|
1246
|
+
const transformed = {
|
|
1247
|
+
id: `chatcmpl-${Date.now()}`,
|
|
1248
|
+
object: "chat.completion.chunk",
|
|
1249
|
+
created: Math.floor(new Date(parsed.created_at || Date.now()).getTime() / 1e3),
|
|
1250
|
+
model: parsed.model,
|
|
1251
|
+
choices: [{
|
|
1252
|
+
index: 0,
|
|
1253
|
+
delta: {
|
|
1254
|
+
role: parsed.message.role,
|
|
1255
|
+
content: parsed.message.content
|
|
1256
|
+
},
|
|
1257
|
+
finish_reason: parsed.done ? parsed.done_reason || "stop" : null
|
|
1258
|
+
}]
|
|
1259
|
+
};
|
|
1260
|
+
if (parsed.done && parsed.total_duration) {
|
|
1261
|
+
transformed.usage = {
|
|
1262
|
+
prompt_tokens: parsed.prompt_eval_count || 0,
|
|
1263
|
+
completion_tokens: parsed.eval_count || 0,
|
|
1264
|
+
total_tokens: (parsed.prompt_eval_count || 0) + (parsed.eval_count || 0)
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
observer.next(transformed);
|
|
1268
|
+
} else {
|
|
1269
|
+
observer.next(parsed);
|
|
1270
|
+
}
|
|
1271
|
+
} catch (err) {
|
|
1272
|
+
debugLogger.error("GatewayService chat stream chunk parsing error", {
|
|
1273
|
+
line: trimmed,
|
|
1274
|
+
rawData: data,
|
|
1275
|
+
error: err
|
|
1276
|
+
});
|
|
1277
|
+
observer.error(err);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
read();
|
|
1282
|
+
}).catch((err) => observer.error(err));
|
|
1283
|
+
};
|
|
1284
|
+
read();
|
|
1285
|
+
}).catch((err) => {
|
|
1286
|
+
debugLogger.error("GatewayService chat fetch error", {
|
|
1287
|
+
error: err,
|
|
1288
|
+
url,
|
|
1289
|
+
provider: request.provider
|
|
1290
|
+
});
|
|
1291
|
+
observer.error(err);
|
|
1292
|
+
});
|
|
1293
|
+
return () => {
|
|
1294
|
+
try {
|
|
1295
|
+
controller.abort();
|
|
1296
|
+
} catch {
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Text generation using the gateway API
|
|
1303
|
+
*/
|
|
1304
|
+
generate(request) {
|
|
1305
|
+
const endpoint = request.provider ? `/api/${request.provider}/generate` : "/api/generate";
|
|
1306
|
+
const url = `${this._baseUrl}${endpoint}`;
|
|
1307
|
+
debugLogger.debug(`Gateway generate request to ${url} with provider: ${request.provider || "default"}`);
|
|
1308
|
+
return new Observable6((observer) => {
|
|
1309
|
+
const task = fetch(url, {
|
|
1310
|
+
method: "POST",
|
|
1311
|
+
headers: this._getHeaders(),
|
|
1312
|
+
body: JSON.stringify({ ...request, stream: request.stream !== false })
|
|
1313
|
+
});
|
|
1314
|
+
task.then(async (response) => {
|
|
1315
|
+
if (!response.ok) {
|
|
1316
|
+
let errorText = "";
|
|
1317
|
+
let errorData = null;
|
|
1318
|
+
try {
|
|
1319
|
+
errorText = await response.text();
|
|
1320
|
+
} catch (readError) {
|
|
1321
|
+
errorText = `Request failed with status ${response.status}`;
|
|
1322
|
+
}
|
|
1323
|
+
try {
|
|
1324
|
+
errorData = JSON.parse(errorText);
|
|
1325
|
+
} catch (parseError) {
|
|
1326
|
+
errorData = { message: errorText };
|
|
1327
|
+
}
|
|
1328
|
+
const error = this._createHttpError(
|
|
1329
|
+
`POST ${url} failed: ${response.status} ${response.statusText ?? ""}`,
|
|
1330
|
+
{
|
|
1331
|
+
status: response.status,
|
|
1332
|
+
statusText: response.statusText ?? "",
|
|
1333
|
+
data: errorData,
|
|
1334
|
+
url
|
|
1335
|
+
}
|
|
1336
|
+
);
|
|
1337
|
+
throw error;
|
|
1338
|
+
}
|
|
1339
|
+
const reader = response.body?.getReader();
|
|
1340
|
+
const decoder = new TextDecoder();
|
|
1341
|
+
let buffer = "";
|
|
1342
|
+
const read = () => {
|
|
1343
|
+
reader?.read().then(({ done, value }) => {
|
|
1344
|
+
if (done) {
|
|
1345
|
+
if (buffer.trim() !== "") {
|
|
1346
|
+
try {
|
|
1347
|
+
observer.next(JSON.parse(buffer));
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
observer.error(err);
|
|
1350
|
+
debugLogger.error("Final chunk parsing error (gateway generate):", { buffer });
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
observer.complete();
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1357
|
+
const lines = buffer.split("\n");
|
|
1358
|
+
buffer = lines.pop() ?? "";
|
|
1359
|
+
for (const line of lines) {
|
|
1360
|
+
if (line.trim()) {
|
|
1361
|
+
try {
|
|
1362
|
+
observer.next(JSON.parse(line));
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
observer.error(err);
|
|
1365
|
+
debugLogger.error("Error parsing JSON line (gateway generate):", { line });
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
read();
|
|
1370
|
+
}).catch((err) => observer.error(err));
|
|
1371
|
+
};
|
|
1372
|
+
read();
|
|
1373
|
+
}).catch((err) => observer.error(err));
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* List all available models from all providers
|
|
1378
|
+
*/
|
|
1379
|
+
listModels() {
|
|
1380
|
+
const url = `${this._baseUrl}/api/models`;
|
|
1381
|
+
const response = this._get(url);
|
|
1382
|
+
const result = response.pipe(
|
|
1383
|
+
map6((data) => data.models),
|
|
1384
|
+
shareReplay2(1)
|
|
1385
|
+
);
|
|
1386
|
+
return result;
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* List models for a specific provider
|
|
1390
|
+
*/
|
|
1391
|
+
listModelsByProvider(provider) {
|
|
1392
|
+
const url = `${this._baseUrl}/api/models/${provider}`;
|
|
1393
|
+
const response = this._get(url);
|
|
1394
|
+
const result = response.pipe(
|
|
1395
|
+
map6((data) => data.models),
|
|
1396
|
+
shareReplay2(1)
|
|
1397
|
+
);
|
|
1398
|
+
return result;
|
|
1399
|
+
}
|
|
1400
|
+
getMemory() {
|
|
1401
|
+
const url = `${this._baseUrl}/api/memory`;
|
|
1402
|
+
return this._get(url);
|
|
1403
|
+
}
|
|
1404
|
+
_get(url, responseType = "json") {
|
|
1405
|
+
const request = this._client.get(url, { responseType });
|
|
1406
|
+
return from5(request).pipe(
|
|
1407
|
+
map6((response) => response.data),
|
|
1408
|
+
shareReplay2(1)
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
_post(url, body) {
|
|
1412
|
+
const request = this._client.post(url, body);
|
|
1413
|
+
return from5(request).pipe(
|
|
1414
|
+
map6((response) => response.data),
|
|
1415
|
+
shareReplay2(1)
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
_getHeaders() {
|
|
1419
|
+
const token = this._tokenFactory();
|
|
1420
|
+
const headers = {
|
|
1421
|
+
"Content-Type": "application/json"
|
|
1422
|
+
};
|
|
1423
|
+
if (token && token.trim() !== "") {
|
|
1424
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
1425
|
+
debugLogger.debug("Authorization header set with token");
|
|
1426
|
+
} else {
|
|
1427
|
+
debugLogger.warn("GatewayService: No token found, skipping Authorization header");
|
|
1428
|
+
}
|
|
1429
|
+
return headers;
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Submit feedback to the gateway API
|
|
1433
|
+
*/
|
|
1434
|
+
submitFeedback(feedback) {
|
|
1435
|
+
const url = `${this._baseUrl}/api/feedback`;
|
|
1436
|
+
debugLogger.debug("Gateway feedback submission", {
|
|
1437
|
+
category: feedback.category,
|
|
1438
|
+
priority: feedback.priority,
|
|
1439
|
+
hasImages: !!(feedback.images && feedback.images.length > 0),
|
|
1440
|
+
hasAttachments: !!(feedback.attachments && feedback.attachments.length > 0)
|
|
1441
|
+
});
|
|
1442
|
+
return from5(
|
|
1443
|
+
this._client.post(url, feedback).then((response) => response.data)
|
|
1444
|
+
).pipe(
|
|
1445
|
+
catchError2((error) => {
|
|
1446
|
+
debugLogger.error("Feedback submission failed, using email fallback", error);
|
|
1447
|
+
const fallbackResponse = {
|
|
1448
|
+
id: `fallback-${Date.now()}`,
|
|
1449
|
+
status: "submitted",
|
|
1450
|
+
message: "Feedback submission failed. Opening email client as fallback.",
|
|
1451
|
+
mailtoUrl: this._generateMailtoUrl(feedback)
|
|
1452
|
+
};
|
|
1453
|
+
return of2(fallbackResponse);
|
|
1454
|
+
})
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Generate a mailto URL as fallback for feedback submission
|
|
1459
|
+
*/
|
|
1460
|
+
_generateMailtoUrl(feedback) {
|
|
1461
|
+
const subject = encodeURIComponent(`[${feedback.category.toUpperCase()}] ${feedback.title}`);
|
|
1462
|
+
let body = `Category: ${feedback.category}
|
|
1463
|
+
`;
|
|
1464
|
+
body += `Priority: ${feedback.priority}
|
|
1465
|
+
`;
|
|
1466
|
+
if (feedback.annoyanceLevel) {
|
|
1467
|
+
const annoyanceLabels = {
|
|
1468
|
+
1: "\u{1F60A} Not annoying at all",
|
|
1469
|
+
2: "\u{1F610} Slightly annoying",
|
|
1470
|
+
3: "\u{1F644} Moderately annoying",
|
|
1471
|
+
4: "\u{1F620} Very annoying",
|
|
1472
|
+
5: "\u{1F92C} Extremely annoying"
|
|
1473
|
+
};
|
|
1474
|
+
body += `Annoyance Level: ${feedback.annoyanceLevel}/5 - ${annoyanceLabels[feedback.annoyanceLevel]}
|
|
1475
|
+
`;
|
|
1476
|
+
}
|
|
1477
|
+
body += `
|
|
1478
|
+
Description:
|
|
1479
|
+
${feedback.description}
|
|
1480
|
+
|
|
1481
|
+
`;
|
|
1482
|
+
if (feedback.sessionInfo) {
|
|
1483
|
+
body += `Session Info:
|
|
1484
|
+
`;
|
|
1485
|
+
body += `- Model: ${feedback.sessionInfo.currentModel}
|
|
1486
|
+
`;
|
|
1487
|
+
body += `- Provider: ${feedback.sessionInfo.currentProvider}
|
|
1488
|
+
`;
|
|
1489
|
+
body += `- Conversation: ${feedback.sessionInfo.conversationId}
|
|
1490
|
+
`;
|
|
1491
|
+
body += `- Timestamp: ${feedback.sessionInfo.timestamp}
|
|
1492
|
+
|
|
1493
|
+
`;
|
|
1494
|
+
}
|
|
1495
|
+
if (feedback.browserInfo) {
|
|
1496
|
+
body += `Browser Info:
|
|
1497
|
+
`;
|
|
1498
|
+
body += `- Name: ${feedback.browserInfo.name}
|
|
1499
|
+
`;
|
|
1500
|
+
body += `- Version: ${feedback.browserInfo.version}
|
|
1501
|
+
`;
|
|
1502
|
+
body += `- Platform: ${feedback.browserInfo.platform}
|
|
1503
|
+
|
|
1504
|
+
`;
|
|
1505
|
+
}
|
|
1506
|
+
if (feedback.userAgent) {
|
|
1507
|
+
body += `User Agent: ${feedback.userAgent}
|
|
1508
|
+
|
|
1509
|
+
`;
|
|
1510
|
+
}
|
|
1511
|
+
if (feedback.contactEmail) {
|
|
1512
|
+
body += `Contact Email: ${feedback.contactEmail}
|
|
1513
|
+
|
|
1514
|
+
`;
|
|
1515
|
+
}
|
|
1516
|
+
if (feedback.images && feedback.images.length > 0) {
|
|
1517
|
+
body += `\u{1F4CE} IMAGE ATTACHMENT:
|
|
1518
|
+
`;
|
|
1519
|
+
body += `Please paste your clipboard contents here (Ctrl+V or Cmd+V)
|
|
1520
|
+
|
|
1521
|
+
`;
|
|
1522
|
+
}
|
|
1523
|
+
if (feedback.attachments && feedback.attachments.length > 0) {
|
|
1524
|
+
body += `\u{1F4CE} IMPORTANT - FILES TO ATTACH:
|
|
1525
|
+
`;
|
|
1526
|
+
body += `Please attach the following ${feedback.attachments.length} file(s) to this email:
|
|
1527
|
+
`;
|
|
1528
|
+
feedback.attachments.forEach((attachment, index) => {
|
|
1529
|
+
body += ` \u2022 File ${index + 1}: ${attachment.name || `[Attachment ${index + 1}]`}
|
|
1530
|
+
`;
|
|
1531
|
+
});
|
|
1532
|
+
body += `
|
|
1533
|
+
(Note: Files cannot be automatically included in email links)
|
|
1534
|
+
|
|
1535
|
+
`;
|
|
1536
|
+
}
|
|
1537
|
+
body += `---
|
|
1538
|
+
Generated by Bandit AI Feedback System`;
|
|
1539
|
+
const encodedBody = encodeURIComponent(body);
|
|
1540
|
+
const toEmail = this._feedbackEmail || "feedback@burtson.ai";
|
|
1541
|
+
return `mailto:${toEmail}?subject=${subject}&body=${encodedBody}`;
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
|
|
1545
|
+
// src/services/gateway/openai-gateway.service.ts
|
|
1546
|
+
import { map as map7 } from "rxjs/operators";
|
|
1547
|
+
var OpenAIGatewayService = class {
|
|
1548
|
+
_gatewayService;
|
|
1549
|
+
constructor(gatewayUrl, tokenFactory) {
|
|
1550
|
+
this._gatewayService = new GatewayService(gatewayUrl, tokenFactory);
|
|
1551
|
+
debugLogger.info("OpenAIGatewayService initialized", { gatewayUrl });
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Validates the availability of the gateway service for OpenAI
|
|
1555
|
+
*/
|
|
1556
|
+
async validateServiceAvailability(args) {
|
|
1557
|
+
return this._gatewayService.validateServiceAvailability(args);
|
|
1558
|
+
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Chat completion using OpenAI through the gateway
|
|
1561
|
+
*/
|
|
1562
|
+
chat(request) {
|
|
1563
|
+
const gatewayRequest = {
|
|
1564
|
+
...request,
|
|
1565
|
+
provider: "openai"
|
|
1566
|
+
};
|
|
1567
|
+
debugLogger.debug("OpenAI Gateway chat request", {
|
|
1568
|
+
model: request.model,
|
|
1569
|
+
messageCount: request.messages.length,
|
|
1570
|
+
stream: request.stream
|
|
1571
|
+
});
|
|
1572
|
+
return this._gatewayService.chat(gatewayRequest);
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Text completion using OpenAI through the gateway
|
|
1576
|
+
*/
|
|
1577
|
+
complete(prompt, options) {
|
|
1578
|
+
const gatewayRequest = {
|
|
1579
|
+
model: options.model,
|
|
1580
|
+
prompt,
|
|
1581
|
+
temperature: options.temperature,
|
|
1582
|
+
max_tokens: options.max_tokens,
|
|
1583
|
+
stream: options.stream,
|
|
1584
|
+
stop: options.stop,
|
|
1585
|
+
provider: "openai"
|
|
1586
|
+
};
|
|
1587
|
+
debugLogger.debug("OpenAI Gateway completion request", {
|
|
1588
|
+
model: options.model,
|
|
1589
|
+
promptLength: prompt.length,
|
|
1590
|
+
stream: options.stream
|
|
1591
|
+
});
|
|
1592
|
+
return this._gatewayService.generate(gatewayRequest);
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* List available OpenAI models through the gateway
|
|
1596
|
+
*/
|
|
1597
|
+
listModels() {
|
|
1598
|
+
debugLogger.debug("Fetching OpenAI models through gateway");
|
|
1599
|
+
return this._gatewayService.listModelsByProvider("openai");
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1602
|
+
* Get gateway health with OpenAI provider status
|
|
1603
|
+
*/
|
|
1604
|
+
getHealth() {
|
|
1605
|
+
return this._gatewayService.getHealth().pipe(
|
|
1606
|
+
map7((health) => ({
|
|
1607
|
+
...health,
|
|
1608
|
+
openai_status: health.providers.find((p) => p.name === "openai")?.status || "unavailable"
|
|
1609
|
+
}))
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
|
|
1614
|
+
// src/services/gateway/azure-openai-gateway.service.ts
|
|
1615
|
+
import { map as map8 } from "rxjs/operators";
|
|
1616
|
+
var AzureOpenAIGatewayService = class {
|
|
1617
|
+
_gatewayService;
|
|
1618
|
+
_azureConfig;
|
|
1619
|
+
constructor(gatewayUrl, tokenFactory, azureConfig) {
|
|
1620
|
+
this._gatewayService = new GatewayService(gatewayUrl, tokenFactory);
|
|
1621
|
+
this._azureConfig = azureConfig;
|
|
1622
|
+
debugLogger.info("AzureOpenAIGatewayService initialized", {
|
|
1623
|
+
gatewayUrl,
|
|
1624
|
+
deploymentName: azureConfig.deploymentName,
|
|
1625
|
+
apiVersion: azureConfig.apiVersion
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Validates the availability of the gateway service for Azure OpenAI
|
|
1630
|
+
*/
|
|
1631
|
+
async validateServiceAvailability(args) {
|
|
1632
|
+
return this._gatewayService.validateServiceAvailability(args);
|
|
1633
|
+
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Chat completion using Azure OpenAI through the gateway
|
|
1636
|
+
*/
|
|
1637
|
+
chat(request) {
|
|
1638
|
+
const gatewayRequest = {
|
|
1639
|
+
...request,
|
|
1640
|
+
model: this._azureConfig.deploymentName,
|
|
1641
|
+
// Use deployment name as model
|
|
1642
|
+
provider: "azure-openai"
|
|
1643
|
+
};
|
|
1644
|
+
debugLogger.debug("Azure OpenAI Gateway chat request", {
|
|
1645
|
+
deploymentName: this._azureConfig.deploymentName,
|
|
1646
|
+
apiVersion: this._azureConfig.apiVersion,
|
|
1647
|
+
messageCount: request.messages.length,
|
|
1648
|
+
stream: request.stream
|
|
1649
|
+
});
|
|
1650
|
+
return this._gatewayService.chat(gatewayRequest);
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Text completion using Azure OpenAI through the gateway
|
|
1654
|
+
*/
|
|
1655
|
+
complete(prompt, options) {
|
|
1656
|
+
const gatewayRequest = {
|
|
1657
|
+
model: options.model || this._azureConfig.deploymentName,
|
|
1658
|
+
prompt,
|
|
1659
|
+
temperature: options.temperature,
|
|
1660
|
+
max_tokens: options.max_tokens,
|
|
1661
|
+
stream: options.stream,
|
|
1662
|
+
stop: options.stop,
|
|
1663
|
+
provider: "azure-openai"
|
|
1664
|
+
};
|
|
1665
|
+
debugLogger.debug("Azure OpenAI Gateway completion request", {
|
|
1666
|
+
deploymentName: options.model || this._azureConfig.deploymentName,
|
|
1667
|
+
promptLength: prompt.length,
|
|
1668
|
+
stream: options.stream
|
|
1669
|
+
});
|
|
1670
|
+
return this._gatewayService.generate(gatewayRequest);
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* List available Azure OpenAI models through the gateway
|
|
1674
|
+
*/
|
|
1675
|
+
listModels() {
|
|
1676
|
+
debugLogger.debug("Fetching Azure OpenAI models through gateway");
|
|
1677
|
+
return this._gatewayService.listModelsByProvider("azure-openai");
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Get gateway health with Azure OpenAI provider status
|
|
1681
|
+
*/
|
|
1682
|
+
getHealth() {
|
|
1683
|
+
return this._gatewayService.getHealth().pipe(
|
|
1684
|
+
map8((health) => ({
|
|
1685
|
+
...health,
|
|
1686
|
+
azure_openai_status: health.providers.find((p) => p.name === "azure-openai")?.status || "unavailable",
|
|
1687
|
+
azure_config: {
|
|
1688
|
+
deploymentName: this._azureConfig.deploymentName,
|
|
1689
|
+
apiVersion: this._azureConfig.apiVersion
|
|
1690
|
+
}
|
|
1691
|
+
}))
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* Update Azure configuration
|
|
1696
|
+
*/
|
|
1697
|
+
updateAzureConfig(newConfig) {
|
|
1698
|
+
this._azureConfig = { ...this._azureConfig, ...newConfig };
|
|
1699
|
+
debugLogger.info("Azure OpenAI configuration updated", this._azureConfig);
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Get current Azure configuration
|
|
1703
|
+
*/
|
|
1704
|
+
getAzureConfig() {
|
|
1705
|
+
return { ...this._azureConfig };
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
|
|
1709
|
+
// src/services/gateway/anthropic-gateway.service.ts
|
|
1710
|
+
import { map as map9 } from "rxjs/operators";
|
|
1711
|
+
var AnthropicGatewayService = class {
|
|
1712
|
+
_gatewayService;
|
|
1713
|
+
constructor(gatewayUrl, tokenFactory) {
|
|
1714
|
+
this._gatewayService = new GatewayService(gatewayUrl, tokenFactory);
|
|
1715
|
+
debugLogger.info("AnthropicGatewayService initialized", { gatewayUrl });
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Validates the availability of the gateway service for Anthropic
|
|
1719
|
+
*/
|
|
1720
|
+
async validateServiceAvailability(args) {
|
|
1721
|
+
return this._gatewayService.validateServiceAvailability(args);
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Chat completion using Anthropic through the gateway
|
|
1725
|
+
*/
|
|
1726
|
+
chat(request) {
|
|
1727
|
+
const gatewayRequest = {
|
|
1728
|
+
model: request.model,
|
|
1729
|
+
messages: request.messages,
|
|
1730
|
+
stream: request.stream,
|
|
1731
|
+
temperature: request.temperature,
|
|
1732
|
+
max_tokens: request.max_tokens,
|
|
1733
|
+
top_p: request.top_p,
|
|
1734
|
+
stop: request.stop_sequences,
|
|
1735
|
+
provider: "anthropic"
|
|
1736
|
+
};
|
|
1737
|
+
debugLogger.debug("Anthropic Gateway chat request", {
|
|
1738
|
+
model: request.model,
|
|
1739
|
+
messageCount: request.messages.length,
|
|
1740
|
+
stream: request.stream,
|
|
1741
|
+
hasSystem: !!request.system
|
|
1742
|
+
});
|
|
1743
|
+
return this._gatewayService.chat(gatewayRequest);
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Text completion using Anthropic through the gateway
|
|
1747
|
+
*/
|
|
1748
|
+
complete(prompt, options) {
|
|
1749
|
+
const gatewayRequest = {
|
|
1750
|
+
model: options.model,
|
|
1751
|
+
prompt,
|
|
1752
|
+
temperature: options.temperature,
|
|
1753
|
+
max_tokens: options.max_tokens,
|
|
1754
|
+
stream: options.stream,
|
|
1755
|
+
stop: options.stop_sequences,
|
|
1756
|
+
provider: "anthropic"
|
|
1757
|
+
};
|
|
1758
|
+
debugLogger.debug("Anthropic Gateway completion request", {
|
|
1759
|
+
model: options.model,
|
|
1760
|
+
promptLength: prompt.length,
|
|
1761
|
+
stream: options.stream,
|
|
1762
|
+
hasSystem: !!options.system
|
|
1763
|
+
});
|
|
1764
|
+
return this._gatewayService.generate(gatewayRequest);
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* List available Anthropic models through the gateway
|
|
1768
|
+
*/
|
|
1769
|
+
listModels() {
|
|
1770
|
+
debugLogger.debug("Fetching Anthropic models through gateway");
|
|
1771
|
+
return this._gatewayService.listModelsByProvider("anthropic");
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Get gateway health with Anthropic provider status
|
|
1775
|
+
*/
|
|
1776
|
+
getHealth() {
|
|
1777
|
+
return this._gatewayService.getHealth().pipe(
|
|
1778
|
+
map9((health) => ({
|
|
1779
|
+
...health,
|
|
1780
|
+
anthropic_status: health.providers.find((p) => p.name === "anthropic")?.status || "unavailable"
|
|
1781
|
+
}))
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Helper method to convert system message to Anthropic format
|
|
1786
|
+
* Anthropic treats system messages differently - they can be separate from messages
|
|
1787
|
+
*/
|
|
1788
|
+
extractSystemMessage(messages) {
|
|
1789
|
+
const systemMessage = messages.find((msg) => msg.role === "system");
|
|
1790
|
+
const userMessages = messages.filter((msg) => msg.role !== "system");
|
|
1791
|
+
return {
|
|
1792
|
+
messages: userMessages,
|
|
1793
|
+
system: systemMessage?.content
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
/**
|
|
1797
|
+
* Enhanced chat method that handles Anthropic's system message format
|
|
1798
|
+
*/
|
|
1799
|
+
chatWithSystem(request) {
|
|
1800
|
+
const { messages, system } = this.extractSystemMessage(request.messages);
|
|
1801
|
+
const enhancedRequest = {
|
|
1802
|
+
...request,
|
|
1803
|
+
messages,
|
|
1804
|
+
system: system || request.system
|
|
1805
|
+
};
|
|
1806
|
+
return this.chat(enhancedRequest);
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1809
|
+
|
|
1810
|
+
// src/services/gateway/ollama-gateway.service.ts
|
|
1811
|
+
import { map as map10 } from "rxjs/operators";
|
|
1812
|
+
var OllamaGatewayService = class {
|
|
1813
|
+
_gatewayService;
|
|
1814
|
+
constructor(gatewayUrl, tokenFactory) {
|
|
1815
|
+
this._gatewayService = new GatewayService(gatewayUrl, tokenFactory);
|
|
1816
|
+
debugLogger.info("OllamaGatewayService initialized", { gatewayUrl });
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Validates the availability of the gateway service for Ollama
|
|
1820
|
+
*/
|
|
1821
|
+
async validateServiceAvailability(args) {
|
|
1822
|
+
return this._gatewayService.validateServiceAvailability(args);
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Chat completion using Ollama through the gateway
|
|
1826
|
+
*/
|
|
1827
|
+
chat(request) {
|
|
1828
|
+
const gatewayRequest = {
|
|
1829
|
+
...request,
|
|
1830
|
+
provider: "ollama"
|
|
1831
|
+
};
|
|
1832
|
+
debugLogger.debug("Ollama Gateway chat request", {
|
|
1833
|
+
model: request.model,
|
|
1834
|
+
messageCount: request.messages.length,
|
|
1835
|
+
stream: request.stream,
|
|
1836
|
+
hasImages: !!(request.images && request.images.length > 0)
|
|
1837
|
+
});
|
|
1838
|
+
return this._gatewayService.chat(gatewayRequest);
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* Text generation using Ollama through the gateway
|
|
1842
|
+
*/
|
|
1843
|
+
generate(request) {
|
|
1844
|
+
const gatewayRequest = {
|
|
1845
|
+
...request,
|
|
1846
|
+
provider: "ollama"
|
|
1847
|
+
};
|
|
1848
|
+
debugLogger.debug("Ollama Gateway generate request", {
|
|
1849
|
+
model: request.model,
|
|
1850
|
+
promptLength: request.prompt.length,
|
|
1851
|
+
stream: request.stream,
|
|
1852
|
+
hasImages: !!(request.images && request.images.length > 0)
|
|
1853
|
+
});
|
|
1854
|
+
return this._gatewayService.generate(gatewayRequest);
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* List available Ollama models through the gateway
|
|
1858
|
+
*/
|
|
1859
|
+
listModels() {
|
|
1860
|
+
debugLogger.debug("Fetching Ollama models through gateway");
|
|
1861
|
+
return this._gatewayService.listModelsByProvider("ollama");
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Get gateway health with Ollama provider status
|
|
1865
|
+
*/
|
|
1866
|
+
getHealth() {
|
|
1867
|
+
return this._gatewayService.getHealth().pipe(
|
|
1868
|
+
map10((health) => ({
|
|
1869
|
+
...health,
|
|
1870
|
+
ollama_status: health.providers.find((p) => p.name === "ollama")?.status || "unavailable"
|
|
1871
|
+
}))
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
};
|
|
1875
|
+
|
|
1876
|
+
// src/services/ai-provider/providers/gateway.provider.ts
|
|
1877
|
+
var GatewayProvider = class {
|
|
1878
|
+
config;
|
|
1879
|
+
gatewayService;
|
|
1880
|
+
providerSpecificService = null;
|
|
1881
|
+
constructor(config) {
|
|
1882
|
+
this.config = config;
|
|
1883
|
+
if (!config.gatewayUrl) {
|
|
1884
|
+
throw new Error("Gateway provider requires gatewayUrl in config");
|
|
1885
|
+
}
|
|
1886
|
+
if (!config.provider) {
|
|
1887
|
+
throw new Error("Gateway provider requires provider field to specify backend (openai, azure-openai, anthropic, ollama)");
|
|
1888
|
+
}
|
|
1889
|
+
const tokenFactory = config.tokenFactory || (() => null);
|
|
1890
|
+
this.gatewayService = new GatewayService(config.gatewayUrl, tokenFactory);
|
|
1891
|
+
this.createProviderSpecificService(config.gatewayUrl, tokenFactory);
|
|
1892
|
+
debugLogger.info("GatewayProvider initialized", {
|
|
1893
|
+
gatewayUrl: config.gatewayUrl,
|
|
1894
|
+
backendProvider: config.provider
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
createProviderSpecificService(gatewayUrl, tokenFactory) {
|
|
1898
|
+
switch (this.config.provider) {
|
|
1899
|
+
case "openai":
|
|
1900
|
+
this.providerSpecificService = new OpenAIGatewayService(gatewayUrl, tokenFactory);
|
|
1901
|
+
break;
|
|
1902
|
+
case "azure-openai":
|
|
1903
|
+
if (!this.config.deploymentName || !this.config.apiVersion) {
|
|
1904
|
+
throw new Error("Azure OpenAI gateway provider requires deploymentName and apiVersion");
|
|
1905
|
+
}
|
|
1906
|
+
this.providerSpecificService = new AzureOpenAIGatewayService(
|
|
1907
|
+
gatewayUrl,
|
|
1908
|
+
tokenFactory,
|
|
1909
|
+
{
|
|
1910
|
+
deploymentName: this.config.deploymentName,
|
|
1911
|
+
apiVersion: this.config.apiVersion
|
|
1912
|
+
}
|
|
1913
|
+
);
|
|
1914
|
+
break;
|
|
1915
|
+
case "anthropic":
|
|
1916
|
+
this.providerSpecificService = new AnthropicGatewayService(gatewayUrl, tokenFactory);
|
|
1917
|
+
break;
|
|
1918
|
+
case "ollama":
|
|
1919
|
+
this.providerSpecificService = new OllamaGatewayService(gatewayUrl, tokenFactory);
|
|
1920
|
+
break;
|
|
1921
|
+
default:
|
|
1922
|
+
debugLogger.warn("Unknown provider for gateway, using generic gateway service", {
|
|
1923
|
+
provider: this.config.provider
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
chat(request) {
|
|
1928
|
+
const messages = request.messages.map((msg) => ({
|
|
1929
|
+
role: msg.role,
|
|
1930
|
+
content: msg.content
|
|
1931
|
+
}));
|
|
1932
|
+
if (request.images && request.images.length > 0) {
|
|
1933
|
+
const lastUserMessageIndex = messages.map((m) => m.role).lastIndexOf("user");
|
|
1934
|
+
if (this.config.provider === "ollama") {
|
|
1935
|
+
if (lastUserMessageIndex !== -1) {
|
|
1936
|
+
messages[lastUserMessageIndex] = {
|
|
1937
|
+
...messages[lastUserMessageIndex],
|
|
1938
|
+
images: request.images
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
} else if (["openai", "azure-openai", "anthropic"].includes(this.config.provider || "")) {
|
|
1942
|
+
if (lastUserMessageIndex !== -1) {
|
|
1943
|
+
const currentMessage = messages[lastUserMessageIndex];
|
|
1944
|
+
const contentArray = [
|
|
1945
|
+
{
|
|
1946
|
+
type: "text",
|
|
1947
|
+
text: currentMessage.content
|
|
1948
|
+
}
|
|
1949
|
+
];
|
|
1950
|
+
request.images.forEach((base64Image) => {
|
|
1951
|
+
contentArray.push({
|
|
1952
|
+
type: "image_url",
|
|
1953
|
+
image_url: {
|
|
1954
|
+
url: base64Image.startsWith("data:") ? base64Image : `data:image/jpeg;base64,${base64Image}`,
|
|
1955
|
+
detail: "auto"
|
|
1956
|
+
}
|
|
1957
|
+
});
|
|
1958
|
+
});
|
|
1959
|
+
messages[lastUserMessageIndex] = {
|
|
1960
|
+
...messages[lastUserMessageIndex],
|
|
1961
|
+
content: contentArray
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
const gatewayRequest = {
|
|
1967
|
+
model: request.model,
|
|
1968
|
+
messages,
|
|
1969
|
+
stream: request.stream,
|
|
1970
|
+
temperature: request.temperature,
|
|
1971
|
+
max_tokens: request.maxTokens,
|
|
1972
|
+
provider: this.config.provider,
|
|
1973
|
+
// Only include top-level images for Ollama (fallback)
|
|
1974
|
+
images: this.config.provider === "ollama" ? request.images : void 0
|
|
1975
|
+
};
|
|
1976
|
+
debugLogger.debug("Gateway provider chat request", {
|
|
1977
|
+
model: request.model,
|
|
1978
|
+
provider: this.config.provider,
|
|
1979
|
+
messageCount: request.messages.length,
|
|
1980
|
+
stream: request.stream,
|
|
1981
|
+
hasImages: !!(request.images && request.images.length > 0),
|
|
1982
|
+
imageCount: request.images?.length || 0,
|
|
1983
|
+
imageStrategy: this.config.provider === "ollama" ? "message-level-array" : ["openai", "azure-openai", "anthropic"].includes(this.config.provider || "") ? "structured-content" : "top-level-fallback",
|
|
1984
|
+
finalMessages: messages.map((m) => ({
|
|
1985
|
+
role: m.role,
|
|
1986
|
+
hasImages: Array.isArray(m.images) && m.images.length > 0,
|
|
1987
|
+
contentType: Array.isArray(m.content) ? "structured" : "text"
|
|
1988
|
+
}))
|
|
1989
|
+
});
|
|
1990
|
+
return this.gatewayService.chat(gatewayRequest).pipe(
|
|
1991
|
+
map11((response) => ({
|
|
1992
|
+
message: {
|
|
1993
|
+
content: response.choices?.[0]?.message?.content || response.choices?.[0]?.delta?.content || "",
|
|
1994
|
+
role: "assistant"
|
|
1995
|
+
},
|
|
1996
|
+
done: response.choices?.[0]?.finish_reason === "stop" || response.choices?.[0]?.finish_reason === "length"
|
|
1997
|
+
}))
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
generate(request) {
|
|
2001
|
+
const gatewayRequest = {
|
|
2002
|
+
model: request.model,
|
|
2003
|
+
prompt: request.prompt,
|
|
2004
|
+
stream: request.stream,
|
|
2005
|
+
provider: this.config.provider
|
|
2006
|
+
};
|
|
2007
|
+
debugLogger.debug("Gateway provider generate request", {
|
|
2008
|
+
model: request.model,
|
|
2009
|
+
provider: this.config.provider,
|
|
2010
|
+
promptLength: request.prompt.length,
|
|
2011
|
+
stream: request.stream
|
|
2012
|
+
});
|
|
2013
|
+
return this.gatewayService.generate(gatewayRequest).pipe(
|
|
2014
|
+
map11((response) => ({
|
|
2015
|
+
response: response.response || "",
|
|
2016
|
+
done: response.done || false
|
|
2017
|
+
}))
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
listModels() {
|
|
2021
|
+
debugLogger.debug("Gateway provider listing models", { provider: this.config.provider });
|
|
2022
|
+
if (this.config.provider) {
|
|
2023
|
+
return this.gatewayService.listModelsByProvider(this.config.provider).pipe(
|
|
2024
|
+
map11((models) => models.map((model) => ({
|
|
2025
|
+
name: model.id || model.name,
|
|
2026
|
+
size: model.size,
|
|
2027
|
+
details: model.details,
|
|
2028
|
+
digest: model.digest,
|
|
2029
|
+
modified_at: model.modified_at
|
|
2030
|
+
})))
|
|
2031
|
+
);
|
|
2032
|
+
} else {
|
|
2033
|
+
return this.gatewayService.listModels().pipe(
|
|
2034
|
+
map11((models) => models.map((model) => ({
|
|
2035
|
+
name: model.id || model.name,
|
|
2036
|
+
size: model.size,
|
|
2037
|
+
details: model.details,
|
|
2038
|
+
digest: model.digest,
|
|
2039
|
+
modified_at: model.modified_at
|
|
2040
|
+
})))
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
async validateServiceAvailability(args) {
|
|
2045
|
+
debugLogger.debug("Gateway provider validating service availability");
|
|
2046
|
+
return this.gatewayService.validateServiceAvailability(args);
|
|
2047
|
+
}
|
|
2048
|
+
getProviderType() {
|
|
2049
|
+
return "gateway" /* GATEWAY */;
|
|
2050
|
+
}
|
|
2051
|
+
getConfig() {
|
|
2052
|
+
return this.config;
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Get the backend provider type
|
|
2056
|
+
*/
|
|
2057
|
+
getBackendProvider() {
|
|
2058
|
+
return this.config.provider;
|
|
2059
|
+
}
|
|
2060
|
+
/**
|
|
2061
|
+
* Get gateway health including backend provider status
|
|
2062
|
+
*/
|
|
2063
|
+
getHealth() {
|
|
2064
|
+
return this.gatewayService.getHealth().pipe(
|
|
2065
|
+
map11((health) => ({
|
|
2066
|
+
...health,
|
|
2067
|
+
backend_provider: this.config.provider,
|
|
2068
|
+
backend_provider_status: health.providers.find((p) => p.name === this.config.provider)?.status || "unavailable"
|
|
2069
|
+
}))
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
/**
|
|
2073
|
+
* Use provider-specific service if available for enhanced functionality
|
|
2074
|
+
*/
|
|
2075
|
+
getProviderSpecificService() {
|
|
2076
|
+
return this.providerSpecificService;
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
|
|
2080
|
+
// src/services/ai-provider/ai-provider.factory.ts
|
|
2081
|
+
var AIProviderFactory = class {
|
|
2082
|
+
static createProvider(config) {
|
|
2083
|
+
switch (config.type) {
|
|
2084
|
+
case "ollama" /* OLLAMA */:
|
|
2085
|
+
return new OllamaProvider(config);
|
|
2086
|
+
case "openai" /* OPENAI */:
|
|
2087
|
+
return new OpenAIProvider(config);
|
|
2088
|
+
case "azure-openai" /* AZURE_OPENAI */:
|
|
2089
|
+
return new AzureOpenAIProvider(config);
|
|
2090
|
+
case "anthropic" /* ANTHROPIC */:
|
|
2091
|
+
return new AnthropicProvider(config);
|
|
2092
|
+
case "gateway" /* GATEWAY */:
|
|
2093
|
+
return new GatewayProvider(config);
|
|
2094
|
+
default:
|
|
2095
|
+
throw new Error(`Unsupported AI provider type: ${config.type}`);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
static getSupportedProviders() {
|
|
2099
|
+
return [
|
|
2100
|
+
"ollama" /* OLLAMA */,
|
|
2101
|
+
"openai" /* OPENAI */,
|
|
2102
|
+
"azure-openai" /* AZURE_OPENAI */,
|
|
2103
|
+
"anthropic" /* ANTHROPIC */,
|
|
2104
|
+
"gateway" /* GATEWAY */
|
|
2105
|
+
];
|
|
2106
|
+
}
|
|
2107
|
+
static validateConfig(config) {
|
|
2108
|
+
switch (config.type) {
|
|
2109
|
+
case "ollama" /* OLLAMA */:
|
|
2110
|
+
return true;
|
|
2111
|
+
// Ollama only needs baseUrl which has defaults
|
|
2112
|
+
case "openai" /* OPENAI */:
|
|
2113
|
+
return !!config.apiKey;
|
|
2114
|
+
case "azure-openai" /* AZURE_OPENAI */:
|
|
2115
|
+
return !!(config.baseUrl && config.apiKey && config.apiVersion && config.deploymentName);
|
|
2116
|
+
case "anthropic" /* ANTHROPIC */:
|
|
2117
|
+
return !!config.apiKey;
|
|
2118
|
+
case "gateway" /* GATEWAY */:
|
|
2119
|
+
return !!(config.gatewayUrl && config.provider);
|
|
2120
|
+
default:
|
|
2121
|
+
return false;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
};
|
|
2125
|
+
|
|
2126
|
+
// src/store/aiProviderStore.ts
|
|
2127
|
+
var useAIProviderStore = create((set, get) => ({
|
|
2128
|
+
provider: null,
|
|
2129
|
+
config: null,
|
|
2130
|
+
setProvider: (provider, config) => set({ provider, config }),
|
|
2131
|
+
createProvider: (config) => {
|
|
2132
|
+
try {
|
|
2133
|
+
const provider = AIProviderFactory.createProvider(config);
|
|
2134
|
+
set({ provider, config });
|
|
2135
|
+
} catch (error) {
|
|
2136
|
+
debugLogger.error("Failed to create AI provider:", { error });
|
|
2137
|
+
throw error;
|
|
2138
|
+
}
|
|
2139
|
+
},
|
|
2140
|
+
switchProvider: (config) => {
|
|
2141
|
+
const currentProvider = get().provider;
|
|
2142
|
+
try {
|
|
2143
|
+
const newProvider = AIProviderFactory.createProvider(config);
|
|
2144
|
+
set({ provider: newProvider, config });
|
|
2145
|
+
} catch (error) {
|
|
2146
|
+
debugLogger.error("Failed to switch AI provider:", { error });
|
|
2147
|
+
throw error;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
}));
|
|
2151
|
+
|
|
2152
|
+
export {
|
|
2153
|
+
GatewayService,
|
|
2154
|
+
AIProviderFactory,
|
|
2155
|
+
useAIProviderStore
|
|
2156
|
+
};
|
|
2157
|
+
//# sourceMappingURL=chunk-AVC6IZJQ.mjs.map
|