diff --git a/.env.example b/.env.example index 9ea49a7..34a4f60 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,8 @@ DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Hugging Face — open models via the OpenAI-compatible Inference Providers router. # HF_TOKEN is canonical; HUGGINGFACE_API_KEY / HUGGINGFACE_TOKEN also work. HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +# Novita AI — OpenAI-compatible API serving open-source and frontier models. +NOVITA_API_KEY=sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx GROQ_API_KEY= TOGETHER_API_KEY= REPLICATE_API_TOKEN= diff --git a/README.md b/README.md index 88da7c5..39f172f 100755 --- a/README.md +++ b/README.md @@ -75,8 +75,16 @@ Prefer to bring a key? Set one and skip the connect step: ```bash export OPENROUTER_API_KEY=... # or VENICE_API_KEY / ANTHROPIC_API_KEY / OPENAI_API_KEY export XAI_API_KEY=... # Grok Build (grok-build-0.1) — xAI's coding model, native tool-calling +export NOVITA_API_KEY=... # Novita AI's hosted OpenAI-compatible API ``` +Novita AI is an opt-in third-party hosted provider. Requests send the selected +model id, prompts/context, generated output, and request metadata to Novita; +your API key authenticates those requests. Review Novita's current +[privacy policy](https://novita.ai/legal/privacy-policy) and +[terms](https://novita.ai/legal/terms-of-service) before using sensitive target +data. T3MP3ST does not claim independent assurance for sensitive workloads. + Slow local agents can be given more room with `T3MP3ST_LOCAL_AGENT_TIMEOUT_MS` for each CLI call, `T3MP3ST_TASK_TIMEOUT_MS` for mission tasks, and `T3MP3ST_GENERAL_TIMEOUT_MS` for planning requests. Values are milliseconds. diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index db3eee0..695f0a0 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -44,10 +44,17 @@ export VENICE_API_KEY=... export ANTHROPIC_API_KEY=... export OPENAI_API_KEY=... export DEEPSEEK_API_KEY=... +export NOVITA_API_KEY=... # third-party hosted OpenAI-compatible API export LITELLM_BASE_URL=http://localhost:4000/v1 export LITELLM_API_KEY=... # only if your LiteLLM proxy requires one ``` +Hosted providers receive the model id, prompts/context, generated output, and +request metadata needed to serve the request. For Novita, review the current +[privacy policy](https://novita.ai/legal/privacy-policy) and +[terms](https://novita.ai/legal/terms-of-service) before sending sensitive +target data; T3MP3ST does not independently certify it for sensitive workloads. + Local/offline example with Ollama: ```bash diff --git a/docs/index.html b/docs/index.html index 06a9b9e..9043238 100755 --- a/docs/index.html +++ b/docs/index.html @@ -5621,6 +5621,7 @@ + @@ -9244,6 +9245,7 @@ o.id === 'coordinator' ? `1. MISSION_STATUS: Progress assessment venice: 'https://api.venice.ai/api/v1', openai: 'https://api.openai.com/v1', nanogpt: 'https://nano-gpt.com/api/v1', + novita: 'https://api.novita.ai/openai/v1', anthropic: 'https://api.anthropic.com', }; function uacProvider() { return document.getElementById('uacProvider')?.value || 'openrouter'; } diff --git a/docsite/t3mp3st-docs/content/GETTING_STARTED.md b/docsite/t3mp3st-docs/content/GETTING_STARTED.md index 3910ab2..83bb49d 100644 --- a/docsite/t3mp3st-docs/content/GETTING_STARTED.md +++ b/docsite/t3mp3st-docs/content/GETTING_STARTED.md @@ -55,10 +55,17 @@ export VENICE_API_KEY=... export ANTHROPIC_API_KEY=... export OPENAI_API_KEY=... export DEEPSEEK_API_KEY=... +export NOVITA_API_KEY=... # third-party hosted OpenAI-compatible API export LITELLM_BASE_URL=http://localhost:4000/v1 export LITELLM_API_KEY=... # only if your LiteLLM proxy requires one ``` +Hosted providers receive the model id, prompts/context, generated output, and +request metadata needed to serve the request. For Novita, review the current +[privacy policy](https://novita.ai/legal/privacy-policy) and +[terms](https://novita.ai/legal/terms-of-service) before sending sensitive +target data; T3MP3ST does not independently certify it for sensitive workloads. + Local/offline example with Ollama: ```bash diff --git a/src/__tests__/api-key-env-static.test.ts b/src/__tests__/api-key-env-static.test.ts index 7004777..a36b99f 100644 --- a/src/__tests__/api-key-env-static.test.ts +++ b/src/__tests__/api-key-env-static.test.ts @@ -61,7 +61,7 @@ describe('API key environment handling hardening', () => { it('exportConfig redacts every supported provider key slot', () => { const block = exportConfigBlock(); - for (const provider of ['openrouter', 'venice', 'anthropic', 'openai', 'xai', 'gemini', 'deepseek', 'huggingface', 'nanogpt', 'litellm']) { + for (const provider of ['openrouter', 'venice', 'anthropic', 'openai', 'xai', 'gemini', 'deepseek', 'huggingface', 'nanogpt', 'novita', 'litellm']) { expect(block).toContain(`${provider}: settings.apiKeys.${provider} ? '***REDACTED***' : undefined`); } }); diff --git a/src/__tests__/novita-provider.test.ts b/src/__tests__/novita-provider.test.ts new file mode 100644 index 0000000..e177bb3 --- /dev/null +++ b/src/__tests__/novita-provider.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, it, afterEach, vi } from 'vitest'; +import { config, AVAILABLE_MODELS } from '../config/index.js'; +import { createNovitaBackbone } from '../llm/index.js'; + +const KEY = 'sk_novita-test-key-0123456789'; + +describe('Novita AI provider wiring', () => { + const originalFetch = global.fetch; + + afterEach(() => { + delete process.env.NOVITA_API_KEY; + delete process.env.TEMPEST_MODEL_FALLBACK; + global.fetch = originalFetch; + vi.restoreAllMocks(); + }); + + it('resolves the Novita base URL, default model, and key from NOVITA_API_KEY', () => { + process.env.NOVITA_API_KEY = KEY; + const cfg = config.getLLMConfig('novita'); + expect(cfg.provider).toBe('novita'); + expect(cfg.baseUrl).toBe('https://api.novita.ai/openai/v1'); + expect(cfg.model).toBe('zai-org/glm-5.2'); + expect(cfg.apiKey).toBe(KEY); + expect(`${cfg.baseUrl}/chat/completions`).toBe( + 'https://api.novita.ai/openai/v1/chat/completions', + ); + }); + + it('routes through the OpenAI-compatible backbone and validates with a key', () => { + process.env.NOVITA_API_KEY = KEY; + const bb = createNovitaBackbone(); + expect(bb.getProvider()).toBe('novita'); + expect(bb.validateConfig().valid).toBe(true); + }); + + it('requires the provider-specific key instead of reporting an OpenAI credential error', () => { + expect(createNovitaBackbone().validateConfig()).toEqual({ + valid: false, + error: expect.stringContaining('NOVITA_API_KEY'), + }); + }); + + it('sends chat to the Novita endpoint with Bearer auth and the configured model', async () => { + const fetchSpy = vi.fn(async (_url: string | URL, _init?: RequestInit) => ({ + ok: true, + json: async () => ({ + model: 'zai-org/glm-5.2', + choices: [{ message: { content: 'ok' }, finish_reason: 'stop' }], + }), + })); + global.fetch = fetchSpy as unknown as typeof fetch; + + const response = await createNovitaBackbone(KEY).chat([{ role: 'user', content: 'hello' }]); + expect(response.content).toBe('ok'); + const [url, init] = fetchSpy.mock.calls[0]; + expect(url).toBe('https://api.novita.ai/openai/v1/chat/completions'); + expect((init as RequestInit).headers).toMatchObject({ + Authorization: `Bearer ${KEY}`, + 'Content-Type': 'application/json', + }); + expect(JSON.parse(String((init as RequestInit).body))).toMatchObject({ + model: 'zai-org/glm-5.2', + messages: [{ role: 'user', content: 'hello' }], + }); + }); + + it.each([401, 429])('surfaces HTTP %i without silently changing providers', async (status) => { + const fetchSpy = vi.fn(async (_url: string | URL, _init?: RequestInit) => ({ + ok: false, + status, + headers: { get: () => null }, + text: async () => 'provider error', + })); + global.fetch = fetchSpy as unknown as typeof fetch; + + await expect(createNovitaBackbone(KEY).chat([{ role: 'user', content: 'hello' }])) + .rejects.toMatchObject({ status }); + expect(fetchSpy).toHaveBeenCalledTimes(status === 401 ? 1 : 3); + for (const [url] of fetchSpy.mock.calls) { + expect(url).toBe('https://api.novita.ai/openai/v1/chat/completions'); + } + }, 10_000); + + it('keeps cross-provider fallback disabled unless the operator opts in', () => { + process.env.NOVITA_API_KEY = KEY; + expect(config.getLLMConfig('novita').fallbackChain).toEqual([]); + }); + + it('publishes a non-empty Novita model catalog and configured provider state', () => { + process.env.NOVITA_API_KEY = KEY; + expect(AVAILABLE_MODELS.novita?.length ?? 0).toBeGreaterThan(0); + expect(config.getConfiguredProviders()).toContain('novita'); + }); +}); diff --git a/src/__tests__/provider-models.test.ts b/src/__tests__/provider-models.test.ts index 04e9d0f..f667bed 100644 --- a/src/__tests__/provider-models.test.ts +++ b/src/__tests__/provider-models.test.ts @@ -58,6 +58,21 @@ describe('listProviderModels — OpenAI-compatible providers', () => { expect((init as { headers: Record }).headers.Authorization) .toBe('Bearer sk-nano-test-key'); }); + + it('uses Novita canonical /openai/v1/models with Bearer authentication', async () => { + const f = fakeFetch({ data: [{ id: 'zai-org/glm-5.2' }] }); + const models = await listProviderModels('novita', { + baseUrl: 'https://api.novita.ai/openai/v1', + apiKey: 'sk_novita-test-key', + fetchImpl: f, + }); + + expect(models.map((m) => m.id)).toEqual(['zai-org/glm-5.2']); + const [url, init] = f.mock.calls[0]; + expect(url).toBe('https://api.novita.ai/openai/v1/models'); + expect((init as { headers: Record }).headers.Authorization) + .toBe('Bearer sk_novita-test-key'); + }); }); describe('listProviderModels — Anthropic wire', () => { diff --git a/src/config/index.ts b/src/config/index.ts index a10deed..22e5306 100755 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -11,7 +11,7 @@ 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' | 'deepseek' | 'huggingface' | 'nanogpt' | 'local'; +type ApiKeyProvider = 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'gemini' | 'litellm' | 'deepseek' | 'huggingface' | 'nanogpt' | 'novita' | 'local'; // ============================================================================= // CONFIGURATION SCHEMA @@ -30,6 +30,7 @@ export interface TempestSettings { huggingface?: string; nanogpt?: string; litellm?: string; + novita?: string; local?: string; }; @@ -99,6 +100,12 @@ export interface TempestSettings { defaultModel: string; }; + // Novita AI — OpenAI-compatible API serving open-source and frontier models + novita: { + baseUrl: string; + defaultModel: string; + }; + // Codex CLI/account subscription backend codex: { command: string; @@ -201,6 +208,13 @@ const DEFAULT_SETTINGS: TempestSettings = { defaultModel: 'minimax/minimax-m2.7', }, + // Novita AI's OpenAI-compatible surface. The OpenAIAdapter appends + // /chat/completions and provider-models appends /models. + novita: { + baseUrl: 'https://api.novita.ai/openai/v1', + defaultModel: 'zai-org/glm-5.2', + }, + codex: { command: 'codex', defaultModel: 'codex-default', @@ -626,6 +640,25 @@ export const AVAILABLE_MODELS: Record = { capabilities: ['reasoning', 'code', 'analysis', 'tools'], }, ], + // Model ids verified live against api.novita.ai/openai/v1/models (2026-08-15). + novita: [ + { + id: 'zai-org/glm-5.2', + name: 'GLM-5.2 (Novita)', + provider: 'Novita', + contextWindow: 1048576, + maxOutput: 131072, + capabilities: ['reasoning', 'code', 'analysis', 'complex-tasks', 'agents', 'tools'], + }, + { + id: 'deepseek/deepseek-v4-pro-0813', + name: 'DeepSeek V4 Pro 0813 (Novita)', + provider: 'Novita', + contextWindow: 1048576, + maxOutput: 393216, + capabilities: ['reasoning', 'code', 'analysis', 'agents', 'tools'], + }, + ], local: [ { id: 'local-model', @@ -832,6 +865,7 @@ class ConfigManager { deepseek: 'DEEPSEEK_API_KEY', huggingface: 'HF_TOKEN', nanogpt: 'NANOGPT_API_KEY', + novita: 'NOVITA_API_KEY', litellm: 'LITELLM_API_KEY', }; @@ -901,6 +935,7 @@ class ConfigManager { if (this.hasApiKey('deepseek')) providers.push('deepseek'); if (this.hasApiKey('huggingface')) providers.push('huggingface'); if (this.hasApiKey('nanogpt')) providers.push('nanogpt'); + if (this.hasApiKey('novita')) providers.push('novita'); // Codex uses the local Codex CLI/account auth instead of API-key storage. providers.push('codex'); @@ -986,6 +1021,12 @@ class ConfigManager { baseUrl = this.config.get('nanogpt').baseUrl; actualModel = model || this.config.get('nanogpt').defaultModel; break; + case 'novita': + // Novita AI's OpenAI-compatible surface. + apiKey = this.getApiKey('novita'); + baseUrl = this.config.get('novita').baseUrl; + actualModel = model || this.config.get('novita').defaultModel; + break; case 'codex': actualModel = model || this.config.get('codex').defaultModel; break; @@ -1052,7 +1093,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' | 'gemini' | 'litellm' | 'deepseek' | 'huggingface' | 'nanogpt') => { + const add = (p: 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'gemini' | 'litellm' | 'deepseek' | 'huggingface' | 'nanogpt' | 'novita') => { if (p === primary) return; if (p === 'litellm' ? !this.hasLiteLLMProxy() : !this.hasApiKey(p)) return; chain.push({ @@ -1072,6 +1113,7 @@ class ConfigManager { add('deepseek'); add('huggingface'); add('nanogpt'); + add('novita'); return chain; } @@ -1199,6 +1241,7 @@ class ConfigManager { deepseek: settings.apiKeys.deepseek ? '***REDACTED***' : undefined, huggingface: settings.apiKeys.huggingface ? '***REDACTED***' : undefined, nanogpt: settings.apiKeys.nanogpt ? '***REDACTED***' : undefined, + novita: settings.apiKeys.novita ? '***REDACTED***' : undefined, litellm: settings.apiKeys.litellm ? '***REDACTED***' : undefined, }, }; @@ -1256,6 +1299,10 @@ HF_TOKEN= # Docs and key management: https://docs.nano-gpt.com/authentication NANOGPT_API_KEY= +# Novita AI API Key (OpenAI-compatible API serving open-source and frontier models) +# Get your key at: https://novita.ai/settings/key-management +NOVITA_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, diff --git a/src/config/provider-models.ts b/src/config/provider-models.ts index 51145e9..0d94597 100644 --- a/src/config/provider-models.ts +++ b/src/config/provider-models.ts @@ -23,7 +23,7 @@ const DEFAULT_MODEL_LIST_TIMEOUT_MS = 15_000; // Pseudo-providers with no remote model list (CLI-driven or in-process). const NO_REMOTE_LIST = new Set(['codex', 'mock', 'local-agent']); -const OPENAI_COMPATIBLE_REMOTE_LIST = new Set(['openai', 'venice', 'xai', 'gemini', 'nanogpt', 'local']); +const OPENAI_COMPATIBLE_REMOTE_LIST = new Set(['openai', 'venice', 'xai', 'gemini', 'nanogpt', 'novita', 'local']); const DIRECT_REMOTE_LIST = new Set(['anthropic', 'openrouter']); const stripTrailingSlash = (u: string): string => u.replace(/\/+$/, ''); diff --git a/src/llm/index.ts b/src/llm/index.ts index 001601c..ccb380b 100755 --- a/src/llm/index.ts +++ b/src/llm/index.ts @@ -8,6 +8,7 @@ * - Anthropic (direct Claude access) * - OpenAI (GPT models) * - HuggingFace (open models via the OpenAI-compatible Inference Providers router) + * - Novita AI (OpenAI-compatible API serving open-source and frontier models) * - Mock (for testing) * - Local (Ollama, etc.) */ @@ -654,7 +655,11 @@ class OpenAIAdapter implements LLMProviderAdapter { if (!response.ok) { const errorText = await response.text(); - throw new Error(`OpenAI API error: ${response.status} - ${errorText}`); + const retryAfter = response.headers?.get?.('retry-after'); + const retryAfterMs = retryAfter && /^\d+$/.test(retryAfter) + ? Number(retryAfter) * 1000 + : undefined; + throw new LLMApiError(`OpenAI-compatible API error: ${response.status} - ${errorText}`, response.status, retryAfterMs); } const data = await response.json() as OpenRouterResponse; @@ -701,6 +706,20 @@ class NanoGPTAdapter extends OpenAIAdapter { } } +class NovitaAdapter extends OpenAIAdapter { + name = 'novita'; + + validateConfig(): { valid: boolean; error?: string } { + if (!this.config.apiKey) { + return { + valid: false, + error: 'Novita AI API key is required. Set NOVITA_API_KEY. Docs: https://novita.ai/settings/key-management', + }; + } + return { valid: true }; + } +} + // ============================================================================= // MOCK ADAPTER (for testing) // ============================================================================= @@ -1547,6 +1566,8 @@ export class LLMBackbone extends EventEmitter { return new OpenAIAdapter(config); // HF Inference Providers router is OpenAI-compatible (baseUrl ends in /v1) case 'nanogpt': return new NanoGPTAdapter(config); + case 'novita': + return new NovitaAdapter(config); // Novita AI's native API is OpenAI-compatible case 'codex': return new CodexAdapter(config); case 'mock': @@ -1853,6 +1874,12 @@ export function createNanoGPTBackbone(apiKey?: string, model?: string): LLMBackb return new LLMBackbone(llmConfig); } +export function createNovitaBackbone(apiKey?: string, model?: string): LLMBackbone { + const llmConfig = config.getLLMConfig('novita', model); + if (apiKey) llmConfig.apiKey = apiKey; + 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; @@ -1883,7 +1910,7 @@ export function createLocalBackbone(model?: string, baseUrl?: string): LLMBackbo * Create the best available backbone based on configured API keys */ export function createBestAvailableBackbone(): LLMBackbone { - // Priority: OpenRouter > Venice > LiteLLM > Anthropic > OpenAI > DeepSeek > HuggingFace > NanoGPT > Local > Mock + // Priority: OpenRouter > Venice > LiteLLM > Anthropic > OpenAI > DeepSeek > HuggingFace > NanoGPT > Novita > Local > Mock const providers = config.getConfiguredProviders(); if (providers.includes('openrouter')) { @@ -1910,6 +1937,9 @@ export function createBestAvailableBackbone(): LLMBackbone { if (providers.includes('nanogpt')) { return createNanoGPTBackbone(); } + if (providers.includes('novita')) { + return createNovitaBackbone(); + } // Default to mock if no API keys configured console.warn('No API keys configured. Using mock provider.'); diff --git a/src/setup.ts b/src/setup.ts index b54f164..c3eaeac 100755 --- a/src/setup.ts +++ b/src/setup.ts @@ -335,6 +335,53 @@ async function setupNanoGPTKey(): Promise { } } +async function setupNovitaKey(): Promise { + console.log(''); + showInfo('Novita AI provides a direct OpenAI-compatible API serving open-source and frontier models.'); + showInfo('Get your API key at: ' + chalk.underline('https://novita.ai/settings/key-management')); + console.log(''); + + const { apiKey } = await inquirer.prompt([ + { + type: 'password', + name: 'apiKey', + message: 'Enter your Novita AI API key:', + mask: '*', + validate: (input: string) => { + if (!input || input.length < 10) { + return 'Please enter a valid API key'; + } + return true; + }, + }, + ]); + + const spinner = ora('Testing API key...').start(); + + try { + const llm = new LLMBackbone({ + provider: 'novita', + model: 'zai-org/glm-5.2', + baseUrl: 'https://api.novita.ai/openai/v1', + apiKey, + maxTokens: 10, + temperature: 0, + }); + + await llm.prompt('Hello', undefined, { maxTokens: 10 }); + spinner.succeed('API key is valid!'); + + setApiKey('novita', apiKey); + showSuccess('Novita AI 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 setupOpenAIKey(): Promise { console.log(''); showInfo('OpenAI provides access to GPT models.'); @@ -627,6 +674,10 @@ async function setupApiKeys(): Promise { name: `NanoGPT ${hasApiKey('nanogpt') ? chalk.green('(configured)') : chalk.gray('(direct OpenAI-compatible API)')}`, value: 'nanogpt', }, + { + name: `Novita AI ${hasApiKey('novita') ? chalk.green('(configured)') : chalk.gray('(direct OpenAI-compatible API)')}`, + value: 'novita', + }, { name: `LiteLLM Proxy ${config.hasLiteLLMProxy() ? chalk.green('(configured)') : chalk.gray('(100+ providers via gateway)')}`, value: 'litellm', @@ -658,6 +709,9 @@ async function setupApiKeys(): Promise { case 'nanogpt': await setupNanoGPTKey(); break; + case 'novita': + await setupNovitaKey(); + break; case 'litellm': await setupLiteLLMProxy(); break; @@ -676,6 +730,7 @@ async function setupProvider(): Promise { if (hasApiKey('deepseek')) configuredProviders.push({ name: 'DeepSeek', value: 'deepseek' }); if (hasApiKey('huggingface')) configuredProviders.push({ name: 'HuggingFace', value: 'huggingface' }); if (hasApiKey('nanogpt')) configuredProviders.push({ name: 'NanoGPT', value: 'nanogpt' }); + if (hasApiKey('novita')) configuredProviders.push({ name: 'Novita AI', value: 'novita' }); if (config.hasLiteLLMProxy()) configuredProviders.push({ name: 'LiteLLM Proxy', value: 'litellm' }); const { provider } = await inquirer.prompt([ diff --git a/src/types/index.ts b/src/types/index.ts index 0f0dd8e..e04de32 100755 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,7 +6,7 @@ // LLM CONFIGURATION // ============================================================================= -export type LLMProvider = 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'gemini' | 'litellm' | 'deepseek' | 'huggingface' | 'nanogpt' | 'codex' | 'mock' | 'local' | 'local-agent'; +export type LLMProvider = 'openrouter' | 'venice' | 'anthropic' | 'openai' | 'xai' | 'gemini' | 'litellm' | 'deepseek' | 'huggingface' | 'nanogpt' | 'novita' | 'codex' | 'mock' | 'local' | 'local-agent'; export interface LLMConfig { provider: LLMProvider;