T3MP3ST public launch build
This commit is contained in:
1 parent
972247b881
commit
184c7d4e59
1589 files changed
+57150
-506
No files matched your search
Regular → Executable
File mode changed.
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* agentHome() — fix for "redirected HOME breaks local-agent detection".
|
||||
*
|
||||
* T3MP3ST may run with $HOME redirected (app-config storage), but the agent CLIs keep their
|
||||
* own auth artifacts (~/.claude.json, ~/.codex/auth.json, ~/.hermes/.env) in the REAL home.
|
||||
* If detection/spawn used the redirected HOME, an installed-and-authed agent would look
|
||||
* unavailable (dead Settings checkboxes). agentHome() resolves the real home independently.
|
||||
*/
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { userInfo } from 'os';
|
||||
import { agentHome } from '../agent/local-agents.js';
|
||||
|
||||
describe('agentHome — real CLI-auth home, independent of a redirected $HOME', () => {
|
||||
const saved = { override: process.env.T3MP3ST_AGENT_HOME, home: process.env.HOME };
|
||||
const restore = (k: 'T3MP3ST_AGENT_HOME' | 'HOME', v: string | undefined): void => {
|
||||
if (v === undefined) delete process.env[k];
|
||||
else process.env[k] = v;
|
||||
};
|
||||
afterEach(() => {
|
||||
restore('T3MP3ST_AGENT_HOME', saved.override);
|
||||
restore('HOME', saved.home);
|
||||
});
|
||||
|
||||
it('prefers the explicit T3MP3ST_AGENT_HOME override over a redirected HOME', () => {
|
||||
process.env.T3MP3ST_AGENT_HOME = '/real/user/home';
|
||||
process.env.HOME = '/tmp/redirected';
|
||||
expect(agentHome()).toBe('/real/user/home');
|
||||
});
|
||||
|
||||
it('auto-recovers the real home (OS user DB) when HOME is redirected and no override is set', () => {
|
||||
delete process.env.T3MP3ST_AGENT_HOME;
|
||||
process.env.HOME = '/tmp/redirected-should-be-ignored';
|
||||
// userInfo().homedir reads getpwuid, NOT $HOME → the true home
|
||||
expect(agentHome()).toBe(userInfo().homedir);
|
||||
expect(agentHome()).not.toBe('/tmp/redirected-should-be-ignored');
|
||||
});
|
||||
|
||||
it('ignores a blank/whitespace override and falls through to the real home', () => {
|
||||
process.env.T3MP3ST_AGENT_HOME = ' ';
|
||||
expect(agentHome()).toBe(userInfo().homedir);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Venice provider — OpenRouter-parity wiring.
|
||||
* Venice is OpenAI-compatible, so it plugs in everywhere OpenRouter does: the LLMProvider
|
||||
* union, the config key/provider block, getLLMConfig, the fallback ladder, and a
|
||||
* VeniceAdapter that reuses the OpenRouter adapter with Venice's base URL. This pins that
|
||||
* the provider resolves end-to-end so a refactor can't silently drop it.
|
||||
*/
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { config, AVAILABLE_MODELS } from '../config/index.js';
|
||||
import { createVeniceBackbone } from '../llm/index.js';
|
||||
|
||||
const KEY = 'venice-key-abcdef123456';
|
||||
|
||||
describe('Venice provider wiring (parity with OpenRouter)', () => {
|
||||
afterEach(() => { delete process.env.VENICE_API_KEY; });
|
||||
|
||||
it('resolves the Venice base URL + default model + key from VENICE_API_KEY', () => {
|
||||
process.env.VENICE_API_KEY = KEY;
|
||||
const cfg = config.getLLMConfig('venice');
|
||||
expect(cfg.provider).toBe('venice');
|
||||
expect(cfg.baseUrl).toBe('https://api.venice.ai/api/v1');
|
||||
expect(cfg.model).toBe('llama-3.3-70b');
|
||||
expect(cfg.apiKey).toBe(KEY);
|
||||
});
|
||||
|
||||
it('the Venice backbone validates and reports the venice provider', () => {
|
||||
process.env.VENICE_API_KEY = KEY;
|
||||
const bb = createVeniceBackbone();
|
||||
expect(bb.getProvider()).toBe('venice');
|
||||
expect(bb.validateConfig().valid).toBe(true);
|
||||
});
|
||||
|
||||
it('surfaces venice in AVAILABLE_MODELS and getConfiguredProviders', () => {
|
||||
process.env.VENICE_API_KEY = KEY;
|
||||
expect(AVAILABLE_MODELS.venice?.length).toBeGreaterThan(0);
|
||||
expect(config.getConfiguredProviders()).toContain('venice');
|
||||
});
|
||||
});
|
||||
Regular → Executable
File mode changed.
@@ -14,7 +14,7 @@
|
||||
|
||||
import { execFile, execFileSync, spawn } from 'child_process';
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'fs';
|
||||
import { homedir, tmpdir } from 'os';
|
||||
import { homedir, tmpdir, userInfo } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
// t3mp3st injects its OWN provider keys (from .env) into the server process. If we let those leak into
|
||||
@@ -27,8 +27,47 @@ const PROVIDER_ENV_TO_STRIP = [
|
||||
'OPENROUTER_API_KEY',
|
||||
];
|
||||
|
||||
const HOME = homedir();
|
||||
const expand = (p: string): string => (p.startsWith('~') ? HOME + p.slice(1) : p);
|
||||
/**
|
||||
* The REAL user home where the agent CLIs keep their own auth artifacts
|
||||
* (~/.claude.json, ~/.codex/auth.json, ~/.hermes/.env, macOS keychain).
|
||||
*
|
||||
* DELIBERATELY separate from os.homedir()/$HOME: T3MP3ST may run with HOME redirected
|
||||
* (e.g. an isolated app-config dir), and os.homedir() returns that redirected path. Detecting
|
||||
* OR spawning the user's CLIs against a redirected HOME makes an installed-and-authed agent
|
||||
* look unavailable — the Settings checkboxes go dead and it reads like a UI bug. Resolution
|
||||
* order:
|
||||
* 1. T3MP3ST_AGENT_HOME — explicit override (a launcher that redirects HOME sets this).
|
||||
* 2. os.userInfo().homedir — the real home from the OS user DB (getpwuid), NOT affected by a
|
||||
* $HOME redirect, so this AUTO-recovers the correct home with no config.
|
||||
* 3. os.homedir() — last-resort fallback.
|
||||
* os.homedir()/$HOME is intentionally left untouched for app-config storage (src/config reads it).
|
||||
*/
|
||||
export function agentHome(): string {
|
||||
const override = (process.env.T3MP3ST_AGENT_HOME || '').trim();
|
||||
if (override) return override;
|
||||
try {
|
||||
const real = userInfo().homedir;
|
||||
if (real) return real;
|
||||
} catch { /* userInfo can throw in some sandboxes — fall through to homedir() */ }
|
||||
return homedir();
|
||||
}
|
||||
const expand = (p: string): string => (p.startsWith('~') ? agentHome() + p.slice(1) : p);
|
||||
|
||||
/**
|
||||
* Env for a spawned agent CLI. Two adjustments to our own env:
|
||||
* - strip the injected provider keys so the CLI falls back to its OWN native login (not t3mp3st's).
|
||||
* - point HOME (and USERPROFILE on Windows) at the agentHome() so the CLI finds that login even
|
||||
* when t3mp3st itself runs with HOME redirected for app-config storage. Same home the detector
|
||||
* used, so "detected as authed" and "actually authenticates when spawned" stay consistent.
|
||||
*/
|
||||
function childEnv(): NodeJS.ProcessEnv {
|
||||
const env: NodeJS.ProcessEnv = { ...process.env };
|
||||
for (const k of PROVIDER_ENV_TO_STRIP) delete env[k];
|
||||
const home = agentHome();
|
||||
env.HOME = home;
|
||||
if (process.platform === 'win32') env.USERPROFILE = home;
|
||||
return env;
|
||||
}
|
||||
|
||||
export type LocalAgentId = 'claude' | 'codex' | 'hermes';
|
||||
|
||||
@@ -199,9 +238,8 @@ export function runLocalAgent(
|
||||
const args = spec.oneShot(prompt, opts.model);
|
||||
const timeoutMs = opts.timeoutMs ?? 120000;
|
||||
const maxChars = opts.maxChars ?? 4000;
|
||||
// child env = ours minus the injected provider keys, so the CLI uses its OWN native login.
|
||||
const env: NodeJS.ProcessEnv = { ...process.env };
|
||||
for (const k of PROVIDER_ENV_TO_STRIP) delete env[k];
|
||||
// child env: provider keys stripped + HOME pinned to the real agent home (see childEnv).
|
||||
const env = childEnv();
|
||||
return new Promise((resolve) => {
|
||||
// stdin:'ignore' so the agent doesn't stall waiting on piped input (e.g. `claude -p`'s 3s stdin wait).
|
||||
const child = spawn(spec.bin, args, { env, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
@@ -243,8 +281,8 @@ export function pingLocalAgent(id: string, prompt?: string, timeoutMs?: number):
|
||||
export function localAgentChat(id: string, prompt: string, opts: { model?: string; timeoutMs?: number } = {}): Promise<string> {
|
||||
const spec = getSpec(id);
|
||||
if (!spec) return Promise.reject(new Error(`unknown local agent: ${id}`));
|
||||
const env: NodeJS.ProcessEnv = { ...process.env };
|
||||
for (const k of PROVIDER_ENV_TO_STRIP) delete env[k];
|
||||
// child env: provider keys stripped + HOME pinned to the real agent home (see childEnv).
|
||||
const env = childEnv();
|
||||
const model = opts.model && opts.model !== 'codex-default' && opts.model !== id ? opts.model : undefined;
|
||||
const timeoutMs = opts.timeoutMs ?? 240000;
|
||||
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
+3
-1
@@ -520,6 +520,7 @@ async function openSettings(): Promise<void> {
|
||||
console.log(chalk.cyan(' Provider:'), settings.defaultProvider);
|
||||
console.log(chalk.cyan(' Model:'), settings.defaultModel);
|
||||
console.log(chalk.cyan(' OpenRouter:'), hasApiKey('openrouter') ? chalk.green('configured') : chalk.red('not set'));
|
||||
console.log(chalk.cyan(' Venice:'), hasApiKey('venice') ? chalk.green('configured') : chalk.red('not set'));
|
||||
console.log(chalk.cyan(' Anthropic:'), hasApiKey('anthropic') ? chalk.green('configured') : chalk.red('not set'));
|
||||
console.log(chalk.cyan(' OpenAI:'), hasApiKey('openai') ? chalk.green('configured') : chalk.red('not set'));
|
||||
console.log('');
|
||||
@@ -574,7 +575,7 @@ async function openSettings(): Promise<void> {
|
||||
type: 'list',
|
||||
name: 'keyProvider',
|
||||
message: 'Which provider?',
|
||||
choices: ['openrouter', 'anthropic', 'openai'],
|
||||
choices: ['openrouter', 'venice', 'anthropic', 'openai'],
|
||||
},
|
||||
]);
|
||||
const { apiKey } = await inquirer.prompt([
|
||||
@@ -638,6 +639,7 @@ program
|
||||
[chalk.cyan('Default Provider'), settings.defaultProvider],
|
||||
[chalk.cyan('Default Model'), settings.defaultModel],
|
||||
[chalk.cyan('OpenRouter API Key'), hasApiKey('openrouter') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
|
||||
[chalk.cyan('Venice API Key'), hasApiKey('venice') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
|
||||
[chalk.cyan('Anthropic API Key'), hasApiKey('anthropic') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
|
||||
[chalk.cyan('OpenAI API Key'), hasApiKey('openai') ? chalk.green('✓ Configured') : chalk.red('✗ Not set')],
|
||||
[chalk.cyan('Config Path'), config.getConfigPath()],
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
+56
-10
@@ -19,6 +19,7 @@ export interface TempestSettings {
|
||||
// API Keys
|
||||
apiKeys: {
|
||||
openrouter?: string;
|
||||
venice?: string;
|
||||
anthropic?: string;
|
||||
openai?: string;
|
||||
};
|
||||
@@ -35,6 +36,12 @@ export interface TempestSettings {
|
||||
siteName?: string;
|
||||
};
|
||||
|
||||
// Venice AI — OpenAI-compatible, privacy-focused (same wire shape as OpenRouter)
|
||||
venice: {
|
||||
baseUrl: string;
|
||||
defaultModel: string;
|
||||
};
|
||||
|
||||
// Anthropic specific
|
||||
anthropic: {
|
||||
baseUrl: string;
|
||||
@@ -90,6 +97,11 @@ const DEFAULT_SETTINGS: TempestSettings = {
|
||||
siteName: 'T3MP3ST',
|
||||
},
|
||||
|
||||
venice: {
|
||||
baseUrl: 'https://api.venice.ai/api/v1',
|
||||
defaultModel: 'llama-3.3-70b',
|
||||
},
|
||||
|
||||
anthropic: {
|
||||
baseUrl: 'https://api.anthropic.com',
|
||||
defaultModel: 'claude-opus-4-6',
|
||||
@@ -140,6 +152,25 @@ export interface ModelInfo {
|
||||
}
|
||||
|
||||
export const AVAILABLE_MODELS: Record<LLMProvider, ModelInfo[]> = {
|
||||
venice: [
|
||||
{
|
||||
id: 'llama-3.3-70b',
|
||||
name: 'Llama 3.3 70B (Venice)',
|
||||
provider: 'Venice',
|
||||
contextWindow: 65536,
|
||||
maxOutput: 8192,
|
||||
capabilities: ['reasoning', 'code', 'analysis', 'uncensored', 'tools'],
|
||||
},
|
||||
{
|
||||
id: 'venice-uncensored',
|
||||
name: 'Venice Uncensored',
|
||||
provider: 'Venice',
|
||||
contextWindow: 32768,
|
||||
maxOutput: 8192,
|
||||
capabilities: ['reasoning', 'uncensored'],
|
||||
},
|
||||
],
|
||||
|
||||
openrouter: [
|
||||
// Anthropic (Feb 2026)
|
||||
{
|
||||
@@ -454,6 +485,7 @@ class ConfigManager {
|
||||
// Check environment variables for API keys
|
||||
const envKeys = {
|
||||
openrouter: process.env.OPENROUTER_API_KEY,
|
||||
venice: process.env.VENICE_API_KEY,
|
||||
anthropic: process.env.ANTHROPIC_API_KEY,
|
||||
openai: process.env.OPENAI_API_KEY,
|
||||
};
|
||||
@@ -463,7 +495,7 @@ class ConfigManager {
|
||||
|
||||
for (const [provider, envKey] of Object.entries(envKeys)) {
|
||||
if (envKey && !currentKeys[provider as keyof typeof currentKeys]) {
|
||||
this.setApiKey(provider as 'openrouter' | 'anthropic' | 'openai', envKey);
|
||||
this.setApiKey(provider as 'openrouter' | 'venice' | 'anthropic' | 'openai', envKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,7 +526,7 @@ class ConfigManager {
|
||||
/**
|
||||
* Set an API key for a provider
|
||||
*/
|
||||
setApiKey(provider: 'openrouter' | 'anthropic' | 'openai', key: string): void {
|
||||
setApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai', key: string): void {
|
||||
const apiKeys = this.config.get('apiKeys');
|
||||
apiKeys[provider] = key;
|
||||
this.config.set('apiKeys', apiKeys);
|
||||
@@ -503,10 +535,11 @@ class ConfigManager {
|
||||
/**
|
||||
* Get an API key for a provider
|
||||
*/
|
||||
getApiKey(provider: 'openrouter' | 'anthropic' | 'openai'): string | undefined {
|
||||
getApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai'): string | undefined {
|
||||
// First check environment variables (highest priority)
|
||||
const envVarMap = {
|
||||
openrouter: 'OPENROUTER_API_KEY',
|
||||
venice: 'VENICE_API_KEY',
|
||||
anthropic: 'ANTHROPIC_API_KEY',
|
||||
openai: 'OPENAI_API_KEY',
|
||||
};
|
||||
@@ -527,7 +560,7 @@ class ConfigManager {
|
||||
/**
|
||||
* Check if a provider has a valid API key configured
|
||||
*/
|
||||
hasApiKey(provider: 'openrouter' | 'anthropic' | 'openai'): boolean {
|
||||
hasApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai'): boolean {
|
||||
const key = this.getApiKey(provider);
|
||||
return !!key && key.length > 10;
|
||||
}
|
||||
@@ -535,7 +568,7 @@ class ConfigManager {
|
||||
/**
|
||||
* Remove an API key
|
||||
*/
|
||||
removeApiKey(provider: 'openrouter' | 'anthropic' | 'openai'): void {
|
||||
removeApiKey(provider: 'openrouter' | 'venice' | 'anthropic' | 'openai'): void {
|
||||
const apiKeys = this.config.get('apiKeys');
|
||||
delete apiKeys[provider];
|
||||
this.config.set('apiKeys', apiKeys);
|
||||
@@ -548,6 +581,7 @@ class ConfigManager {
|
||||
const providers: LLMProvider[] = [];
|
||||
|
||||
if (this.hasApiKey('openrouter')) providers.push('openrouter');
|
||||
if (this.hasApiKey('venice')) providers.push('venice');
|
||||
if (this.hasApiKey('anthropic')) providers.push('anthropic');
|
||||
if (this.hasApiKey('openai')) providers.push('openai');
|
||||
|
||||
@@ -576,6 +610,11 @@ class ConfigManager {
|
||||
baseUrl = this.config.get('openrouter').baseUrl;
|
||||
actualModel = model || this.config.get('openrouter').defaultModel;
|
||||
break;
|
||||
case 'venice':
|
||||
apiKey = this.getApiKey('venice');
|
||||
baseUrl = this.config.get('venice').baseUrl;
|
||||
actualModel = model || this.config.get('venice').defaultModel;
|
||||
break;
|
||||
case 'anthropic':
|
||||
apiKey = this.getApiKey('anthropic');
|
||||
baseUrl = this.config.get('anthropic').baseUrl;
|
||||
@@ -617,7 +656,7 @@ class ConfigManager {
|
||||
* model failure that the model can't self-recover from — a refusal, an empty 200,
|
||||
* or a hard error that survives same-model retries (rate-limit, 5xx, timeout, dead
|
||||
* key, missing model, context blowout) — escalates across the OTHER configured
|
||||
* providers in priority order (openrouter → anthropic → openai), each with its own
|
||||
* providers in priority order (openrouter → venice → anthropic → openai), each with its own
|
||||
* key/model. OFF by default (no surprise model-switching). On a refusal the real
|
||||
* authorization context is restated — honest escalation, no jailbreak prompts
|
||||
* (see LLMBackbone.chat).
|
||||
@@ -626,7 +665,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' | 'anthropic' | 'openai') => {
|
||||
const add = (p: 'openrouter' | 'venice' | 'anthropic' | 'openai') => {
|
||||
if (p === primary || !this.hasApiKey(p)) return;
|
||||
chain.push({
|
||||
provider: p,
|
||||
@@ -636,6 +675,7 @@ class ConfigManager {
|
||||
});
|
||||
};
|
||||
add('openrouter');
|
||||
add('venice');
|
||||
add('anthropic');
|
||||
add('openai');
|
||||
return chain;
|
||||
@@ -652,6 +692,9 @@ class ConfigManager {
|
||||
case 'openrouter':
|
||||
this.config.set('defaultModel', this.config.get('openrouter').defaultModel);
|
||||
break;
|
||||
case 'venice':
|
||||
this.config.set('defaultModel', this.config.get('venice').defaultModel);
|
||||
break;
|
||||
case 'anthropic':
|
||||
this.config.set('defaultModel', this.config.get('anthropic').defaultModel);
|
||||
break;
|
||||
@@ -672,6 +715,9 @@ class ConfigManager {
|
||||
case 'openrouter':
|
||||
this.config.set('openrouter', { ...this.config.get('openrouter'), defaultModel: model });
|
||||
break;
|
||||
case 'venice':
|
||||
this.config.set('venice', { ...this.config.get('venice'), defaultModel: model });
|
||||
break;
|
||||
case 'anthropic':
|
||||
this.config.set('anthropic', { ...this.config.get('anthropic'), defaultModel: model });
|
||||
break;
|
||||
@@ -749,8 +795,8 @@ OPENAI_API_KEY=
|
||||
export const config = new ConfigManager();
|
||||
|
||||
// Helper functions for quick access
|
||||
export const getApiKey = (provider: 'openrouter' | 'anthropic' | 'openai') => config.getApiKey(provider);
|
||||
export const setApiKey = (provider: 'openrouter' | 'anthropic' | 'openai', key: string) => config.setApiKey(provider, key);
|
||||
export const hasApiKey = (provider: 'openrouter' | 'anthropic' | 'openai') => config.hasApiKey(provider);
|
||||
export const getApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai') => config.getApiKey(provider);
|
||||
export const setApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai', key: string) => config.setApiKey(provider, key);
|
||||
export const hasApiKey = (provider: 'openrouter' | 'venice' | 'anthropic' | 'openai') => config.hasApiKey(provider);
|
||||
export const getLLMConfig = (provider?: LLMProvider, model?: string) => config.getLLMConfig(provider, model);
|
||||
export const getConfiguredProviders = () => config.getConfiguredProviders();
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
+36
-2
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* Multi-provider LLM integration supporting:
|
||||
* - OpenRouter (recommended - access to Claude, GPT-4, Llama, etc.)
|
||||
* - Venice (OpenAI-compatible, privacy-focused / uncensored models)
|
||||
* - Anthropic (direct Claude access)
|
||||
* - OpenAI (GPT models)
|
||||
* - Mock (for testing)
|
||||
@@ -123,7 +124,7 @@ interface OllamaResponse {
|
||||
|
||||
class OpenRouterAdapter implements LLMProviderAdapter {
|
||||
name = 'openrouter';
|
||||
private config: LLMConfig;
|
||||
protected config: LLMConfig;
|
||||
|
||||
constructor(config: LLMConfig) {
|
||||
this.config = config;
|
||||
@@ -337,6 +338,28 @@ class OpenRouterAdapter implements LLMProviderAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// VENICE ADAPTER
|
||||
// =============================================================================
|
||||
// Venice AI is OpenAI-compatible on the wire (Bearer auth, POST /chat/completions,
|
||||
// identical request/response + tool-calling + SSE stream shape), so it reuses the entire
|
||||
// OpenRouter adapter and only differs in its default base URL (set via the `venice` config
|
||||
// block), its name, and the key-required error message. The optional HTTP-Referer/X-Title
|
||||
// headers the parent sends are harmless (Venice ignores unknown headers).
|
||||
class VeniceAdapter extends OpenRouterAdapter {
|
||||
name = 'venice';
|
||||
|
||||
validateConfig(): { valid: boolean; error?: string } {
|
||||
if (!this.config.apiKey) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Venice API key is required. Get one at https://venice.ai/settings/api',
|
||||
};
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ANTHROPIC ADAPTER
|
||||
// =============================================================================
|
||||
@@ -996,6 +1019,8 @@ export class LLMBackbone extends EventEmitter<LLMEvents> {
|
||||
switch (config.provider) {
|
||||
case 'openrouter':
|
||||
return new OpenRouterAdapter(config);
|
||||
case 'venice':
|
||||
return new VeniceAdapter(config);
|
||||
case 'anthropic':
|
||||
return new AnthropicAdapter(config);
|
||||
case 'openai':
|
||||
@@ -1262,6 +1287,12 @@ export function createOpenRouterBackbone(apiKey?: string, model?: string): LLMBa
|
||||
return new LLMBackbone(llmConfig);
|
||||
}
|
||||
|
||||
export function createVeniceBackbone(apiKey?: string, model?: string): LLMBackbone {
|
||||
const llmConfig = config.getLLMConfig('venice', model);
|
||||
if (apiKey) llmConfig.apiKey = apiKey;
|
||||
return new LLMBackbone(llmConfig);
|
||||
}
|
||||
|
||||
export function createOpenAIBackbone(apiKey?: string, model?: string): LLMBackbone {
|
||||
const llmConfig = config.getLLMConfig('openai', model);
|
||||
if (apiKey) llmConfig.apiKey = apiKey;
|
||||
@@ -1291,12 +1322,15 @@ export function createLocalBackbone(model?: string, baseUrl?: string): LLMBackbo
|
||||
* Create the best available backbone based on configured API keys
|
||||
*/
|
||||
export function createBestAvailableBackbone(): LLMBackbone {
|
||||
// Priority: OpenRouter > Anthropic > OpenAI > Local > Mock
|
||||
// Priority: OpenRouter > Venice > Anthropic > OpenAI > Local > Mock
|
||||
const providers = config.getConfiguredProviders();
|
||||
|
||||
if (providers.includes('openrouter')) {
|
||||
return createOpenRouterBackbone();
|
||||
}
|
||||
if (providers.includes('venice')) {
|
||||
return createVeniceBackbone();
|
||||
}
|
||||
if (providers.includes('anthropic')) {
|
||||
return createAnthropicBackbone();
|
||||
}
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
+56
@@ -101,6 +101,53 @@ async function setupOpenRouterKey(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
async function setupVeniceKey(): Promise<boolean> {
|
||||
console.log('');
|
||||
showInfo('Venice AI is an OpenAI-compatible, privacy-focused provider (uncensored models).');
|
||||
showInfo('Get your API key at: ' + chalk.underline('https://venice.ai/settings/api'));
|
||||
console.log('');
|
||||
|
||||
const { apiKey } = await inquirer.prompt([
|
||||
{
|
||||
type: 'password',
|
||||
name: 'apiKey',
|
||||
message: 'Enter your Venice API key:',
|
||||
mask: '*',
|
||||
validate: (input: string) => {
|
||||
if (!input || input.length < 10) {
|
||||
return 'Please enter a valid API key';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Test the API key
|
||||
const spinner = ora('Testing API key...').start();
|
||||
|
||||
try {
|
||||
const llm = new LLMBackbone({
|
||||
provider: 'venice',
|
||||
model: 'llama-3.3-70b',
|
||||
apiKey,
|
||||
maxTokens: 10,
|
||||
temperature: 0,
|
||||
});
|
||||
|
||||
await llm.prompt('Hello', undefined, { maxTokens: 10 });
|
||||
spinner.succeed('API key is valid!');
|
||||
|
||||
setApiKey('venice', apiKey);
|
||||
showSuccess('Venice API key saved successfully!');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
spinner.fail('API key validation failed');
|
||||
showError(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function setupAnthropicKey(): Promise<boolean> {
|
||||
console.log('');
|
||||
showInfo('Anthropic provides direct access to Claude models.');
|
||||
@@ -325,6 +372,10 @@ async function setupApiKeys(): Promise<void> {
|
||||
value: 'openrouter',
|
||||
checked: !hasApiKey('openrouter'),
|
||||
},
|
||||
{
|
||||
name: `Venice ${hasApiKey('venice') ? chalk.green('(configured)') : ''}`,
|
||||
value: 'venice',
|
||||
},
|
||||
{
|
||||
name: `Anthropic ${hasApiKey('anthropic') ? chalk.green('(configured)') : ''}`,
|
||||
value: 'anthropic',
|
||||
@@ -339,6 +390,9 @@ async function setupApiKeys(): Promise<void> {
|
||||
|
||||
for (const provider of providers) {
|
||||
switch (provider) {
|
||||
case 'venice':
|
||||
await setupVeniceKey();
|
||||
break;
|
||||
case 'openrouter':
|
||||
await setupOpenRouterKey();
|
||||
break;
|
||||
@@ -356,6 +410,7 @@ async function setupProvider(): Promise<void> {
|
||||
const configuredProviders = [];
|
||||
|
||||
if (hasApiKey('openrouter')) configuredProviders.push({ name: 'OpenRouter', value: 'openrouter' });
|
||||
if (hasApiKey('venice')) configuredProviders.push({ name: 'Venice', value: 'venice' });
|
||||
if (hasApiKey('anthropic')) configuredProviders.push({ name: 'Anthropic', value: 'anthropic' });
|
||||
if (hasApiKey('openai')) configuredProviders.push({ name: 'OpenAI', value: 'openai' });
|
||||
|
||||
@@ -396,6 +451,7 @@ function viewConfiguration(): void {
|
||||
console.log('');
|
||||
console.log(chalk.cyan(' API Keys:'));
|
||||
console.log(' OpenRouter: ' + (hasApiKey('openrouter') ? chalk.green('configured') : chalk.red('not set')));
|
||||
console.log(' Venice: ' + (hasApiKey('venice') ? chalk.green('configured') : chalk.red('not set')));
|
||||
console.log(' Anthropic: ' + (hasApiKey('anthropic') ? chalk.green('configured') : chalk.red('not set')));
|
||||
console.log(' OpenAI: ' + (hasApiKey('openai') ? chalk.green('configured') : chalk.red('not set')));
|
||||
console.log('');
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
+1
-1
@@ -6,7 +6,7 @@
|
||||
// LLM CONFIGURATION
|
||||
// =============================================================================
|
||||
|
||||
export type LLMProvider = 'openrouter' | 'anthropic' | 'openai' | 'codex' | 'mock' | 'local' | 'local-agent';
|
||||
export type LLMProvider = 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'codex' | 'mock' | 'local' | 'local-agent';
|
||||
|
||||
export interface LLMConfig {
|
||||
provider: LLMProvider;
|
||||
|
||||
Reference in new issue
Block a user