@lukaplayground/aikit 1.0.0
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 +21 -0
- package/README.md +100 -0
- package/dist/aikit.js +1967 -0
- package/dist/aikit.js.map +1 -0
- package/dist/aikit.min.js +8 -0
- package/dist/aikit.min.js.map +1 -0
- package/package.json +55 -0
package/dist/aikit.js
ADDED
|
@@ -0,0 +1,1967 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* AIKit v0.1.0
|
|
3
|
+
* Universal AI API Client Framework
|
|
4
|
+
* (c) 2025 Luka (lukaPlayground)
|
|
5
|
+
* Released under the MIT License
|
|
6
|
+
*/
|
|
7
|
+
(function (global, factory) {
|
|
8
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
|
9
|
+
typeof define === 'function' && define.amd ? define(factory) :
|
|
10
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.AIKit = factory());
|
|
11
|
+
})(this, (function () { 'use strict';
|
|
12
|
+
|
|
13
|
+
function getDefaultExportFromCjs (x) {
|
|
14
|
+
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
var src = {exports: {}};
|
|
18
|
+
|
|
19
|
+
var CacheManager$1 = {exports: {}};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* CacheManager - LocalStorage based caching system
|
|
23
|
+
* @class
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
(function (module) {
|
|
27
|
+
class CacheManager {
|
|
28
|
+
constructor(options = {}) {
|
|
29
|
+
this.prefix = options.prefix || 'aikit_cache_';
|
|
30
|
+
this.maxAge = options.maxAge || 3600000; // 1 hour default
|
|
31
|
+
this.maxSize = options.maxSize || 100; // Maximum cache entries
|
|
32
|
+
|
|
33
|
+
this.storage = typeof window !== 'undefined' && window.localStorage
|
|
34
|
+
? window.localStorage
|
|
35
|
+
: new MemoryStorage();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generate cache key from message and options
|
|
40
|
+
*/
|
|
41
|
+
generateKey(message, options = {}) {
|
|
42
|
+
const normalized = {
|
|
43
|
+
message: message.trim().toLowerCase(),
|
|
44
|
+
...options
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const str = JSON.stringify(normalized);
|
|
48
|
+
return this.prefix + this.hashCode(str);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Simple hash function
|
|
53
|
+
* @private
|
|
54
|
+
*/
|
|
55
|
+
hashCode(str) {
|
|
56
|
+
let hash = 0;
|
|
57
|
+
for (let i = 0; i < str.length; i++) {
|
|
58
|
+
const char = str.charCodeAt(i);
|
|
59
|
+
hash = ((hash << 5) - hash) + char;
|
|
60
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
61
|
+
}
|
|
62
|
+
return Math.abs(hash).toString(36);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get cached item
|
|
67
|
+
*/
|
|
68
|
+
get(key) {
|
|
69
|
+
try {
|
|
70
|
+
const item = this.storage.getItem(key);
|
|
71
|
+
if (!item) return null;
|
|
72
|
+
|
|
73
|
+
const parsed = JSON.parse(item);
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
|
|
76
|
+
// Check expiration
|
|
77
|
+
if (now - parsed.timestamp > this.maxAge) {
|
|
78
|
+
this.storage.removeItem(key);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return parsed.data;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.warn('AIKit CacheManager: Error reading cache', error);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Set cache item
|
|
91
|
+
*/
|
|
92
|
+
set(key, data) {
|
|
93
|
+
try {
|
|
94
|
+
// Check size limit
|
|
95
|
+
this.enforceMaxSize();
|
|
96
|
+
|
|
97
|
+
const item = {
|
|
98
|
+
data,
|
|
99
|
+
timestamp: Date.now()
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
this.storage.setItem(key, JSON.stringify(item));
|
|
103
|
+
return true;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.warn('AIKit CacheManager: Error writing cache', error);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Enforce maximum cache size
|
|
112
|
+
* @private
|
|
113
|
+
*/
|
|
114
|
+
enforceMaxSize() {
|
|
115
|
+
const keys = this.getKeys();
|
|
116
|
+
|
|
117
|
+
if (keys.length >= this.maxSize) {
|
|
118
|
+
// Remove oldest entries
|
|
119
|
+
const entries = keys.map(key => {
|
|
120
|
+
const item = this.storage.getItem(key);
|
|
121
|
+
const parsed = JSON.parse(item);
|
|
122
|
+
return { key, timestamp: parsed.timestamp };
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
entries.sort((a, b) => a.timestamp - b.timestamp);
|
|
126
|
+
|
|
127
|
+
// Remove oldest 20%
|
|
128
|
+
const toRemove = Math.ceil(this.maxSize * 0.2);
|
|
129
|
+
for (let i = 0; i < toRemove; i++) {
|
|
130
|
+
this.storage.removeItem(entries[i].key);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get all cache keys
|
|
137
|
+
* @private
|
|
138
|
+
*/
|
|
139
|
+
getKeys() {
|
|
140
|
+
const keys = [];
|
|
141
|
+
for (let i = 0; i < this.storage.length; i++) {
|
|
142
|
+
const key = this.storage.key(i);
|
|
143
|
+
if (key && key.startsWith(this.prefix)) {
|
|
144
|
+
keys.push(key);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return keys;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Clear all cache
|
|
152
|
+
*/
|
|
153
|
+
clear() {
|
|
154
|
+
const keys = this.getKeys();
|
|
155
|
+
keys.forEach(key => this.storage.removeItem(key));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get cache statistics
|
|
160
|
+
*/
|
|
161
|
+
getStats() {
|
|
162
|
+
const keys = this.getKeys();
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
|
|
165
|
+
let totalSize = 0;
|
|
166
|
+
let validEntries = 0;
|
|
167
|
+
|
|
168
|
+
keys.forEach(key => {
|
|
169
|
+
const item = this.storage.getItem(key);
|
|
170
|
+
if (item) {
|
|
171
|
+
totalSize += item.length;
|
|
172
|
+
const parsed = JSON.parse(item);
|
|
173
|
+
if (now - parsed.timestamp <= this.maxAge) {
|
|
174
|
+
validEntries++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
totalEntries: keys.length,
|
|
181
|
+
validEntries,
|
|
182
|
+
expiredEntries: keys.length - validEntries,
|
|
183
|
+
totalSize,
|
|
184
|
+
maxAge: this.maxAge,
|
|
185
|
+
maxSize: this.maxSize
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Memory-based storage fallback (for Node.js or browsers without localStorage)
|
|
192
|
+
* @class
|
|
193
|
+
*/
|
|
194
|
+
class MemoryStorage {
|
|
195
|
+
constructor() {
|
|
196
|
+
this.storage = new Map();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
getItem(key) {
|
|
200
|
+
return this.storage.get(key) || null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
setItem(key, value) {
|
|
204
|
+
this.storage.set(key, value);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
removeItem(key) {
|
|
208
|
+
this.storage.delete(key);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
key(index) {
|
|
212
|
+
return Array.from(this.storage.keys())[index];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
get length() {
|
|
216
|
+
return this.storage.size;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
clear() {
|
|
220
|
+
this.storage.clear();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Export
|
|
225
|
+
if (module.exports) {
|
|
226
|
+
module.exports = CacheManager;
|
|
227
|
+
}
|
|
228
|
+
} (CacheManager$1));
|
|
229
|
+
|
|
230
|
+
var CacheManagerExports = CacheManager$1.exports;
|
|
231
|
+
|
|
232
|
+
var CostTracker$1 = {exports: {}};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* CostTracker - Track API usage costs
|
|
236
|
+
* @class
|
|
237
|
+
*/
|
|
238
|
+
|
|
239
|
+
(function (module) {
|
|
240
|
+
class CostTracker {
|
|
241
|
+
constructor(options = {}) {
|
|
242
|
+
this.storageKey = options.storageKey || 'aikit_cost_tracker';
|
|
243
|
+
this.storage = typeof window !== 'undefined' && window.localStorage
|
|
244
|
+
? window.localStorage
|
|
245
|
+
: new Map();
|
|
246
|
+
|
|
247
|
+
// 토큰당 가격 (USD) - 2024년 기준 근사치
|
|
248
|
+
this.pricing = {
|
|
249
|
+
openai: {
|
|
250
|
+
'gpt-4': { input: 0.03 / 1000, output: 0.06 / 1000 },
|
|
251
|
+
'gpt-4-turbo': { input: 0.01 / 1000, output: 0.03 / 1000 },
|
|
252
|
+
'gpt-3.5-turbo': { input: 0.0005 / 1000, output: 0.0015 / 1000 }
|
|
253
|
+
},
|
|
254
|
+
claude: {
|
|
255
|
+
'claude-3-opus': { input: 0.015 / 1000, output: 0.075 / 1000 },
|
|
256
|
+
'claude-3-sonnet': { input: 0.003 / 1000, output: 0.015 / 1000 },
|
|
257
|
+
'claude-3-haiku': { input: 0.00025 / 1000, output: 0.00125 / 1000 }
|
|
258
|
+
},
|
|
259
|
+
gemini: {
|
|
260
|
+
'gemini-pro': { input: 0.00025 / 1000, output: 0.0005 / 1000 },
|
|
261
|
+
'gemini-ultra': { input: 0.0005 / 1000, output: 0.001 / 1000 }
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
this.loadData();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Load existing tracking data
|
|
270
|
+
* @private
|
|
271
|
+
*/
|
|
272
|
+
loadData() {
|
|
273
|
+
try {
|
|
274
|
+
if (this.storage instanceof Map) {
|
|
275
|
+
this.data = this.storage.get(this.storageKey) || this.initializeData();
|
|
276
|
+
} else {
|
|
277
|
+
const stored = this.storage.getItem(this.storageKey);
|
|
278
|
+
this.data = stored ? JSON.parse(stored) : this.initializeData();
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.warn('AIKit CostTracker: Error loading data', error);
|
|
282
|
+
this.data = this.initializeData();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Initialize empty data structure
|
|
288
|
+
* @private
|
|
289
|
+
*/
|
|
290
|
+
initializeData() {
|
|
291
|
+
return {
|
|
292
|
+
total: 0,
|
|
293
|
+
byProvider: {},
|
|
294
|
+
byModel: {},
|
|
295
|
+
requests: [],
|
|
296
|
+
startDate: new Date().toISOString()
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Save tracking data
|
|
302
|
+
* @private
|
|
303
|
+
*/
|
|
304
|
+
saveData() {
|
|
305
|
+
try {
|
|
306
|
+
if (this.storage instanceof Map) {
|
|
307
|
+
this.storage.set(this.storageKey, this.data);
|
|
308
|
+
} else {
|
|
309
|
+
this.storage.setItem(this.storageKey, JSON.stringify(this.data));
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.warn('AIKit CostTracker: Error saving data', error);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Track a request
|
|
318
|
+
* @param {Object} request
|
|
319
|
+
*/
|
|
320
|
+
track(request) {
|
|
321
|
+
const { provider, model, tokens = {}, timestamp } = request;
|
|
322
|
+
|
|
323
|
+
// Calculate cost
|
|
324
|
+
const cost = this.calculateCost(provider, model, tokens);
|
|
325
|
+
|
|
326
|
+
// Update totals
|
|
327
|
+
this.data.total += cost;
|
|
328
|
+
|
|
329
|
+
// Update by provider
|
|
330
|
+
if (!this.data.byProvider[provider]) {
|
|
331
|
+
this.data.byProvider[provider] = 0;
|
|
332
|
+
}
|
|
333
|
+
this.data.byProvider[provider] += cost;
|
|
334
|
+
|
|
335
|
+
// Update by model
|
|
336
|
+
const modelKey = `${provider}:${model || 'default'}`;
|
|
337
|
+
if (!this.data.byModel[modelKey]) {
|
|
338
|
+
this.data.byModel[modelKey] = 0;
|
|
339
|
+
}
|
|
340
|
+
this.data.byModel[modelKey] += cost;
|
|
341
|
+
|
|
342
|
+
// Add to request history (keep last 100)
|
|
343
|
+
this.data.requests.push({
|
|
344
|
+
provider,
|
|
345
|
+
model,
|
|
346
|
+
tokens,
|
|
347
|
+
cost,
|
|
348
|
+
timestamp: timestamp || new Date().toISOString()
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
if (this.data.requests.length > 100) {
|
|
352
|
+
this.data.requests.shift();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
this.saveData();
|
|
356
|
+
|
|
357
|
+
return cost;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Calculate cost for a request
|
|
362
|
+
* @private
|
|
363
|
+
*/
|
|
364
|
+
calculateCost(provider, model, tokens) {
|
|
365
|
+
const providerPricing = this.pricing[provider.toLowerCase()];
|
|
366
|
+
if (!providerPricing) {
|
|
367
|
+
console.warn(`AIKit CostTracker: Unknown provider '${provider}'`);
|
|
368
|
+
return 0;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Find matching model pricing
|
|
372
|
+
let modelPricing;
|
|
373
|
+
if (model) {
|
|
374
|
+
const modelKey = Object.keys(providerPricing).find(key =>
|
|
375
|
+
model.toLowerCase().includes(key)
|
|
376
|
+
);
|
|
377
|
+
modelPricing = providerPricing[modelKey];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Fallback to first available pricing
|
|
381
|
+
if (!modelPricing) {
|
|
382
|
+
modelPricing = Object.values(providerPricing)[0];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const inputCost = (tokens.input || tokens.prompt_tokens || 0) * modelPricing.input;
|
|
386
|
+
const outputCost = (tokens.output || tokens.completion_tokens || 0) * modelPricing.output;
|
|
387
|
+
|
|
388
|
+
return inputCost + outputCost;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get cost report
|
|
393
|
+
*/
|
|
394
|
+
getReport() {
|
|
395
|
+
const daysSinceStart = Math.ceil(
|
|
396
|
+
(new Date() - new Date(this.data.startDate)) / (1000 * 60 * 60 * 24)
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
total: this.data.total.toFixed(4),
|
|
401
|
+
totalUSD: `$${this.data.total.toFixed(4)}`,
|
|
402
|
+
byProvider: Object.entries(this.data.byProvider).reduce((acc, [key, value]) => {
|
|
403
|
+
acc[key] = `$${value.toFixed(4)}`;
|
|
404
|
+
return acc;
|
|
405
|
+
}, {}),
|
|
406
|
+
byModel: Object.entries(this.data.byModel).reduce((acc, [key, value]) => {
|
|
407
|
+
acc[key] = `$${value.toFixed(4)}`;
|
|
408
|
+
return acc;
|
|
409
|
+
}, {}),
|
|
410
|
+
totalRequests: this.data.requests.length,
|
|
411
|
+
averageCostPerRequest: this.data.requests.length > 0
|
|
412
|
+
? `$${(this.data.total / this.data.requests.length).toFixed(6)}`
|
|
413
|
+
: '$0',
|
|
414
|
+
dailyAverage: `$${(this.data.total / Math.max(daysSinceStart, 1)).toFixed(4)}`,
|
|
415
|
+
startDate: this.data.startDate,
|
|
416
|
+
lastRequest: this.data.requests.length > 0
|
|
417
|
+
? this.data.requests[this.data.requests.length - 1].timestamp
|
|
418
|
+
: null
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Get recent requests
|
|
424
|
+
*/
|
|
425
|
+
getRecentRequests(limit = 10) {
|
|
426
|
+
return this.data.requests.slice(-limit).reverse();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Reset all tracking data
|
|
431
|
+
*/
|
|
432
|
+
reset() {
|
|
433
|
+
this.data = this.initializeData();
|
|
434
|
+
this.saveData();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Update pricing for a provider/model
|
|
439
|
+
*/
|
|
440
|
+
updatePricing(provider, model, inputPrice, outputPrice) {
|
|
441
|
+
if (!this.pricing[provider]) {
|
|
442
|
+
this.pricing[provider] = {};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
this.pricing[provider][model] = {
|
|
446
|
+
input: inputPrice,
|
|
447
|
+
output: outputPrice
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Export data as JSON
|
|
453
|
+
*/
|
|
454
|
+
exportData() {
|
|
455
|
+
return JSON.stringify(this.data, null, 2);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Import data from JSON
|
|
460
|
+
*/
|
|
461
|
+
importData(jsonString) {
|
|
462
|
+
try {
|
|
463
|
+
this.data = JSON.parse(jsonString);
|
|
464
|
+
this.saveData();
|
|
465
|
+
return true;
|
|
466
|
+
} catch (error) {
|
|
467
|
+
console.error('AIKit CostTracker: Error importing data', error);
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Export
|
|
474
|
+
if (module.exports) {
|
|
475
|
+
module.exports = CostTracker;
|
|
476
|
+
}
|
|
477
|
+
} (CostTracker$1));
|
|
478
|
+
|
|
479
|
+
var CostTrackerExports = CostTracker$1.exports;
|
|
480
|
+
|
|
481
|
+
var ResponseValidator$1 = {exports: {}};
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* ResponseValidator - Validate AI responses based on rules
|
|
485
|
+
* QA-focused validation layer
|
|
486
|
+
* @class
|
|
487
|
+
*/
|
|
488
|
+
|
|
489
|
+
(function (module) {
|
|
490
|
+
class ResponseValidator {
|
|
491
|
+
constructor() {
|
|
492
|
+
this.rules = new Map();
|
|
493
|
+
this.loadDefaultRules();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Load default validation rules
|
|
498
|
+
* @private
|
|
499
|
+
*/
|
|
500
|
+
loadDefaultRules() {
|
|
501
|
+
// Length validation
|
|
502
|
+
this.addRule('maxLength', (response, limit) => {
|
|
503
|
+
const text = this.extractText(response);
|
|
504
|
+
if (text.length > limit) {
|
|
505
|
+
return {
|
|
506
|
+
valid: false,
|
|
507
|
+
error: `Response exceeds maximum length (${text.length} > ${limit})`
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
return { valid: true };
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
this.addRule('minLength', (response, limit) => {
|
|
514
|
+
const text = this.extractText(response);
|
|
515
|
+
if (text.length < limit) {
|
|
516
|
+
return {
|
|
517
|
+
valid: false,
|
|
518
|
+
error: `Response below minimum length (${text.length} < ${limit})`
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
return { valid: true };
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Content validation
|
|
525
|
+
this.addRule('mustInclude', (response, keywords) => {
|
|
526
|
+
const text = this.extractText(response).toLowerCase();
|
|
527
|
+
const missing = keywords.filter(kw => !text.includes(kw.toLowerCase()));
|
|
528
|
+
|
|
529
|
+
if (missing.length > 0) {
|
|
530
|
+
return {
|
|
531
|
+
valid: false,
|
|
532
|
+
error: `Response missing required keywords: ${missing.join(', ')}`
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
return { valid: true };
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
this.addRule('mustNotInclude', (response, keywords) => {
|
|
539
|
+
const text = this.extractText(response).toLowerCase();
|
|
540
|
+
const found = keywords.filter(kw => text.includes(kw.toLowerCase()));
|
|
541
|
+
|
|
542
|
+
if (found.length > 0) {
|
|
543
|
+
return {
|
|
544
|
+
valid: false,
|
|
545
|
+
error: `Response contains forbidden keywords: ${found.join(', ')}`
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
return { valid: true };
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// Format validation
|
|
552
|
+
this.addRule('format', (response, format) => {
|
|
553
|
+
const text = this.extractText(response);
|
|
554
|
+
|
|
555
|
+
const formats = {
|
|
556
|
+
json: () => {
|
|
557
|
+
try {
|
|
558
|
+
JSON.parse(text);
|
|
559
|
+
return { valid: true };
|
|
560
|
+
} catch (e) {
|
|
561
|
+
return { valid: false, error: 'Response is not valid JSON' };
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
email: () => {
|
|
565
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
566
|
+
const hasEmail = emailRegex.test(text);
|
|
567
|
+
return hasEmail
|
|
568
|
+
? { valid: true }
|
|
569
|
+
: { valid: false, error: 'Response does not contain valid email' };
|
|
570
|
+
},
|
|
571
|
+
url: () => {
|
|
572
|
+
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
|
573
|
+
const hasUrl = urlRegex.test(text);
|
|
574
|
+
return hasUrl
|
|
575
|
+
? { valid: true }
|
|
576
|
+
: { valid: false, error: 'Response does not contain valid URL' };
|
|
577
|
+
},
|
|
578
|
+
number: () => {
|
|
579
|
+
const hasNumber = /\d+/.test(text);
|
|
580
|
+
return hasNumber
|
|
581
|
+
? { valid: true }
|
|
582
|
+
: { valid: false, error: 'Response does not contain numbers' };
|
|
583
|
+
},
|
|
584
|
+
markdown: () => {
|
|
585
|
+
const hasMarkdown = /[#*_`\[\]]/.test(text);
|
|
586
|
+
return hasMarkdown
|
|
587
|
+
? { valid: true }
|
|
588
|
+
: { valid: false, error: 'Response does not contain markdown formatting' };
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const validator = formats[format.toLowerCase()];
|
|
593
|
+
if (!validator) {
|
|
594
|
+
return { valid: false, error: `Unknown format: ${format}` };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return validator();
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// Language validation
|
|
601
|
+
this.addRule('language', (response, lang) => {
|
|
602
|
+
const text = this.extractText(response);
|
|
603
|
+
|
|
604
|
+
const patterns = {
|
|
605
|
+
korean: /[가-힣]/,
|
|
606
|
+
english: /[a-zA-Z]/,
|
|
607
|
+
japanese: /[ぁ-んァ-ン]/,
|
|
608
|
+
chinese: /[\u4e00-\u9fff]/,
|
|
609
|
+
numbers: /\d/
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
const pattern = patterns[lang.toLowerCase()];
|
|
613
|
+
if (!pattern) {
|
|
614
|
+
return { valid: false, error: `Unknown language: ${lang}` };
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (!pattern.test(text)) {
|
|
618
|
+
return {
|
|
619
|
+
valid: false,
|
|
620
|
+
error: `Response does not contain ${lang} characters`
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return { valid: true };
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Sentiment validation
|
|
628
|
+
this.addRule('sentiment', (response, expectedSentiment) => {
|
|
629
|
+
const text = this.extractText(response).toLowerCase();
|
|
630
|
+
|
|
631
|
+
const sentimentWords = {
|
|
632
|
+
positive: ['good', 'great', 'excellent', 'amazing', 'wonderful', 'fantastic', '좋', '훌륭', '멋진'],
|
|
633
|
+
negative: ['bad', 'poor', 'terrible', 'awful', 'horrible', '나쁜', '끔찍', '최악'],
|
|
634
|
+
neutral: ['okay', 'fine', 'acceptable', '괜찮', '무난']
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const words = sentimentWords[expectedSentiment.toLowerCase()] || [];
|
|
638
|
+
const hasSentiment = words.some(word => text.includes(word));
|
|
639
|
+
|
|
640
|
+
if (!hasSentiment) {
|
|
641
|
+
return {
|
|
642
|
+
valid: false,
|
|
643
|
+
error: `Response does not match expected sentiment: ${expectedSentiment}`
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return { valid: true };
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// Custom regex validation
|
|
651
|
+
this.addRule('regex', (response, pattern) => {
|
|
652
|
+
const text = this.extractText(response);
|
|
653
|
+
const regex = new RegExp(pattern);
|
|
654
|
+
|
|
655
|
+
if (!regex.test(text)) {
|
|
656
|
+
return {
|
|
657
|
+
valid: false,
|
|
658
|
+
error: `Response does not match pattern: ${pattern}`
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return { valid: true };
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Extract text from response object
|
|
668
|
+
* @private
|
|
669
|
+
*/
|
|
670
|
+
extractText(response) {
|
|
671
|
+
if (typeof response === 'string') {
|
|
672
|
+
return response;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (response.text) return response.text;
|
|
676
|
+
if (response.content) return response.content;
|
|
677
|
+
if (response.message) return response.message;
|
|
678
|
+
|
|
679
|
+
return JSON.stringify(response);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Add custom validation rule
|
|
684
|
+
*/
|
|
685
|
+
addRule(name, validator) {
|
|
686
|
+
this.rules.set(name, validator);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Remove validation rule
|
|
691
|
+
*/
|
|
692
|
+
removeRule(name) {
|
|
693
|
+
this.rules.delete(name);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Validate response against rules
|
|
698
|
+
*/
|
|
699
|
+
validate(response, validationConfig) {
|
|
700
|
+
const errors = [];
|
|
701
|
+
|
|
702
|
+
for (const [ruleName, ruleValue] of Object.entries(validationConfig)) {
|
|
703
|
+
const validator = this.rules.get(ruleName);
|
|
704
|
+
|
|
705
|
+
if (!validator) {
|
|
706
|
+
console.warn(`AIKit Validator: Unknown rule '${ruleName}'`);
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const result = validator(response, ruleValue);
|
|
711
|
+
|
|
712
|
+
if (!result.valid) {
|
|
713
|
+
errors.push(result.error);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return {
|
|
718
|
+
isValid: errors.length === 0,
|
|
719
|
+
errors,
|
|
720
|
+
validatedAt: new Date().toISOString()
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Validate with custom function
|
|
726
|
+
*/
|
|
727
|
+
validateCustom(response, customFn) {
|
|
728
|
+
try {
|
|
729
|
+
const result = customFn(response);
|
|
730
|
+
return {
|
|
731
|
+
isValid: result === true || (result && result.valid === true),
|
|
732
|
+
errors: result.errors || (result === false ? ['Custom validation failed'] : []),
|
|
733
|
+
validatedAt: new Date().toISOString()
|
|
734
|
+
};
|
|
735
|
+
} catch (error) {
|
|
736
|
+
return {
|
|
737
|
+
isValid: false,
|
|
738
|
+
errors: [`Custom validation error: ${error.message}`],
|
|
739
|
+
validatedAt: new Date().toISOString()
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Get available validation rules
|
|
746
|
+
*/
|
|
747
|
+
getAvailableRules() {
|
|
748
|
+
return Array.from(this.rules.keys());
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Export
|
|
753
|
+
if (module.exports) {
|
|
754
|
+
module.exports = ResponseValidator;
|
|
755
|
+
}
|
|
756
|
+
} (ResponseValidator$1));
|
|
757
|
+
|
|
758
|
+
var ResponseValidatorExports = ResponseValidator$1.exports;
|
|
759
|
+
|
|
760
|
+
var BaseAdapter$1 = {exports: {}};
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* BaseAdapter - Base class for all AI provider adapters
|
|
764
|
+
* Defines the interface that all adapters must implement
|
|
765
|
+
* @class
|
|
766
|
+
*/
|
|
767
|
+
|
|
768
|
+
(function (module) {
|
|
769
|
+
class BaseAdapter {
|
|
770
|
+
constructor() {
|
|
771
|
+
if (new.target === BaseAdapter) {
|
|
772
|
+
throw new Error('BaseAdapter is an abstract class and cannot be instantiated directly');
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Send chat request to AI provider
|
|
778
|
+
* Must be implemented by subclasses
|
|
779
|
+
* @abstract
|
|
780
|
+
* @param {Object} request - Request object
|
|
781
|
+
* @param {string} request.message - User message
|
|
782
|
+
* @param {string} request.apiKey - API key
|
|
783
|
+
* @param {Object} request.options - Additional options
|
|
784
|
+
* @returns {Promise<Object>} Normalized response
|
|
785
|
+
*/
|
|
786
|
+
async chat(request) {
|
|
787
|
+
throw new Error('chat() must be implemented by subclass');
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Normalize response to standard format
|
|
792
|
+
* @protected
|
|
793
|
+
* @param {Object} rawResponse - Raw API response
|
|
794
|
+
* @returns {Object} Normalized response
|
|
795
|
+
*/
|
|
796
|
+
normalizeResponse(rawResponse) {
|
|
797
|
+
return {
|
|
798
|
+
text: this.extractText(rawResponse),
|
|
799
|
+
raw: rawResponse,
|
|
800
|
+
usage: this.extractUsage(rawResponse),
|
|
801
|
+
model: this.extractModel(rawResponse),
|
|
802
|
+
finishReason: this.extractFinishReason(rawResponse)
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Extract text from raw response
|
|
808
|
+
* @abstract
|
|
809
|
+
* @protected
|
|
810
|
+
*/
|
|
811
|
+
extractText(rawResponse) {
|
|
812
|
+
throw new Error('extractText() must be implemented by subclass');
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Extract usage information from raw response
|
|
817
|
+
* @abstract
|
|
818
|
+
* @protected
|
|
819
|
+
*/
|
|
820
|
+
extractUsage(rawResponse) {
|
|
821
|
+
throw new Error('extractUsage() must be implemented by subclass');
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Extract model name from raw response
|
|
826
|
+
* @abstract
|
|
827
|
+
* @protected
|
|
828
|
+
*/
|
|
829
|
+
extractModel(rawResponse) {
|
|
830
|
+
throw new Error('extractModel() must be implemented by subclass');
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Extract finish reason from raw response
|
|
835
|
+
* @abstract
|
|
836
|
+
* @protected
|
|
837
|
+
*/
|
|
838
|
+
extractFinishReason(rawResponse) {
|
|
839
|
+
throw new Error('extractFinishReason() must be implemented by subclass');
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Build request headers
|
|
844
|
+
* @protected
|
|
845
|
+
* @param {string} apiKey - API key
|
|
846
|
+
* @returns {Object} Headers object
|
|
847
|
+
*/
|
|
848
|
+
buildHeaders(apiKey) {
|
|
849
|
+
return {
|
|
850
|
+
'Content-Type': 'application/json',
|
|
851
|
+
'Authorization': `Bearer ${apiKey}`
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Make HTTP request
|
|
857
|
+
* @protected
|
|
858
|
+
* @param {string} url - API endpoint URL
|
|
859
|
+
* @param {Object} options - Fetch options
|
|
860
|
+
* @returns {Promise<Object>} Response data
|
|
861
|
+
*/
|
|
862
|
+
async makeRequest(url, options) {
|
|
863
|
+
try {
|
|
864
|
+
const response = await fetch(url, options);
|
|
865
|
+
|
|
866
|
+
if (!response.ok) {
|
|
867
|
+
const errorData = await response.json().catch(() => ({}));
|
|
868
|
+
throw new Error(
|
|
869
|
+
`HTTP ${response.status}: ${errorData.error?.message || response.statusText}`
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return await response.json();
|
|
874
|
+
} catch (error) {
|
|
875
|
+
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
|
876
|
+
throw new Error('Network error: Unable to reach API endpoint');
|
|
877
|
+
}
|
|
878
|
+
throw error;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Validate request parameters
|
|
884
|
+
* @protected
|
|
885
|
+
* @param {Object} request - Request object
|
|
886
|
+
*/
|
|
887
|
+
validateRequest(request) {
|
|
888
|
+
if (!request.message || typeof request.message !== 'string') {
|
|
889
|
+
throw new Error('Message is required and must be a string');
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (!request.apiKey || typeof request.apiKey !== 'string') {
|
|
893
|
+
throw new Error('API key is required and must be a string');
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (request.message.trim().length === 0) {
|
|
897
|
+
throw new Error('Message cannot be empty');
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Handle rate limiting with exponential backoff
|
|
903
|
+
* @protected
|
|
904
|
+
* @param {Function} fn - Function to retry
|
|
905
|
+
* @param {number} maxRetries - Maximum retry attempts
|
|
906
|
+
* @returns {Promise<any>} Function result
|
|
907
|
+
*/
|
|
908
|
+
async withRetry(fn, maxRetries = 3) {
|
|
909
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
910
|
+
try {
|
|
911
|
+
return await fn();
|
|
912
|
+
} catch (error) {
|
|
913
|
+
if (i === maxRetries - 1) throw error;
|
|
914
|
+
|
|
915
|
+
// Check if it's a rate limit error
|
|
916
|
+
const isRateLimit = error.message.includes('429') ||
|
|
917
|
+
error.message.includes('rate limit');
|
|
918
|
+
|
|
919
|
+
if (!isRateLimit) throw error;
|
|
920
|
+
|
|
921
|
+
// Exponential backoff
|
|
922
|
+
const delay = Math.pow(2, i) * 1000;
|
|
923
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Get provider name
|
|
930
|
+
* @returns {string} Provider name
|
|
931
|
+
*/
|
|
932
|
+
getProviderName() {
|
|
933
|
+
return this.constructor.name.replace('Adapter', '').toLowerCase();
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Export
|
|
938
|
+
if (module.exports) {
|
|
939
|
+
module.exports = BaseAdapter;
|
|
940
|
+
}
|
|
941
|
+
} (BaseAdapter$1));
|
|
942
|
+
|
|
943
|
+
var BaseAdapterExports = BaseAdapter$1.exports;
|
|
944
|
+
|
|
945
|
+
var OpenAIAdapter$1 = {exports: {}};
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* OpenAIAdapter - Adapter for OpenAI API
|
|
949
|
+
* @class
|
|
950
|
+
* @extends BaseAdapter
|
|
951
|
+
*/
|
|
952
|
+
|
|
953
|
+
(function (module) {
|
|
954
|
+
class OpenAIAdapter extends BaseAdapter {
|
|
955
|
+
constructor() {
|
|
956
|
+
super();
|
|
957
|
+
this.baseURL = 'https://api.openai.com/v1';
|
|
958
|
+
this.defaultModel = 'gpt-3.5-turbo';
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Send chat request to OpenAI
|
|
963
|
+
* @override
|
|
964
|
+
*/
|
|
965
|
+
async chat(request) {
|
|
966
|
+
this.validateRequest(request);
|
|
967
|
+
|
|
968
|
+
const { message, apiKey, options = {} } = request;
|
|
969
|
+
|
|
970
|
+
const requestBody = {
|
|
971
|
+
model: options.model || this.defaultModel,
|
|
972
|
+
messages: this.buildMessages(message, options),
|
|
973
|
+
temperature: options.temperature ?? 0.7,
|
|
974
|
+
max_tokens: options.maxTokens || options.max_tokens,
|
|
975
|
+
top_p: options.topP || options.top_p,
|
|
976
|
+
frequency_penalty: options.frequencyPenalty || options.frequency_penalty,
|
|
977
|
+
presence_penalty: options.presencePenalty || options.presence_penalty,
|
|
978
|
+
stream: false
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
// Remove undefined values
|
|
982
|
+
Object.keys(requestBody).forEach(key => {
|
|
983
|
+
if (requestBody[key] === undefined) {
|
|
984
|
+
delete requestBody[key];
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
const fetchOptions = {
|
|
989
|
+
method: 'POST',
|
|
990
|
+
headers: this.buildHeaders(apiKey),
|
|
991
|
+
body: JSON.stringify(requestBody)
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
const rawResponse = await this.withRetry(() =>
|
|
995
|
+
this.makeRequest(`${this.baseURL}/chat/completions`, fetchOptions)
|
|
996
|
+
);
|
|
997
|
+
|
|
998
|
+
return this.normalizeResponse(rawResponse);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Build messages array from user message
|
|
1003
|
+
* @private
|
|
1004
|
+
*/
|
|
1005
|
+
buildMessages(message, options) {
|
|
1006
|
+
const messages = [];
|
|
1007
|
+
|
|
1008
|
+
// Add system message if provided
|
|
1009
|
+
if (options.systemMessage) {
|
|
1010
|
+
messages.push({
|
|
1011
|
+
role: 'system',
|
|
1012
|
+
content: options.systemMessage
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Add conversation history if provided
|
|
1017
|
+
if (options.history && Array.isArray(options.history)) {
|
|
1018
|
+
messages.push(...options.history);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Add current user message
|
|
1022
|
+
messages.push({
|
|
1023
|
+
role: 'user',
|
|
1024
|
+
content: message
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
return messages;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Extract text from OpenAI response
|
|
1032
|
+
* @override
|
|
1033
|
+
*/
|
|
1034
|
+
extractText(rawResponse) {
|
|
1035
|
+
if (!rawResponse.choices || rawResponse.choices.length === 0) {
|
|
1036
|
+
throw new Error('Invalid OpenAI response: no choices returned');
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const firstChoice = rawResponse.choices[0];
|
|
1040
|
+
return firstChoice.message?.content || '';
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* Extract usage information
|
|
1045
|
+
* @override
|
|
1046
|
+
*/
|
|
1047
|
+
extractUsage(rawResponse) {
|
|
1048
|
+
const usage = rawResponse.usage || {};
|
|
1049
|
+
|
|
1050
|
+
return {
|
|
1051
|
+
promptTokens: usage.prompt_tokens || 0,
|
|
1052
|
+
completionTokens: usage.completion_tokens || 0,
|
|
1053
|
+
totalTokens: usage.total_tokens || 0,
|
|
1054
|
+
// Normalized names for compatibility
|
|
1055
|
+
input: usage.prompt_tokens || 0,
|
|
1056
|
+
output: usage.completion_tokens || 0,
|
|
1057
|
+
total: usage.total_tokens || 0
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Extract model name
|
|
1063
|
+
* @override
|
|
1064
|
+
*/
|
|
1065
|
+
extractModel(rawResponse) {
|
|
1066
|
+
return rawResponse.model || this.defaultModel;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Extract finish reason
|
|
1071
|
+
* @override
|
|
1072
|
+
*/
|
|
1073
|
+
extractFinishReason(rawResponse) {
|
|
1074
|
+
if (!rawResponse.choices || rawResponse.choices.length === 0) {
|
|
1075
|
+
return 'unknown';
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
return rawResponse.choices[0].finish_reason || 'unknown';
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Stream chat (for future implementation)
|
|
1083
|
+
* @param {Object} request - Request object
|
|
1084
|
+
* @param {Function} onChunk - Callback for each chunk
|
|
1085
|
+
*/
|
|
1086
|
+
async streamChat(request, onChunk) {
|
|
1087
|
+
// TODO: Implement streaming support
|
|
1088
|
+
throw new Error('Streaming is not yet implemented for OpenAI adapter');
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* List available models
|
|
1093
|
+
* @param {string} apiKey - API key
|
|
1094
|
+
*/
|
|
1095
|
+
async listModels(apiKey) {
|
|
1096
|
+
const fetchOptions = {
|
|
1097
|
+
method: 'GET',
|
|
1098
|
+
headers: this.buildHeaders(apiKey)
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
const response = await this.makeRequest(`${this.baseURL}/models`, fetchOptions);
|
|
1102
|
+
|
|
1103
|
+
// Filter to chat models only
|
|
1104
|
+
const chatModels = response.data.filter(model =>
|
|
1105
|
+
model.id.includes('gpt') || model.id.includes('turbo')
|
|
1106
|
+
);
|
|
1107
|
+
|
|
1108
|
+
return chatModels.map(model => ({
|
|
1109
|
+
id: model.id,
|
|
1110
|
+
created: model.created,
|
|
1111
|
+
ownedBy: model.owned_by
|
|
1112
|
+
}));
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* Generate embeddings
|
|
1117
|
+
* @param {string} text - Text to embed
|
|
1118
|
+
* @param {string} apiKey - API key
|
|
1119
|
+
* @param {Object} options - Options
|
|
1120
|
+
*/
|
|
1121
|
+
async createEmbedding(text, apiKey, options = {}) {
|
|
1122
|
+
const requestBody = {
|
|
1123
|
+
model: options.model || 'text-embedding-ada-002',
|
|
1124
|
+
input: text
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
const fetchOptions = {
|
|
1128
|
+
method: 'POST',
|
|
1129
|
+
headers: this.buildHeaders(apiKey),
|
|
1130
|
+
body: JSON.stringify(requestBody)
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
const response = await this.makeRequest(`${this.baseURL}/embeddings`, fetchOptions);
|
|
1134
|
+
|
|
1135
|
+
return {
|
|
1136
|
+
embedding: response.data[0].embedding,
|
|
1137
|
+
model: response.model,
|
|
1138
|
+
usage: response.usage
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Export
|
|
1144
|
+
if (module.exports) {
|
|
1145
|
+
module.exports = OpenAIAdapter;
|
|
1146
|
+
}
|
|
1147
|
+
} (OpenAIAdapter$1));
|
|
1148
|
+
|
|
1149
|
+
var OpenAIAdapterExports = OpenAIAdapter$1.exports;
|
|
1150
|
+
|
|
1151
|
+
var ClaudeAdapter$1 = {exports: {}};
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* ClaudeAdapter - Adapter for Anthropic Claude API
|
|
1155
|
+
* @class
|
|
1156
|
+
* @extends BaseAdapter
|
|
1157
|
+
*/
|
|
1158
|
+
|
|
1159
|
+
(function (module) {
|
|
1160
|
+
class ClaudeAdapter extends BaseAdapter {
|
|
1161
|
+
constructor() {
|
|
1162
|
+
super();
|
|
1163
|
+
this.baseURL = 'https://api.anthropic.com/v1';
|
|
1164
|
+
this.defaultModel = 'claude-3-sonnet-20240229';
|
|
1165
|
+
this.apiVersion = '2023-06-01';
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Send chat request to Claude
|
|
1170
|
+
* @override
|
|
1171
|
+
*/
|
|
1172
|
+
async chat(request) {
|
|
1173
|
+
this.validateRequest(request);
|
|
1174
|
+
|
|
1175
|
+
const { message, apiKey, options = {} } = request;
|
|
1176
|
+
|
|
1177
|
+
const requestBody = {
|
|
1178
|
+
model: options.model || this.defaultModel,
|
|
1179
|
+
messages: this.buildMessages(message, options),
|
|
1180
|
+
max_tokens: options.maxTokens || options.max_tokens || 1024,
|
|
1181
|
+
temperature: options.temperature ?? 1.0,
|
|
1182
|
+
top_p: options.topP || options.top_p,
|
|
1183
|
+
top_k: options.topK || options.top_k,
|
|
1184
|
+
stream: false
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
// Add system message if provided
|
|
1188
|
+
if (options.systemMessage) {
|
|
1189
|
+
requestBody.system = options.systemMessage;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Remove undefined values
|
|
1193
|
+
Object.keys(requestBody).forEach(key => {
|
|
1194
|
+
if (requestBody[key] === undefined) {
|
|
1195
|
+
delete requestBody[key];
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
const fetchOptions = {
|
|
1200
|
+
method: 'POST',
|
|
1201
|
+
headers: this.buildHeaders(apiKey),
|
|
1202
|
+
body: JSON.stringify(requestBody)
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
const rawResponse = await this.withRetry(() =>
|
|
1206
|
+
this.makeRequest(`${this.baseURL}/messages`, fetchOptions)
|
|
1207
|
+
);
|
|
1208
|
+
|
|
1209
|
+
return this.normalizeResponse(rawResponse);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Build headers for Claude API
|
|
1214
|
+
* @override
|
|
1215
|
+
*/
|
|
1216
|
+
buildHeaders(apiKey) {
|
|
1217
|
+
return {
|
|
1218
|
+
'Content-Type': 'application/json',
|
|
1219
|
+
'x-api-key': apiKey,
|
|
1220
|
+
'anthropic-version': this.apiVersion
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Build messages array from user message
|
|
1226
|
+
* @private
|
|
1227
|
+
*/
|
|
1228
|
+
buildMessages(message, options) {
|
|
1229
|
+
const messages = [];
|
|
1230
|
+
|
|
1231
|
+
// Add conversation history if provided
|
|
1232
|
+
if (options.history && Array.isArray(options.history)) {
|
|
1233
|
+
// Claude uses 'user' and 'assistant' roles
|
|
1234
|
+
messages.push(...options.history.map(msg => ({
|
|
1235
|
+
role: msg.role === 'system' ? 'user' : msg.role,
|
|
1236
|
+
content: msg.content
|
|
1237
|
+
})));
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Add current user message
|
|
1241
|
+
messages.push({
|
|
1242
|
+
role: 'user',
|
|
1243
|
+
content: message
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
return messages;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Extract text from Claude response
|
|
1251
|
+
* @override
|
|
1252
|
+
*/
|
|
1253
|
+
extractText(rawResponse) {
|
|
1254
|
+
if (!rawResponse.content || rawResponse.content.length === 0) {
|
|
1255
|
+
throw new Error('Invalid Claude response: no content returned');
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Claude can return multiple content blocks
|
|
1259
|
+
const textBlocks = rawResponse.content
|
|
1260
|
+
.filter(block => block.type === 'text')
|
|
1261
|
+
.map(block => block.text);
|
|
1262
|
+
|
|
1263
|
+
return textBlocks.join('\n');
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Extract usage information
|
|
1268
|
+
* @override
|
|
1269
|
+
*/
|
|
1270
|
+
extractUsage(rawResponse) {
|
|
1271
|
+
const usage = rawResponse.usage || {};
|
|
1272
|
+
|
|
1273
|
+
return {
|
|
1274
|
+
promptTokens: usage.input_tokens || 0,
|
|
1275
|
+
completionTokens: usage.output_tokens || 0,
|
|
1276
|
+
totalTokens: (usage.input_tokens || 0) + (usage.output_tokens || 0),
|
|
1277
|
+
// Normalized names for compatibility
|
|
1278
|
+
input: usage.input_tokens || 0,
|
|
1279
|
+
output: usage.output_tokens || 0,
|
|
1280
|
+
total: (usage.input_tokens || 0) + (usage.output_tokens || 0)
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Extract model name
|
|
1286
|
+
* @override
|
|
1287
|
+
*/
|
|
1288
|
+
extractModel(rawResponse) {
|
|
1289
|
+
return rawResponse.model || this.defaultModel;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* Extract finish reason
|
|
1294
|
+
* @override
|
|
1295
|
+
*/
|
|
1296
|
+
extractFinishReason(rawResponse) {
|
|
1297
|
+
return rawResponse.stop_reason || 'unknown';
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Stream chat (for future implementation)
|
|
1302
|
+
* @param {Object} request - Request object
|
|
1303
|
+
* @param {Function} onChunk - Callback for each chunk
|
|
1304
|
+
*/
|
|
1305
|
+
async streamChat(request, onChunk) {
|
|
1306
|
+
// TODO: Implement streaming support
|
|
1307
|
+
throw new Error('Streaming is not yet implemented for Claude adapter');
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* Get model information
|
|
1312
|
+
*/
|
|
1313
|
+
getAvailableModels() {
|
|
1314
|
+
return [
|
|
1315
|
+
{
|
|
1316
|
+
id: 'claude-3-opus-20240229',
|
|
1317
|
+
name: 'Claude 3 Opus',
|
|
1318
|
+
description: 'Most powerful model, best for complex tasks',
|
|
1319
|
+
maxTokens: 4096
|
|
1320
|
+
},
|
|
1321
|
+
{
|
|
1322
|
+
id: 'claude-3-sonnet-20240229',
|
|
1323
|
+
name: 'Claude 3 Sonnet',
|
|
1324
|
+
description: 'Balanced performance and speed',
|
|
1325
|
+
maxTokens: 4096
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
id: 'claude-3-haiku-20240307',
|
|
1329
|
+
name: 'Claude 3 Haiku',
|
|
1330
|
+
description: 'Fastest model, best for simple tasks',
|
|
1331
|
+
maxTokens: 4096
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
id: 'claude-2.1',
|
|
1335
|
+
name: 'Claude 2.1',
|
|
1336
|
+
description: 'Previous generation model',
|
|
1337
|
+
maxTokens: 4096
|
|
1338
|
+
}
|
|
1339
|
+
];
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Count tokens (approximate)
|
|
1344
|
+
* Claude's tokenization is similar to GPT but not identical
|
|
1345
|
+
* This is a rough estimate
|
|
1346
|
+
*/
|
|
1347
|
+
estimateTokens(text) {
|
|
1348
|
+
// Rough estimate: ~4 characters per token for English
|
|
1349
|
+
// ~2 characters per token for code
|
|
1350
|
+
const avgCharsPerToken = text.match(/[a-zA-Z0-9_]/g) ? 4 : 3;
|
|
1351
|
+
return Math.ceil(text.length / avgCharsPerToken);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Export
|
|
1356
|
+
if (module.exports) {
|
|
1357
|
+
module.exports = ClaudeAdapter;
|
|
1358
|
+
}
|
|
1359
|
+
} (ClaudeAdapter$1));
|
|
1360
|
+
|
|
1361
|
+
var ClaudeAdapterExports = ClaudeAdapter$1.exports;
|
|
1362
|
+
|
|
1363
|
+
var GeminiAdapter$1 = {exports: {}};
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* GeminiAdapter - Adapter for Google Gemini API
|
|
1367
|
+
* @class
|
|
1368
|
+
* @extends BaseAdapter
|
|
1369
|
+
*/
|
|
1370
|
+
|
|
1371
|
+
(function (module) {
|
|
1372
|
+
class GeminiAdapter extends BaseAdapter {
|
|
1373
|
+
constructor() {
|
|
1374
|
+
super();
|
|
1375
|
+
this.baseURL = 'https://generativelanguage.googleapis.com/v1beta';
|
|
1376
|
+
this.defaultModel = 'gemini-pro';
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* Send chat request to Gemini
|
|
1381
|
+
* @override
|
|
1382
|
+
*/
|
|
1383
|
+
async chat(request) {
|
|
1384
|
+
this.validateRequest(request);
|
|
1385
|
+
|
|
1386
|
+
const { message, apiKey, options = {} } = request;
|
|
1387
|
+
const model = options.model || this.defaultModel;
|
|
1388
|
+
|
|
1389
|
+
const requestBody = {
|
|
1390
|
+
contents: this.buildContents(message, options),
|
|
1391
|
+
generationConfig: {
|
|
1392
|
+
temperature: options.temperature ?? 0.9,
|
|
1393
|
+
topK: options.topK || options.top_k,
|
|
1394
|
+
topP: (options.topP || options.top_p) ?? 1,
|
|
1395
|
+
maxOutputTokens: options.maxTokens || options.max_tokens || options.maxOutputTokens,
|
|
1396
|
+
stopSequences: options.stopSequences || options.stop
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
// Add safety settings if provided
|
|
1401
|
+
if (options.safetySettings) {
|
|
1402
|
+
requestBody.safetySettings = options.safetySettings;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Remove undefined values
|
|
1406
|
+
if (requestBody.generationConfig) {
|
|
1407
|
+
Object.keys(requestBody.generationConfig).forEach(key => {
|
|
1408
|
+
if (requestBody.generationConfig[key] === undefined) {
|
|
1409
|
+
delete requestBody.generationConfig[key];
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const url = `${this.baseURL}/models/${model}:generateContent?key=${apiKey}`;
|
|
1415
|
+
|
|
1416
|
+
const fetchOptions = {
|
|
1417
|
+
method: 'POST',
|
|
1418
|
+
headers: {
|
|
1419
|
+
'Content-Type': 'application/json'
|
|
1420
|
+
},
|
|
1421
|
+
body: JSON.stringify(requestBody)
|
|
1422
|
+
};
|
|
1423
|
+
|
|
1424
|
+
const rawResponse = await this.withRetry(() =>
|
|
1425
|
+
this.makeRequest(url, fetchOptions)
|
|
1426
|
+
);
|
|
1427
|
+
|
|
1428
|
+
return this.normalizeResponse(rawResponse);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* Build contents array from user message
|
|
1433
|
+
* @private
|
|
1434
|
+
*/
|
|
1435
|
+
buildContents(message, options) {
|
|
1436
|
+
const contents = [];
|
|
1437
|
+
|
|
1438
|
+
// Add conversation history if provided
|
|
1439
|
+
if (options.history && Array.isArray(options.history)) {
|
|
1440
|
+
options.history.forEach(msg => {
|
|
1441
|
+
contents.push({
|
|
1442
|
+
role: msg.role === 'assistant' ? 'model' : 'user',
|
|
1443
|
+
parts: [{ text: msg.content }]
|
|
1444
|
+
});
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// Add current user message
|
|
1449
|
+
contents.push({
|
|
1450
|
+
role: 'user',
|
|
1451
|
+
parts: [{ text: message }]
|
|
1452
|
+
});
|
|
1453
|
+
|
|
1454
|
+
return contents;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Extract text from Gemini response
|
|
1459
|
+
* @override
|
|
1460
|
+
*/
|
|
1461
|
+
extractText(rawResponse) {
|
|
1462
|
+
if (!rawResponse.candidates || rawResponse.candidates.length === 0) {
|
|
1463
|
+
throw new Error('Invalid Gemini response: no candidates returned');
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
const candidate = rawResponse.candidates[0];
|
|
1467
|
+
|
|
1468
|
+
if (!candidate.content || !candidate.content.parts) {
|
|
1469
|
+
throw new Error('Invalid Gemini response: no content parts');
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// Combine all text parts
|
|
1473
|
+
const textParts = candidate.content.parts
|
|
1474
|
+
.filter(part => part.text)
|
|
1475
|
+
.map(part => part.text);
|
|
1476
|
+
|
|
1477
|
+
return textParts.join('\n');
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/**
|
|
1481
|
+
* Extract usage information
|
|
1482
|
+
* @override
|
|
1483
|
+
*/
|
|
1484
|
+
extractUsage(rawResponse) {
|
|
1485
|
+
const metadata = rawResponse.usageMetadata || {};
|
|
1486
|
+
|
|
1487
|
+
return {
|
|
1488
|
+
promptTokens: metadata.promptTokenCount || 0,
|
|
1489
|
+
completionTokens: metadata.candidatesTokenCount || 0,
|
|
1490
|
+
totalTokens: metadata.totalTokenCount || 0,
|
|
1491
|
+
// Normalized names for compatibility
|
|
1492
|
+
input: metadata.promptTokenCount || 0,
|
|
1493
|
+
output: metadata.candidatesTokenCount || 0,
|
|
1494
|
+
total: metadata.totalTokenCount || 0
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* Extract model name
|
|
1500
|
+
* @override
|
|
1501
|
+
*/
|
|
1502
|
+
extractModel(rawResponse) {
|
|
1503
|
+
return rawResponse.modelVersion || this.defaultModel;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/**
|
|
1507
|
+
* Extract finish reason
|
|
1508
|
+
* @override
|
|
1509
|
+
*/
|
|
1510
|
+
extractFinishReason(rawResponse) {
|
|
1511
|
+
if (!rawResponse.candidates || rawResponse.candidates.length === 0) {
|
|
1512
|
+
return 'unknown';
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
return rawResponse.candidates[0].finishReason || 'STOP';
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* Stream chat (for future implementation)
|
|
1520
|
+
* @param {Object} request - Request object
|
|
1521
|
+
* @param {Function} onChunk - Callback for each chunk
|
|
1522
|
+
*/
|
|
1523
|
+
async streamChat(request, onChunk) {
|
|
1524
|
+
// TODO: Implement streaming support
|
|
1525
|
+
throw new Error('Streaming is not yet implemented for Gemini adapter');
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
/**
|
|
1529
|
+
* Get available models
|
|
1530
|
+
*/
|
|
1531
|
+
getAvailableModels() {
|
|
1532
|
+
return [
|
|
1533
|
+
{
|
|
1534
|
+
id: 'gemini-pro',
|
|
1535
|
+
name: 'Gemini Pro',
|
|
1536
|
+
description: 'Best for text generation',
|
|
1537
|
+
maxTokens: 8192
|
|
1538
|
+
},
|
|
1539
|
+
{
|
|
1540
|
+
id: 'gemini-pro-vision',
|
|
1541
|
+
name: 'Gemini Pro Vision',
|
|
1542
|
+
description: 'Multimodal model for text and images',
|
|
1543
|
+
maxTokens: 4096
|
|
1544
|
+
},
|
|
1545
|
+
{
|
|
1546
|
+
id: 'gemini-ultra',
|
|
1547
|
+
name: 'Gemini Ultra',
|
|
1548
|
+
description: 'Most capable model (limited access)',
|
|
1549
|
+
maxTokens: 8192
|
|
1550
|
+
}
|
|
1551
|
+
];
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/**
|
|
1555
|
+
* List models via API
|
|
1556
|
+
* @param {string} apiKey - API key
|
|
1557
|
+
*/
|
|
1558
|
+
async listModels(apiKey) {
|
|
1559
|
+
const url = `${this.baseURL}/models?key=${apiKey}`;
|
|
1560
|
+
|
|
1561
|
+
const fetchOptions = {
|
|
1562
|
+
method: 'GET',
|
|
1563
|
+
headers: {
|
|
1564
|
+
'Content-Type': 'application/json'
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
const response = await this.makeRequest(url, fetchOptions);
|
|
1569
|
+
|
|
1570
|
+
return response.models
|
|
1571
|
+
.filter(model => model.supportedGenerationMethods?.includes('generateContent'))
|
|
1572
|
+
.map(model => ({
|
|
1573
|
+
id: model.name.replace('models/', ''),
|
|
1574
|
+
displayName: model.displayName,
|
|
1575
|
+
description: model.description,
|
|
1576
|
+
inputTokenLimit: model.inputTokenLimit,
|
|
1577
|
+
outputTokenLimit: model.outputTokenLimit
|
|
1578
|
+
}));
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
/**
|
|
1582
|
+
* Count tokens
|
|
1583
|
+
* @param {string} text - Text to count tokens for
|
|
1584
|
+
* @param {string} apiKey - API key
|
|
1585
|
+
* @param {string} model - Model name
|
|
1586
|
+
*/
|
|
1587
|
+
async countTokens(text, apiKey, model = this.defaultModel) {
|
|
1588
|
+
const url = `${this.baseURL}/models/${model}:countTokens?key=${apiKey}`;
|
|
1589
|
+
|
|
1590
|
+
const requestBody = {
|
|
1591
|
+
contents: [{
|
|
1592
|
+
role: 'user',
|
|
1593
|
+
parts: [{ text }]
|
|
1594
|
+
}]
|
|
1595
|
+
};
|
|
1596
|
+
|
|
1597
|
+
const fetchOptions = {
|
|
1598
|
+
method: 'POST',
|
|
1599
|
+
headers: {
|
|
1600
|
+
'Content-Type': 'application/json'
|
|
1601
|
+
},
|
|
1602
|
+
body: JSON.stringify(requestBody)
|
|
1603
|
+
};
|
|
1604
|
+
|
|
1605
|
+
const response = await this.makeRequest(url, fetchOptions);
|
|
1606
|
+
|
|
1607
|
+
return {
|
|
1608
|
+
totalTokens: response.totalTokens || 0
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/**
|
|
1613
|
+
* Generate embeddings
|
|
1614
|
+
* @param {string} text - Text to embed
|
|
1615
|
+
* @param {string} apiKey - API key
|
|
1616
|
+
*/
|
|
1617
|
+
async createEmbedding(text, apiKey) {
|
|
1618
|
+
const model = 'embedding-001';
|
|
1619
|
+
const url = `${this.baseURL}/models/${model}:embedContent?key=${apiKey}`;
|
|
1620
|
+
|
|
1621
|
+
const requestBody = {
|
|
1622
|
+
content: {
|
|
1623
|
+
parts: [{ text }]
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
const fetchOptions = {
|
|
1628
|
+
method: 'POST',
|
|
1629
|
+
headers: {
|
|
1630
|
+
'Content-Type': 'application/json'
|
|
1631
|
+
},
|
|
1632
|
+
body: JSON.stringify(requestBody)
|
|
1633
|
+
};
|
|
1634
|
+
|
|
1635
|
+
const response = await this.makeRequest(url, fetchOptions);
|
|
1636
|
+
|
|
1637
|
+
return {
|
|
1638
|
+
embedding: response.embedding.values,
|
|
1639
|
+
model: model
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// Export
|
|
1645
|
+
if (module.exports) {
|
|
1646
|
+
module.exports = GeminiAdapter;
|
|
1647
|
+
}
|
|
1648
|
+
} (GeminiAdapter$1));
|
|
1649
|
+
|
|
1650
|
+
var GeminiAdapterExports = GeminiAdapter$1.exports;
|
|
1651
|
+
|
|
1652
|
+
var AIKit = {exports: {}};
|
|
1653
|
+
|
|
1654
|
+
/**
|
|
1655
|
+
* AIKit - Universal AI API Client
|
|
1656
|
+
* @class
|
|
1657
|
+
*/
|
|
1658
|
+
|
|
1659
|
+
(function (module) {
|
|
1660
|
+
class AIKit {
|
|
1661
|
+
/**
|
|
1662
|
+
* @param {Object} config - Configuration object
|
|
1663
|
+
* @param {string} config.provider - AI provider name ('openai', 'claude', 'gemini')
|
|
1664
|
+
* @param {string} config.apiKey - API key for the provider
|
|
1665
|
+
* @param {Object} [config.options] - Additional options
|
|
1666
|
+
*/
|
|
1667
|
+
constructor(config) {
|
|
1668
|
+
this.validateConfig(config);
|
|
1669
|
+
|
|
1670
|
+
this.config = {
|
|
1671
|
+
provider: config.provider,
|
|
1672
|
+
apiKey: config.apiKey,
|
|
1673
|
+
options: config.options || {},
|
|
1674
|
+
autoFallback: config.autoFallback || false,
|
|
1675
|
+
enableCache: config.enableCache !== false,
|
|
1676
|
+
enableCostTracking: config.enableCostTracking !== false,
|
|
1677
|
+
maxRetries: config.maxRetries || 3,
|
|
1678
|
+
timeout: config.timeout || 30000
|
|
1679
|
+
};
|
|
1680
|
+
|
|
1681
|
+
// 멀티 프로바이더 설정
|
|
1682
|
+
this.providers = config.providers || [
|
|
1683
|
+
{ name: config.provider, apiKey: config.apiKey, priority: 1 }
|
|
1684
|
+
];
|
|
1685
|
+
|
|
1686
|
+
this.currentProviderIndex = 0;
|
|
1687
|
+
|
|
1688
|
+
// 모듈 초기화
|
|
1689
|
+
this.adapter = this.loadAdapter(this.config.provider);
|
|
1690
|
+
this.cache = this.config.enableCache ? new CacheManager() : null;
|
|
1691
|
+
this.costTracker = this.config.enableCostTracking ? new CostTracker() : null;
|
|
1692
|
+
this.validator = new ResponseValidator();
|
|
1693
|
+
|
|
1694
|
+
// 통계
|
|
1695
|
+
this.stats = {
|
|
1696
|
+
totalRequests: 0,
|
|
1697
|
+
successfulRequests: 0,
|
|
1698
|
+
failedRequests: 0,
|
|
1699
|
+
cachedResponses: 0
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
/**
|
|
1704
|
+
* 설정 유효성 검사
|
|
1705
|
+
* @private
|
|
1706
|
+
*/
|
|
1707
|
+
validateConfig(config) {
|
|
1708
|
+
if (!config) {
|
|
1709
|
+
throw new Error('AIKit: Configuration is required');
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
if (config.providers) {
|
|
1713
|
+
// 멀티 프로바이더 모드
|
|
1714
|
+
if (!Array.isArray(config.providers) || config.providers.length === 0) {
|
|
1715
|
+
throw new Error('AIKit: providers must be a non-empty array');
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
config.providers.forEach((p, i) => {
|
|
1719
|
+
if (!p.name || !p.apiKey) {
|
|
1720
|
+
throw new Error(`AIKit: Provider at index ${i} must have 'name' and 'apiKey'`);
|
|
1721
|
+
}
|
|
1722
|
+
});
|
|
1723
|
+
} else {
|
|
1724
|
+
// 싱글 프로바이더 모드
|
|
1725
|
+
if (!config.provider) {
|
|
1726
|
+
throw new Error('AIKit: provider is required');
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
if (!config.apiKey) {
|
|
1730
|
+
throw new Error('AIKit: apiKey is required');
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
/**
|
|
1736
|
+
* 프로바이더 어댑터 로드
|
|
1737
|
+
* @private
|
|
1738
|
+
*/
|
|
1739
|
+
loadAdapter(provider) {
|
|
1740
|
+
const adapterMap = {
|
|
1741
|
+
'openai': OpenAIAdapter,
|
|
1742
|
+
'claude': ClaudeAdapter,
|
|
1743
|
+
'gemini': GeminiAdapter
|
|
1744
|
+
};
|
|
1745
|
+
|
|
1746
|
+
const AdapterClass = adapterMap[provider.toLowerCase()];
|
|
1747
|
+
|
|
1748
|
+
if (!AdapterClass) {
|
|
1749
|
+
throw new Error(`AIKit: Unsupported provider '${provider}'. Supported: ${Object.keys(adapterMap).join(', ')}`);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
return new AdapterClass();
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/**
|
|
1756
|
+
* AI 채팅 요청
|
|
1757
|
+
* @param {string} message - User message
|
|
1758
|
+
* @param {Object} [options] - Request options
|
|
1759
|
+
* @returns {Promise<Object>} Response object
|
|
1760
|
+
*/
|
|
1761
|
+
async chat(message, options = {}) {
|
|
1762
|
+
this.stats.totalRequests++;
|
|
1763
|
+
|
|
1764
|
+
// 캐시 체크
|
|
1765
|
+
if (this.cache && !options.skipCache) {
|
|
1766
|
+
const cacheKey = this.cache.generateKey(message, options);
|
|
1767
|
+
const cached = this.cache.get(cacheKey);
|
|
1768
|
+
|
|
1769
|
+
if (cached) {
|
|
1770
|
+
this.stats.cachedResponses++;
|
|
1771
|
+
return {
|
|
1772
|
+
...cached,
|
|
1773
|
+
fromCache: true,
|
|
1774
|
+
timestamp: new Date().toISOString()
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// 검증 옵션 추출
|
|
1780
|
+
const validation = options.validate || {};
|
|
1781
|
+
delete options.validate;
|
|
1782
|
+
|
|
1783
|
+
// 재시도 로직
|
|
1784
|
+
let lastError;
|
|
1785
|
+
for (let attempt = 0; attempt < this.config.maxRetries; attempt++) {
|
|
1786
|
+
try {
|
|
1787
|
+
const response = await this.makeRequest(message, options);
|
|
1788
|
+
|
|
1789
|
+
// 응답 검증
|
|
1790
|
+
if (Object.keys(validation).length > 0) {
|
|
1791
|
+
const validationResult = this.validator.validate(response, validation);
|
|
1792
|
+
if (!validationResult.isValid) {
|
|
1793
|
+
throw new Error(`Validation failed: ${validationResult.errors.join(', ')}`);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// 캐시 저장
|
|
1798
|
+
if (this.cache && !options.skipCache) {
|
|
1799
|
+
const cacheKey = this.cache.generateKey(message, options);
|
|
1800
|
+
this.cache.set(cacheKey, response);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// 비용 추적
|
|
1804
|
+
if (this.costTracker) {
|
|
1805
|
+
this.costTracker.track({
|
|
1806
|
+
provider: this.config.provider,
|
|
1807
|
+
tokens: response.usage || {},
|
|
1808
|
+
timestamp: new Date().toISOString()
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
this.stats.successfulRequests++;
|
|
1813
|
+
|
|
1814
|
+
return {
|
|
1815
|
+
...response,
|
|
1816
|
+
fromCache: false,
|
|
1817
|
+
timestamp: new Date().toISOString()
|
|
1818
|
+
};
|
|
1819
|
+
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
lastError = error;
|
|
1822
|
+
console.warn(`AIKit: Attempt ${attempt + 1} failed:`, error.message);
|
|
1823
|
+
|
|
1824
|
+
// Auto fallback to next provider
|
|
1825
|
+
if (this.config.autoFallback && this.providers.length > 1) {
|
|
1826
|
+
const switched = await this.switchToNextProvider();
|
|
1827
|
+
if (!switched) break;
|
|
1828
|
+
} else {
|
|
1829
|
+
// Wait before retry
|
|
1830
|
+
if (attempt < this.config.maxRetries - 1) {
|
|
1831
|
+
await this.sleep(Math.pow(2, attempt) * 1000);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
this.stats.failedRequests++;
|
|
1838
|
+
throw new Error(`AIKit: All attempts failed. Last error: ${lastError.message}`);
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
/**
|
|
1842
|
+
* API 요청 실행
|
|
1843
|
+
* @private
|
|
1844
|
+
*/
|
|
1845
|
+
async makeRequest(message, options) {
|
|
1846
|
+
const provider = this.providers[this.currentProviderIndex];
|
|
1847
|
+
|
|
1848
|
+
const requestData = {
|
|
1849
|
+
message,
|
|
1850
|
+
apiKey: provider.apiKey,
|
|
1851
|
+
options: {
|
|
1852
|
+
...this.config.options,
|
|
1853
|
+
...options
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
|
|
1857
|
+
return await this.adapter.chat(requestData);
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* 다음 프로바이더로 전환
|
|
1862
|
+
* @private
|
|
1863
|
+
*/
|
|
1864
|
+
async switchToNextProvider() {
|
|
1865
|
+
this.currentProviderIndex++;
|
|
1866
|
+
|
|
1867
|
+
if (this.currentProviderIndex >= this.providers.length) {
|
|
1868
|
+
this.currentProviderIndex = 0;
|
|
1869
|
+
return false; // 모든 프로바이더 시도 완료
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
const nextProvider = this.providers[this.currentProviderIndex];
|
|
1873
|
+
console.log(`AIKit: Switching to provider '${nextProvider.name}'`);
|
|
1874
|
+
|
|
1875
|
+
this.adapter = this.loadAdapter(nextProvider.name);
|
|
1876
|
+
this.config.provider = nextProvider.name;
|
|
1877
|
+
|
|
1878
|
+
return true;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
/**
|
|
1882
|
+
* Sleep utility
|
|
1883
|
+
* @private
|
|
1884
|
+
*/
|
|
1885
|
+
sleep(ms) {
|
|
1886
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
/**
|
|
1890
|
+
* 비용 리포트 가져오기
|
|
1891
|
+
*/
|
|
1892
|
+
getCostReport() {
|
|
1893
|
+
if (!this.costTracker) {
|
|
1894
|
+
return { error: 'Cost tracking is disabled' };
|
|
1895
|
+
}
|
|
1896
|
+
return this.costTracker.getReport();
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
/**
|
|
1900
|
+
* 통계 가져오기
|
|
1901
|
+
*/
|
|
1902
|
+
getStats() {
|
|
1903
|
+
return { ...this.stats };
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
/**
|
|
1907
|
+
* 캐시 초기화
|
|
1908
|
+
*/
|
|
1909
|
+
clearCache() {
|
|
1910
|
+
if (this.cache) {
|
|
1911
|
+
this.cache.clear();
|
|
1912
|
+
return true;
|
|
1913
|
+
}
|
|
1914
|
+
return false;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
/**
|
|
1918
|
+
* 설정 업데이트
|
|
1919
|
+
*/
|
|
1920
|
+
updateConfig(newConfig) {
|
|
1921
|
+
Object.assign(this.config, newConfig);
|
|
1922
|
+
|
|
1923
|
+
if (newConfig.provider && newConfig.provider !== this.config.provider) {
|
|
1924
|
+
this.adapter = this.loadAdapter(newConfig.provider);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// Export for different module systems
|
|
1930
|
+
if (module.exports) {
|
|
1931
|
+
module.exports = AIKit;
|
|
1932
|
+
}
|
|
1933
|
+
} (AIKit));
|
|
1934
|
+
|
|
1935
|
+
var AIKitExports = AIKit.exports;
|
|
1936
|
+
|
|
1937
|
+
(function (module) {
|
|
1938
|
+
// Import all modules
|
|
1939
|
+
const modules = {
|
|
1940
|
+
CacheManager: CacheManagerExports,
|
|
1941
|
+
CostTracker: CostTrackerExports,
|
|
1942
|
+
ResponseValidator: ResponseValidatorExports,
|
|
1943
|
+
BaseAdapter: BaseAdapterExports,
|
|
1944
|
+
OpenAIAdapter: OpenAIAdapterExports,
|
|
1945
|
+
ClaudeAdapter: ClaudeAdapterExports,
|
|
1946
|
+
GeminiAdapter: GeminiAdapterExports,
|
|
1947
|
+
AIKit: AIKitExports
|
|
1948
|
+
};
|
|
1949
|
+
|
|
1950
|
+
// Attach to global for browser usage
|
|
1951
|
+
if (typeof window !== 'undefined') {
|
|
1952
|
+
Object.assign(window, modules);
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// Export for module systems
|
|
1956
|
+
module.exports = modules.AIKit;
|
|
1957
|
+
module.exports.default = modules.AIKit;
|
|
1958
|
+
Object.assign(module.exports, modules);
|
|
1959
|
+
} (src));
|
|
1960
|
+
|
|
1961
|
+
var srcExports = src.exports;
|
|
1962
|
+
var index = /*@__PURE__*/getDefaultExportFromCjs(srcExports);
|
|
1963
|
+
|
|
1964
|
+
return index;
|
|
1965
|
+
|
|
1966
|
+
}));
|
|
1967
|
+
//# sourceMappingURL=aikit.js.map
|