@lukaplayground/aikit 1.0.0

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