feat: add LiteLLM as AI gateway provider

This commit is contained in:
RheagalFire committed 2026-07-06 21:46:48 +05:30
1 parent 2f20ee5820
commit 26217cbe33
5 files changed
+192 -11

No files matched your search

+63 -9
View File
@@ -23,6 +23,7 @@ export interface TempestSettings {
anthropic?: string; anthropic?: string;
openai?: string; openai?: string;
xai?: string; xai?: string;
litellm?: string;
}; };
// Default LLM settings // Default LLM settings
@@ -61,6 +62,12 @@ export interface TempestSettings {
defaultModel: string; defaultModel: string;
}; };
// LiteLLM AI gateway proxy — routes to 100+ LLM providers via unified API
litellm: {
baseUrl: string;
defaultModel: string;
};
// Codex CLI/account subscription backend // Codex CLI/account subscription backend
codex: { codex: {
command: string; command: string;
@@ -124,6 +131,11 @@ const DEFAULT_SETTINGS: TempestSettings = {
defaultModel: 'grok-build-0.1', defaultModel: 'grok-build-0.1',
}, },
litellm: {
baseUrl: 'http://localhost:4000/v1',
defaultModel: 'gpt-4o',
},
codex: { codex: {
command: 'codex', command: 'codex',
defaultModel: 'codex-default', defaultModel: 'codex-default',
@@ -381,6 +393,16 @@ export const AVAILABLE_MODELS: Record<LLMProvider, ModelInfo[]> = {
capabilities: ['reasoning', 'code', 'analysis', 'vision', 'fast'], capabilities: ['reasoning', 'code', 'analysis', 'vision', 'fast'],
}, },
], ],
litellm: [
{
id: 'gpt-4o',
name: 'Any model via LiteLLM proxy (default: gpt-4o)',
provider: 'LiteLLM',
contextWindow: 128000,
maxOutput: 4096,
capabilities: ['reasoning', 'code', 'analysis', 'vision', 'tools'],
},
],
codex: [ codex: [
{ {
id: 'codex-default', id: 'codex-default',
@@ -547,7 +569,7 @@ class ConfigManager {
/** /**
* Set an API key for a provider * Set an API key for a provider
*/ */
setApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai', key: string): void { setApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'litellm', key: string): void {
const apiKeys = this.config.get('apiKeys'); const apiKeys = this.config.get('apiKeys');
apiKeys[provider] = key; apiKeys[provider] = key;
this.config.set('apiKeys', apiKeys); this.config.set('apiKeys', apiKeys);
@@ -556,7 +578,7 @@ class ConfigManager {
/** /**
* Get an API key for a provider * Get an API key for a provider
*/ */
getApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai'): string | undefined { getApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'litellm'): string | undefined {
// First check environment variables (highest priority) // First check environment variables (highest priority)
const envVarMap = { const envVarMap = {
openrouter: 'OPENROUTER_API_KEY', openrouter: 'OPENROUTER_API_KEY',
@@ -564,6 +586,7 @@ class ConfigManager {
anthropic: 'ANTHROPIC_API_KEY', anthropic: 'ANTHROPIC_API_KEY',
openai: 'OPENAI_API_KEY', openai: 'OPENAI_API_KEY',
xai: 'XAI_API_KEY', xai: 'XAI_API_KEY',
litellm: 'LITELLM_API_KEY',
}; };
// Force a fully UNCONFIGURED server (no key from env OR the saved store) — used by // Force a fully UNCONFIGURED server (no key from env OR the saved store) — used by
@@ -582,7 +605,7 @@ class ConfigManager {
/** /**
* Check if a provider has a valid API key configured * Check if a provider has a valid API key configured
*/ */
hasApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai'): boolean { hasApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'litellm'): boolean {
const key = this.getApiKey(provider); const key = this.getApiKey(provider);
return !!key && key.length > 10; return !!key && key.length > 10;
} }
@@ -590,12 +613,22 @@ class ConfigManager {
/** /**
* Remove an API key * Remove an API key
*/ */
removeApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai'): void { removeApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'litellm'): void {
const apiKeys = this.config.get('apiKeys'); const apiKeys = this.config.get('apiKeys');
delete apiKeys[provider]; delete apiKeys[provider];
this.config.set('apiKeys', apiKeys); this.config.set('apiKeys', apiKeys);
} }
/**
* Check if a LiteLLM proxy is configured (base URL set via env or config)
*/
hasLiteLLMProxy(): boolean {
const envUrl = process.env.LITELLM_BASE_URL?.trim();
if (envUrl) return true;
const storedUrl = this.config.get('litellm')?.baseUrl;
return !!storedUrl && storedUrl !== 'http://localhost:4000/v1';
}
/** /**
* Get configured providers (those with API keys) * Get configured providers (those with API keys)
*/ */
@@ -607,6 +640,7 @@ class ConfigManager {
if (this.hasApiKey('anthropic')) providers.push('anthropic'); if (this.hasApiKey('anthropic')) providers.push('anthropic');
if (this.hasApiKey('openai')) providers.push('openai'); if (this.hasApiKey('openai')) providers.push('openai');
if (this.hasApiKey('xai')) providers.push('xai'); if (this.hasApiKey('xai')) providers.push('xai');
if (this.hasLiteLLMProxy()) providers.push('litellm');
// Codex uses the local Codex CLI/account auth instead of API-key storage. // Codex uses the local Codex CLI/account auth instead of API-key storage.
providers.push('codex'); providers.push('codex');
@@ -654,6 +688,11 @@ class ConfigManager {
baseUrl = this.config.get('xai').baseUrl; baseUrl = this.config.get('xai').baseUrl;
actualModel = model || this.config.get('xai').defaultModel; actualModel = model || this.config.get('xai').defaultModel;
break; break;
case 'litellm':
apiKey = this.getApiKey('litellm');
baseUrl = process.env.LITELLM_BASE_URL?.trim() || this.config.get('litellm').baseUrl;
actualModel = model || process.env.LITELLM_MODEL?.trim() || this.config.get('litellm').defaultModel;
break;
case 'codex': case 'codex':
actualModel = model || this.config.get('codex').defaultModel; actualModel = model || this.config.get('codex').defaultModel;
break; break;
@@ -699,8 +738,9 @@ class ConfigManager {
const flag = (process.env.TEMPEST_MODEL_FALLBACK || '').trim().toLowerCase(); const flag = (process.env.TEMPEST_MODEL_FALLBACK || '').trim().toLowerCase();
if (!flag || ['0', 'false', 'off', 'no'].includes(flag)) return []; if (!flag || ['0', 'false', 'off', 'no'].includes(flag)) return [];
const chain: FallbackEntry[] = []; const chain: FallbackEntry[] = [];
const add = (p: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai') => { const add = (p: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'litellm') => {
if (p === primary || !this.hasApiKey(p)) return; if (p === primary) return;
if (p === 'litellm' ? !this.hasLiteLLMProxy() : !this.hasApiKey(p)) return;
chain.push({ chain.push({
provider: p, provider: p,
model: this.config.get(p).defaultModel, model: this.config.get(p).defaultModel,
@@ -710,6 +750,7 @@ class ConfigManager {
}; };
add('openrouter'); add('openrouter');
add('venice'); add('venice');
add('litellm');
add('anthropic'); add('anthropic');
add('openai'); add('openai');
add('xai'); add('xai');
@@ -736,6 +777,9 @@ class ConfigManager {
case 'openai': case 'openai':
this.config.set('defaultModel', this.config.get('openai').defaultModel); this.config.set('defaultModel', this.config.get('openai').defaultModel);
break; break;
case 'litellm':
this.config.set('defaultModel', this.config.get('litellm').defaultModel);
break;
case 'codex': case 'codex':
this.config.set('defaultModel', this.config.get('codex').defaultModel); this.config.set('defaultModel', this.config.get('codex').defaultModel);
break; break;
@@ -759,6 +803,9 @@ class ConfigManager {
case 'openai': case 'openai':
this.config.set('openai', { ...this.config.get('openai'), defaultModel: model }); this.config.set('openai', { ...this.config.get('openai'), defaultModel: model });
break; break;
case 'litellm':
this.config.set('litellm', { ...this.config.get('litellm'), defaultModel: model });
break;
case 'codex': case 'codex':
this.config.set('codex', { ...this.config.get('codex'), defaultModel: model }); this.config.set('codex', { ...this.config.get('codex'), defaultModel: model });
break; break;
@@ -797,6 +844,7 @@ class ConfigManager {
anthropic: settings.apiKeys.anthropic ? '***REDACTED***' : undefined, anthropic: settings.apiKeys.anthropic ? '***REDACTED***' : undefined,
openai: settings.apiKeys.openai ? '***REDACTED***' : undefined, openai: settings.apiKeys.openai ? '***REDACTED***' : undefined,
xai: settings.apiKeys.xai ? '***REDACTED***' : undefined, xai: settings.apiKeys.xai ? '***REDACTED***' : undefined,
litellm: settings.apiKeys.litellm ? '***REDACTED***' : undefined,
}, },
}; };
writeFileSync(filePath, JSON.stringify(safeSettings, null, 2)); writeFileSync(filePath, JSON.stringify(safeSettings, null, 2));
@@ -829,6 +877,12 @@ OPENAI_API_KEY=
# xAI API Key # xAI API Key
# Get your key at: https://console.x.ai/ # Get your key at: https://console.x.ai/
XAI_API_KEY= XAI_API_KEY=
# LiteLLM Proxy (AI gateway — 100+ providers via single API)
# Docs: https://docs.litellm.ai/docs/proxy/quick_start
LITELLM_BASE_URL=http://localhost:4000/v1
LITELLM_API_KEY=
LITELLM_MODEL=gpt-4o
`; `;
writeFileSync(filePath, template); writeFileSync(filePath, template);
} }
@@ -841,8 +895,8 @@ XAI_API_KEY=
export const config = new ConfigManager(); export const config = new ConfigManager();
// Helper functions for quick access // Helper functions for quick access
export const getApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai') => config.getApiKey(provider); export const getApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'litellm') => config.getApiKey(provider);
export const setApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai', key: string) => config.setApiKey(provider, key); export const setApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'litellm', key: string) => config.setApiKey(provider, key);
export const hasApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai') => config.hasApiKey(provider); export const hasApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'litellm') => config.hasApiKey(provider);
export const getLLMConfig = (provider?: LLMProvider, model?: string) => config.getLLMConfig(provider, model); export const getLLMConfig = (provider?: LLMProvider, model?: string) => config.getLLMConfig(provider, model);
export const getConfiguredProviders = () => config.getConfiguredProviders(); export const getConfiguredProviders = () => config.getConfiguredProviders();
+1
View File
@@ -44,6 +44,7 @@ export {
createAnthropicBackbone, createAnthropicBackbone,
createOpenRouterBackbone, createOpenRouterBackbone,
createOpenAIBackbone, createOpenAIBackbone,
createLiteLLMBackbone,
createMockBackbone, createMockBackbone,
createLocalBackbone, createLocalBackbone,
createBestAvailableBackbone, createBestAvailableBackbone,
+36 -1
View File
@@ -4,6 +4,7 @@
* Multi-provider LLM integration supporting: * Multi-provider LLM integration supporting:
* - OpenRouter (recommended - access to Claude, GPT-4, Llama, etc.) * - OpenRouter (recommended - access to Claude, GPT-4, Llama, etc.)
* - Venice (OpenAI-compatible, privacy-focused / uncensored models) * - Venice (OpenAI-compatible, privacy-focused / uncensored models)
* - LiteLLM (OpenAI-compatible AI gateway — 100+ providers via unified proxy)
* - Anthropic (direct Claude access) * - Anthropic (direct Claude access)
* - OpenAI (GPT models) * - OpenAI (GPT models)
* - Mock (for testing) * - Mock (for testing)
@@ -360,6 +361,28 @@ class VeniceAdapter extends OpenRouterAdapter {
} }
} }
// =============================================================================
// LITELLM ADAPTER
// =============================================================================
// LiteLLM is an AI gateway proxy that routes to 100+ LLM providers through a
// single OpenAI-compatible API. Same wire shape as OpenRouter (Bearer auth,
// POST /chat/completions, identical request/response + tool-calling + SSE
// stream), so it reuses the entire OpenRouter adapter and only differs in its
// default base URL and the key-required error message.
class LiteLLMAdapter extends OpenRouterAdapter {
name = 'litellm';
validateConfig(): { valid: boolean; error?: string } {
if (!this.config.baseUrl) {
return {
valid: false,
error: 'LiteLLM proxy base URL is required. Set LITELLM_BASE_URL or configure via tempest setup. Docs: https://docs.litellm.ai/docs/proxy/quick_start',
};
}
return { valid: true };
}
}
// ============================================================================= // =============================================================================
// ANTHROPIC ADAPTER // ANTHROPIC ADAPTER
// ============================================================================= // =============================================================================
@@ -1197,6 +1220,8 @@ export class LLMBackbone extends EventEmitter<LLMEvents> {
return new OpenRouterAdapter(config); return new OpenRouterAdapter(config);
case 'venice': case 'venice':
return new VeniceAdapter(config); return new VeniceAdapter(config);
case 'litellm':
return new LiteLLMAdapter(config);
case 'anthropic': case 'anthropic':
return new AnthropicAdapter(config); return new AnthropicAdapter(config);
case 'openai': case 'openai':
@@ -1477,6 +1502,13 @@ export function createOpenAIBackbone(apiKey?: string, model?: string): LLMBackbo
return new LLMBackbone(llmConfig); return new LLMBackbone(llmConfig);
} }
export function createLiteLLMBackbone(apiKey?: string, model?: string, baseUrl?: string): LLMBackbone {
const llmConfig = config.getLLMConfig('litellm', model);
if (apiKey) llmConfig.apiKey = apiKey;
if (baseUrl) llmConfig.baseUrl = baseUrl;
return new LLMBackbone(llmConfig);
}
export function createMockBackbone(): LLMBackbone { export function createMockBackbone(): LLMBackbone {
return new LLMBackbone({ return new LLMBackbone({
provider: 'mock', provider: 'mock',
@@ -1500,7 +1532,7 @@ export function createLocalBackbone(model?: string, baseUrl?: string): LLMBackbo
* Create the best available backbone based on configured API keys * Create the best available backbone based on configured API keys
*/ */
export function createBestAvailableBackbone(): LLMBackbone { export function createBestAvailableBackbone(): LLMBackbone {
// Priority: OpenRouter > Venice > Anthropic > OpenAI > Local > Mock // Priority: OpenRouter > Venice > LiteLLM > Anthropic > OpenAI > Local > Mock
const providers = config.getConfiguredProviders(); const providers = config.getConfiguredProviders();
if (providers.includes('openrouter')) { if (providers.includes('openrouter')) {
@@ -1509,6 +1541,9 @@ export function createBestAvailableBackbone(): LLMBackbone {
if (providers.includes('venice')) { if (providers.includes('venice')) {
return createVeniceBackbone(); return createVeniceBackbone();
} }
if (providers.includes('litellm')) {
return createLiteLLMBackbone();
}
if (providers.includes('anthropic')) { if (providers.includes('anthropic')) {
return createAnthropicBackbone(); return createAnthropicBackbone();
} }
+91
View File
@@ -240,6 +240,88 @@ async function setupOpenAIKey(): Promise<boolean> {
} }
} }
async function setupLiteLLMProxy(): Promise<boolean> {
console.log('');
showInfo('LiteLLM is an AI gateway proxy that routes to 100+ LLM providers.');
showInfo('Docs: ' + chalk.underline('https://docs.litellm.ai/docs/proxy/quick_start'));
console.log('');
const { baseUrl } = await inquirer.prompt([
{
type: 'input',
name: 'baseUrl',
message: 'Enter your LiteLLM proxy base URL:',
default: 'http://localhost:4000/v1',
validate: (input: string) => {
if (!input || !input.startsWith('http')) {
return 'Please enter a valid URL (e.g. http://localhost:4000/v1)';
}
return true;
},
},
]);
const { apiKey } = await inquirer.prompt([
{
type: 'password',
name: 'apiKey',
message: 'Enter your LiteLLM API key (leave blank if not required):',
mask: '*',
},
]);
const { model } = await inquirer.prompt([
{
type: 'input',
name: 'model',
message: 'Enter the default model (uses LiteLLM model format, e.g. gpt-4o, anthropic/claude-sonnet-4-6):',
default: 'gpt-4o',
},
]);
const spinner = ora('Testing LiteLLM proxy connection...').start();
try {
const llm = new LLMBackbone({
provider: 'litellm',
model,
apiKey: apiKey || undefined,
baseUrl,
maxTokens: 10,
temperature: 0,
});
await llm.prompt('Hello', undefined, { maxTokens: 10 });
spinner.succeed('LiteLLM proxy connection successful!');
config.set('litellm', { baseUrl, defaultModel: model });
if (apiKey) setApiKey('litellm', apiKey);
showSuccess('LiteLLM proxy configured successfully!');
return true;
} catch (error) {
spinner.fail('LiteLLM proxy connection failed');
showError(`Error: ${error instanceof Error ? error.message : String(error)}`);
const { saveAnyway } = await inquirer.prompt([
{
type: 'confirm',
name: 'saveAnyway',
message: 'Save the configuration anyway? (proxy may not be running yet)',
default: true,
},
]);
if (saveAnyway) {
config.set('litellm', { baseUrl, defaultModel: model });
if (apiKey) setApiKey('litellm', apiKey);
showSuccess('LiteLLM proxy configuration saved.');
return true;
}
return false;
}
}
// ============================================================================= // =============================================================================
// MODEL SELECTION // MODEL SELECTION
// ============================================================================= // =============================================================================
@@ -285,6 +367,7 @@ You'll need an API key from one of these providers:
• ${chalk.cyan('OpenRouter')} (recommended) - Access multiple models • ${chalk.cyan('OpenRouter')} (recommended) - Access multiple models
• ${chalk.cyan('Anthropic')} - Direct Claude access • ${chalk.cyan('Anthropic')} - Direct Claude access
• ${chalk.cyan('OpenAI')} - GPT models • ${chalk.cyan('OpenAI')} - GPT models
• ${chalk.cyan('LiteLLM')} - AI gateway proxy (100+ providers)
The setup will guide you through: The setup will guide you through:
1. Adding your API key(s) 1. Adding your API key(s)
@@ -384,6 +467,10 @@ async function setupApiKeys(): Promise<void> {
name: `OpenAI ${hasApiKey('openai') ? chalk.green('(configured)') : ''}`, name: `OpenAI ${hasApiKey('openai') ? chalk.green('(configured)') : ''}`,
value: 'openai', value: 'openai',
}, },
{
name: `LiteLLM Proxy ${config.hasLiteLLMProxy() ? chalk.green('(configured)') : chalk.gray('(100+ providers via gateway)')}`,
value: 'litellm',
},
], ],
}, },
]); ]);
@@ -402,6 +489,9 @@ async function setupApiKeys(): Promise<void> {
case 'openai': case 'openai':
await setupOpenAIKey(); await setupOpenAIKey();
break; break;
case 'litellm':
await setupLiteLLMProxy();
break;
} }
} }
} }
@@ -413,6 +503,7 @@ async function setupProvider(): Promise<void> {
if (hasApiKey('venice')) configuredProviders.push({ name: 'Venice', value: 'venice' }); if (hasApiKey('venice')) configuredProviders.push({ name: 'Venice', value: 'venice' });
if (hasApiKey('anthropic')) configuredProviders.push({ name: 'Anthropic', value: 'anthropic' }); if (hasApiKey('anthropic')) configuredProviders.push({ name: 'Anthropic', value: 'anthropic' });
if (hasApiKey('openai')) configuredProviders.push({ name: 'OpenAI', value: 'openai' }); if (hasApiKey('openai')) configuredProviders.push({ name: 'OpenAI', value: 'openai' });
if (config.hasLiteLLMProxy()) configuredProviders.push({ name: 'LiteLLM Proxy', value: 'litellm' });
if (configuredProviders.length === 0) { if (configuredProviders.length === 0) {
showWarning('No API keys configured. Please add at least one API key first.'); showWarning('No API keys configured. Please add at least one API key first.');
+1 -1
View File
@@ -6,7 +6,7 @@
// LLM CONFIGURATION // LLM CONFIGURATION
// ============================================================================= // =============================================================================
export type LLMProvider = 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'codex' | 'mock' | 'local' | 'local-agent'; export type LLMProvider = 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'litellm' | 'codex' | 'mock' | 'local' | 'local-agent';
export interface LLMConfig { export interface LLMConfig {
provider: LLMProvider; provider: LLMProvider;