-
-
Notifications
You must be signed in to change notification settings - Fork 9.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ feat: support multiple API Keys (#1345)
* ✨ feat: Support multiple API keys [RFC 027] * 📝 docs: add env variable API_KEY_SELECT_MODE * 🔧 chore: Adjust the parameter API_KEY_SELECT_MODE to be optional * 🔧 fix: Adjustments made according to Code Review requirements * ✅ test: add test for ApiKeyManager * 🔧 fix: Support for multiple API Keys from user input on the client side * 🔧 chore: handle Perplexity API Key * 🔧 chore: update OpenAI or Azure API Key select * 🔧 chore: update OpenAI or Azure API Key select
- Loading branch information
1 parent
a1a055d
commit 17c5da3
Showing
9 changed files
with
222 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
import { nanoid } from 'nanoid'; | ||
import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
||
import { getServerConfig } from '@/config/server'; | ||
|
||
import { ApiKeyManager } from './apiKeyManager'; | ||
|
||
function generateKeys(count: number = 1) { | ||
return new Array(count) | ||
.fill('') | ||
.map(() => { | ||
return `sk-${nanoid()}`; | ||
}) | ||
.join(','); | ||
} | ||
|
||
// Stub the global process object to safely mock environment variables | ||
vi.stubGlobal('process', { | ||
...process, // Preserve the original process object | ||
env: { ...process.env }, // Clone the environment variables object for modification | ||
}); | ||
|
||
describe('apiKeyManager', () => { | ||
beforeEach(() => { | ||
vi.restoreAllMocks(); | ||
}); | ||
|
||
describe('API Key unset or empty', () => { | ||
it('should return an empty string when API_KEY_SELECT_MODE is unset', () => { | ||
const apiKeyManager = new ApiKeyManager(); | ||
|
||
expect(apiKeyManager.pick('')).toBe(''); | ||
expect(apiKeyManager.pick()).toBe(''); | ||
}); | ||
|
||
it('should return an empty string when API_KEY_SELECT_MODE is "random"', () => { | ||
process.env.API_KEY_SELECT_MODE = 'random'; | ||
const apiKeyManager = new ApiKeyManager(); | ||
|
||
expect(apiKeyManager.pick('')).toBe(''); | ||
expect(apiKeyManager.pick()).toBe(''); | ||
}); | ||
|
||
it('should return an empty string when API_KEY_SELECT_MODE is "turn"', () => { | ||
process.env.API_KEY_SELECT_MODE = 'turn'; | ||
const apiKeyManager = new ApiKeyManager(); | ||
|
||
expect(apiKeyManager.pick('')).toBe(''); | ||
expect(apiKeyManager.pick()).toBe(''); | ||
}); | ||
}); | ||
|
||
describe('single API Key', () => { | ||
it('should return the only API Key when API_KEY_SELECT_MODE is unset', () => { | ||
const apiKeyManager = new ApiKeyManager(); | ||
const apiKeyStr = generateKeys(1); | ||
|
||
expect(apiKeyManager.pick(apiKeyStr)).toBe(apiKeyStr); | ||
}); | ||
|
||
it('should return the only API when API_KEY_SELECT_MODE is "random"', () => { | ||
process.env.API_KEY_SELECT_MODE = 'random'; | ||
const apiKeyStr = generateKeys(1); | ||
const apiKeyManager = new ApiKeyManager(); | ||
|
||
expect(apiKeyManager.pick(apiKeyStr)).toBe(apiKeyStr); | ||
// multiple | ||
expect(apiKeyManager.pick(apiKeyStr)).toBe(apiKeyStr); | ||
expect(apiKeyManager.pick(apiKeyStr)).toBe(apiKeyStr); | ||
}); | ||
|
||
it('should return the only API when API_KEY_SELECT_MODE is "turn"', () => { | ||
process.env.API_KEY_SELECT_MODE = 'turn'; | ||
const apiKeyStr = generateKeys(1); | ||
const apiKeyManager = new ApiKeyManager(); | ||
|
||
expect(apiKeyManager.pick(apiKeyStr)).toBe(apiKeyStr); | ||
// multiple | ||
expect(apiKeyManager.pick(apiKeyStr)).toBe(apiKeyStr); | ||
}); | ||
}); | ||
|
||
describe('multiple API Keys', () => { | ||
it('should return a random API Key when API_KEY_SELECT_MODE is unset', () => { | ||
const apiKeyStr = generateKeys(5); | ||
const apiKeys = apiKeyStr.split(','); | ||
const apiKeyManager = new ApiKeyManager(); | ||
const keyLen = apiKeys.length * 2; // multiple round | ||
|
||
for (let i = 0; i < keyLen; i++) { | ||
expect(apiKeys).toContain(apiKeyManager.pick(apiKeyStr)); | ||
} | ||
}); | ||
|
||
it('should return a random API Key when environment variable of API_KEY_SELECT_MODE is "random"', () => { | ||
process.env.API_KEY_SELECT_MODE = 'random'; | ||
const apiKeyStr = generateKeys(5); | ||
const apiKeys = apiKeyStr.split(','); | ||
const apiKeyManager = new ApiKeyManager(); | ||
const keyLen = apiKeys.length * 2; // multiple round | ||
|
||
for (let i = 0; i < keyLen; i++) { | ||
expect(apiKeys).toContain(apiKeyManager.pick(apiKeyStr)); | ||
} | ||
}); | ||
|
||
it('should return API Keys sequentially when environment variable of API_KEY_SELECT_MODE is "turn"', () => { | ||
process.env.API_KEY_SELECT_MODE = 'turn'; | ||
const apiKeyStr = generateKeys(5); | ||
const apiKeys = apiKeyStr.split(','); | ||
const apiKeyManager = new ApiKeyManager(); | ||
|
||
const total = apiKeys.length; | ||
const rounds = total * 2; | ||
for (let i = 0; i < total; i++) { | ||
expect(apiKeyManager.pick(apiKeyStr)).toBe(apiKeys[i % total]); | ||
} | ||
}); | ||
|
||
it('should return a random API Key when API_KEY_SELECT_MODE is anything other than "random" or "turn"', () => { | ||
process.env.API_KEY_SELECT_MODE = nanoid(); | ||
const apiKeyStr = generateKeys(5); | ||
const apiKeys = apiKeyStr.split(','); | ||
const apiKeyManager = new ApiKeyManager(); | ||
const keyLen = apiKeys.length * 2; // multiple round | ||
|
||
for (let i = 0; i < keyLen; i++) { | ||
expect(apiKeys).toContain(apiKeyManager.pick(apiKeyStr)); | ||
} | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { getServerConfig } from '@/config/server'; | ||
|
||
interface KeyStore { | ||
index: number; | ||
keyLen: number; | ||
keys: string[]; | ||
} | ||
|
||
export class ApiKeyManager { | ||
private _cache: Map<string, KeyStore> = new Map(); | ||
|
||
private _mode: string; | ||
|
||
constructor() { | ||
const { API_KEY_SELECT_MODE: mode = 'random' } = getServerConfig(); | ||
|
||
this._mode = mode; | ||
} | ||
|
||
private getKeyStore(apiKeys: string) { | ||
let store = this._cache.get(apiKeys); | ||
|
||
if (!store) { | ||
const keys = apiKeys.split(',').filter((_) => !!_.trim()); | ||
|
||
store = { index: 0, keyLen: keys.length, keys } as KeyStore; | ||
this._cache.set(apiKeys, store); | ||
} | ||
|
||
return store; | ||
} | ||
|
||
pick(apiKeys: string = '') { | ||
if (!apiKeys) return ''; | ||
|
||
const store = this.getKeyStore(apiKeys); | ||
let index = 0; | ||
|
||
if (this._mode === 'turn') index = store.index++ % store.keyLen; | ||
if (this._mode === 'random') index = Math.floor(Math.random() * store.keyLen); | ||
|
||
return store.keys[index]; | ||
} | ||
} | ||
|
||
export default new ApiKeyManager(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters