@lukaplayground/aikit 1.0.0 → 1.0.1

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