Merge remote-tracking branch 'upstream/main' into pr-45
# Conflicts: # src/config/index.ts # src/types/index.ts
This commit is contained in:
93 files changed
+9379
-649
No files matched your search
+134
-15
@@ -11,6 +11,8 @@ import { join } from 'path';
|
||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||
import type { LLMProvider, LLMConfig, FallbackEntry, OpsecLevel } from '../types/index.js';
|
||||
|
||||
type ApiKeyProvider = 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'gemini' | 'litellm' | 'local';
|
||||
|
||||
// =============================================================================
|
||||
// CONFIGURATION SCHEMA
|
||||
// =============================================================================
|
||||
@@ -23,7 +25,9 @@ export interface TempestSettings {
|
||||
anthropic?: string;
|
||||
openai?: string;
|
||||
xai?: string;
|
||||
gemini?: string;
|
||||
litellm?: string;
|
||||
local?: string;
|
||||
};
|
||||
|
||||
// Default LLM settings
|
||||
@@ -68,6 +72,12 @@ export interface TempestSettings {
|
||||
defaultModel: string;
|
||||
};
|
||||
|
||||
// Google Gemini — native Gemini API via its OpenAI-compatible endpoint
|
||||
gemini: {
|
||||
baseUrl: string;
|
||||
defaultModel: string;
|
||||
};
|
||||
|
||||
// Codex CLI/account subscription backend
|
||||
codex: {
|
||||
command: string;
|
||||
@@ -136,6 +146,14 @@ const DEFAULT_SETTINGS: TempestSettings = {
|
||||
defaultModel: 'gpt-4o',
|
||||
},
|
||||
|
||||
// Gemini's OpenAI-compatible surface lives under /v1beta/openai. The OpenAIAdapter posts to
|
||||
// `${baseUrl}/chat/completions`, so the /openai path segment is REQUIRED here — without it,
|
||||
// requests hit /v1beta/chat/completions, which is not a valid Gemini endpoint.
|
||||
gemini: {
|
||||
baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
defaultModel: 'gemini-2.5-flash',
|
||||
},
|
||||
|
||||
codex: {
|
||||
command: 'codex',
|
||||
defaultModel: 'codex-default',
|
||||
@@ -444,6 +462,34 @@ export const AVAILABLE_MODELS: Record<LLMProvider, ModelInfo[]> = {
|
||||
capabilities: ['reasoning', 'code', 'analysis', 'agents', 'tools'],
|
||||
},
|
||||
],
|
||||
// Native Gemini model ids (bare, no `google/` prefix) for Google's OpenAI-compatible endpoint.
|
||||
// Distinct from the `google/gemini-*` ids under `openrouter`, which route through OpenRouter.
|
||||
gemini: [
|
||||
{
|
||||
id: 'gemini-2.5-flash',
|
||||
name: 'Gemini 2.5 Flash (native)',
|
||||
provider: 'Google',
|
||||
contextWindow: 1000000,
|
||||
maxOutput: 8192,
|
||||
capabilities: ['reasoning', 'code', 'analysis', 'vision', 'fast', 'tools'],
|
||||
},
|
||||
{
|
||||
id: 'gemini-2.5-pro',
|
||||
name: 'Gemini 2.5 Pro (native)',
|
||||
provider: 'Google',
|
||||
contextWindow: 1000000,
|
||||
maxOutput: 8192,
|
||||
capabilities: ['reasoning', 'code', 'analysis', 'vision', 'multimodal', 'tools'],
|
||||
},
|
||||
{
|
||||
id: 'gemini-2.0-flash',
|
||||
name: 'Gemini 2.0 Flash (native)',
|
||||
provider: 'Google',
|
||||
contextWindow: 1000000,
|
||||
maxOutput: 8192,
|
||||
capabilities: ['reasoning', 'code', 'analysis', 'vision', 'fast', 'tools'],
|
||||
},
|
||||
],
|
||||
local: [
|
||||
{
|
||||
id: 'local-model',
|
||||
@@ -515,10 +561,14 @@ class ConfigManager {
|
||||
join(homedir(), '.env'),
|
||||
];
|
||||
|
||||
let envProvider: string | undefined;
|
||||
|
||||
for (const envPath of envPaths) {
|
||||
if (existsSync(envPath)) {
|
||||
const envContent = readFileSync(envPath, 'utf-8');
|
||||
const lines = envContent.split('\n');
|
||||
// Validation variables added
|
||||
const VALID_PROVIDERS = ['openrouter', 'venice', 'anthropic', 'openai', 'xai', 'gemini', 'litellm', 'local'];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
@@ -531,9 +581,18 @@ class ConfigManager {
|
||||
// empty OPENROUTER_API_KEY= — the .env file no longer clobbers it.
|
||||
if (key && value && process.env[key] === undefined) {
|
||||
process.env[key] = value;
|
||||
|
||||
// Track LLM_PROVIDER for default provider
|
||||
if (key === 'LLM_PROVIDER' && VALID_PROVIDERS.includes(value.toLowerCase())) {
|
||||
envProvider = value.toLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set TEMPEST_DEFAULT_PROVIDER if LLM_PROVIDER found
|
||||
if (envProvider && !process.env.TEMPEST_DEFAULT_PROVIDER) {
|
||||
process.env.TEMPEST_DEFAULT_PROVIDER = envProvider;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -569,7 +628,7 @@ class ConfigManager {
|
||||
/**
|
||||
* Set an API key for a provider
|
||||
*/
|
||||
setApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'litellm', key: string): void {
|
||||
setApiKey(provider: ApiKeyProvider, key: string): void {
|
||||
const apiKeys = this.config.get('apiKeys');
|
||||
apiKeys[provider] = key;
|
||||
this.config.set('apiKeys', apiKeys);
|
||||
@@ -578,14 +637,22 @@ class ConfigManager {
|
||||
/**
|
||||
* Get an API key for a provider
|
||||
*/
|
||||
getApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'litellm'): string | undefined {
|
||||
getApiKey(provider: ApiKeyProvider): string | undefined {
|
||||
// First check environment variables (highest priority)
|
||||
// local provider: a self-hosted/OpenAI-compatible server MAY require a bearer
|
||||
// (Zhipu/z.ai, Together, etc.) — accept TEMPEST_LOCAL_API_KEY or provider-specific vars.
|
||||
if (provider === 'local') {
|
||||
const localKey = process.env.TEMPEST_LOCAL_API_KEY?.trim() || process.env.ZAI_API_KEY?.trim() || process.env.ZHIPUAI_API_KEY?.trim();
|
||||
if (localKey) return localKey;
|
||||
return this.config.get('apiKeys')[provider];
|
||||
}
|
||||
const envVarMap = {
|
||||
openrouter: 'OPENROUTER_API_KEY',
|
||||
venice: 'VENICE_API_KEY',
|
||||
anthropic: 'ANTHROPIC_API_KEY',
|
||||
openai: 'OPENAI_API_KEY',
|
||||
xai: 'XAI_API_KEY',
|
||||
gemini: 'GEMINI_API_KEY',
|
||||
litellm: 'LITELLM_API_KEY',
|
||||
};
|
||||
|
||||
@@ -605,7 +672,7 @@ class ConfigManager {
|
||||
/**
|
||||
* Check if a provider has a valid API key configured
|
||||
*/
|
||||
hasApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'litellm'): boolean {
|
||||
hasApiKey(provider: ApiKeyProvider): boolean {
|
||||
const key = this.getApiKey(provider);
|
||||
return !!key && key.length > 10;
|
||||
}
|
||||
@@ -613,7 +680,7 @@ class ConfigManager {
|
||||
/**
|
||||
* Remove an API key
|
||||
*/
|
||||
removeApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'litellm'): void {
|
||||
removeApiKey(provider: ApiKeyProvider): void {
|
||||
const apiKeys = this.config.get('apiKeys');
|
||||
delete apiKeys[provider];
|
||||
this.config.set('apiKeys', apiKeys);
|
||||
@@ -641,6 +708,7 @@ class ConfigManager {
|
||||
if (this.hasApiKey('openai')) providers.push('openai');
|
||||
if (this.hasApiKey('xai')) providers.push('xai');
|
||||
if (this.hasLiteLLMProxy()) providers.push('litellm');
|
||||
if (this.hasApiKey('gemini')) providers.push('gemini');
|
||||
|
||||
// Codex uses the local Codex CLI/account auth instead of API-key storage.
|
||||
providers.push('codex');
|
||||
@@ -655,7 +723,16 @@ class ConfigManager {
|
||||
* Get the LLM configuration for the default or specified provider
|
||||
*/
|
||||
getLLMConfig(provider?: LLMProvider, model?: string): LLMConfig {
|
||||
const actualProvider = provider || this.config.get('defaultProvider');
|
||||
let actualProvider = provider;
|
||||
|
||||
if (!actualProvider) {
|
||||
const envProvider = process.env.TEMPEST_DEFAULT_PROVIDER?.trim();
|
||||
if (envProvider) {
|
||||
actualProvider = envProvider as LLMProvider;
|
||||
} else {
|
||||
actualProvider = this.config.get('defaultProvider');
|
||||
}
|
||||
}
|
||||
|
||||
let apiKey: string | undefined;
|
||||
let baseUrl: string | undefined;
|
||||
@@ -693,6 +770,12 @@ class ConfigManager {
|
||||
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 'gemini':
|
||||
// Google Gemini via its OpenAI-compatible endpoint (native tool-calling).
|
||||
apiKey = this.getApiKey('gemini');
|
||||
baseUrl = this.config.get('gemini').baseUrl;
|
||||
actualModel = model || this.config.get('gemini').defaultModel;
|
||||
break;
|
||||
case 'codex':
|
||||
actualModel = model || this.config.get('codex').defaultModel;
|
||||
break;
|
||||
@@ -700,13 +783,17 @@ class ConfigManager {
|
||||
actualModel = 'mock-model';
|
||||
break;
|
||||
case 'local':
|
||||
// Self-hosted, keyless. Defaults to Ollama; point TEMPEST_LOCAL_BASE_URL at
|
||||
// any OpenAI-compatible server (LM Studio :1234/v1, vLLM :8000/v1, llama.cpp)
|
||||
// and TEMPEST_LOCAL_MODEL at the model tag you're serving.
|
||||
// Self-hosted, usually keyless. Defaults to Ollama; point TEMPEST_LOCAL_BASE_URL at
|
||||
// any OpenAI-compatible server (LM Studio :1234/v1, vLLM :8000/v1, llama.cpp, Zhipu/z.ai
|
||||
// /api/paas/v4) and TEMPEST_LOCAL_MODEL at the model tag you're serving.
|
||||
// Some OpenAI-compatible servers require a real bearer (Zhipu, Together, etc.) —
|
||||
// TEMPEST_LOCAL_API_KEY (or a provider-specific env like ZAI_API_KEY) provides it.
|
||||
baseUrl = process.env.TEMPEST_LOCAL_BASE_URL?.trim() || 'http://localhost:11434/api';
|
||||
// 'local-model' is the placeholder id from AVAILABLE_MODELS — treat it as unset so
|
||||
// TEMPEST_LOCAL_MODEL (or the llama3 default) wins; a real tag passed in still takes priority.
|
||||
actualModel = (model && model !== 'local-model' ? model : undefined) || process.env.TEMPEST_LOCAL_MODEL?.trim() || 'llama3';
|
||||
// Placeholder ids from AVAILABLE_MODELS / the static UI are not real served model tags.
|
||||
// Treat them as unset so TEMPEST_LOCAL_MODEL (or the llama3 default) wins; a real tag
|
||||
// passed in still takes priority.
|
||||
actualModel = (model && !['local-model', 'local/ollama'].includes(model) ? model : undefined) || process.env.TEMPEST_LOCAL_MODEL?.trim() || 'llama3';
|
||||
apiKey = process.env.TEMPEST_LOCAL_API_KEY?.trim() || process.env.ZAI_API_KEY?.trim() || process.env.ZHIPUAI_API_KEY?.trim();
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown provider: ${actualProvider}`);
|
||||
@@ -738,7 +825,7 @@ class ConfigManager {
|
||||
const flag = (process.env.TEMPEST_MODEL_FALLBACK || '').trim().toLowerCase();
|
||||
if (!flag || ['0', 'false', 'off', 'no'].includes(flag)) return [];
|
||||
const chain: FallbackEntry[] = [];
|
||||
const add = (p: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'litellm') => {
|
||||
const add = (p: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'gemini' | 'litellm') => {
|
||||
if (p === primary) return;
|
||||
if (p === 'litellm' ? !this.hasLiteLLMProxy() : !this.hasApiKey(p)) return;
|
||||
chain.push({
|
||||
@@ -754,6 +841,7 @@ class ConfigManager {
|
||||
add('anthropic');
|
||||
add('openai');
|
||||
add('xai');
|
||||
add('gemini');
|
||||
return chain;
|
||||
}
|
||||
|
||||
@@ -777,12 +865,21 @@ class ConfigManager {
|
||||
case 'openai':
|
||||
this.config.set('defaultModel', this.config.get('openai').defaultModel);
|
||||
break;
|
||||
case 'xai':
|
||||
this.config.set('defaultModel', this.config.get('xai').defaultModel);
|
||||
break;
|
||||
case 'gemini':
|
||||
this.config.set('defaultModel', this.config.get('gemini').defaultModel);
|
||||
break;
|
||||
case 'litellm':
|
||||
this.config.set('defaultModel', this.config.get('litellm').defaultModel);
|
||||
break;
|
||||
case 'codex':
|
||||
this.config.set('defaultModel', this.config.get('codex').defaultModel);
|
||||
break;
|
||||
case 'local':
|
||||
this.config.set('defaultModel', process.env.TEMPEST_LOCAL_MODEL?.trim() || 'llama3');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,6 +900,12 @@ class ConfigManager {
|
||||
case 'openai':
|
||||
this.config.set('openai', { ...this.config.get('openai'), defaultModel: model });
|
||||
break;
|
||||
case 'xai':
|
||||
this.config.set('xai', { ...this.config.get('xai'), defaultModel: model });
|
||||
break;
|
||||
case 'gemini':
|
||||
this.config.set('gemini', { ...this.config.get('gemini'), defaultModel: model });
|
||||
break;
|
||||
case 'litellm':
|
||||
this.config.set('litellm', { ...this.config.get('litellm'), defaultModel: model });
|
||||
break;
|
||||
@@ -844,6 +947,7 @@ class ConfigManager {
|
||||
anthropic: settings.apiKeys.anthropic ? '***REDACTED***' : undefined,
|
||||
openai: settings.apiKeys.openai ? '***REDACTED***' : undefined,
|
||||
xai: settings.apiKeys.xai ? '***REDACTED***' : undefined,
|
||||
gemini: settings.apiKeys.gemini ? '***REDACTED***' : undefined,
|
||||
litellm: settings.apiKeys.litellm ? '***REDACTED***' : undefined,
|
||||
},
|
||||
};
|
||||
@@ -883,6 +987,21 @@ XAI_API_KEY=
|
||||
LITELLM_BASE_URL=http://localhost:4000/v1
|
||||
LITELLM_API_KEY=
|
||||
LITELLM_MODEL=gpt-4o
|
||||
|
||||
# Google Gemini API Key (direct Gemini API via its OpenAI-compatible endpoint)
|
||||
# Get your key at: https://aistudio.google.com/apikey
|
||||
GEMINI_API_KEY=
|
||||
|
||||
# Local model (Ollama / LM Studio / vLLM / llama.cpp, or any OpenAI-compatible server)
|
||||
# Point TEMPEST_LOCAL_BASE_URL at the server root (Ollama default shown below).
|
||||
# For an OpenAI-compatible server, use a versioned path: LM Studio :1234/v1,
|
||||
# vLLM :8000/v1, or e.g. Zhipu/z.ai /api/paas/v4 — any /vN path speaks the
|
||||
# OpenAI wire (/chat/completions, choices[]); otherwise the Ollama native format is used.
|
||||
# Most local servers are keyless; if yours requires a bearer (Zhipu, Together, …)
|
||||
# set TEMPEST_LOCAL_API_KEY (ZAI_API_KEY / ZHIPUAI_API_KEY are accepted as aliases).
|
||||
TEMPEST_LOCAL_BASE_URL=http://localhost:11434/api
|
||||
TEMPEST_LOCAL_MODEL=llama3
|
||||
TEMPEST_LOCAL_API_KEY=
|
||||
`;
|
||||
writeFileSync(filePath, template);
|
||||
}
|
||||
@@ -895,8 +1014,8 @@ LITELLM_MODEL=gpt-4o
|
||||
export const config = new ConfigManager();
|
||||
|
||||
// Helper functions for quick access
|
||||
export const getApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'litellm') => config.getApiKey(provider);
|
||||
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' | 'litellm') => config.hasApiKey(provider);
|
||||
export const getApiKey = (provider: ApiKeyProvider) => config.getApiKey(provider);
|
||||
export const setApiKey = (provider: ApiKeyProvider, key: string) => config.setApiKey(provider, key);
|
||||
export const hasApiKey = (provider: ApiKeyProvider) => config.hasApiKey(provider);
|
||||
export const getLLMConfig = (provider?: LLMProvider, model?: string) => config.getLLMConfig(provider, model);
|
||||
export const getConfiguredProviders = () => config.getConfiguredProviders();
|
||||
Reference in new issue
Block a user