feat(llm): add native Novita AI provider via OpenAI-compatible endpoint (#155)
* feat(llm): add native Novita AI provider via OpenAI-compatible endpoint Wire Novita AI (https://api.novita.ai/openai/v1) as a first-class LLM provider, following the same pattern used for DeepSeek/HuggingFace/NanoGPT: dispatched through the existing OpenAIAdapter, resolved via NOVITA_API_KEY, with a default model, model catalog entries, config/setup wiring, and regression tests. * fixup(llm): use deepseek/deepseek-v4-flash-0731 as the second Novita model Swap the curated Novita model catalog's second entry from qwen/qwen3-coder-30b-a3b-instruct to deepseek/deepseek-v4-flash-0731 per requested model selection. Verified live against api.novita.ai/openai/v1/models. * fixup(llm): replace superseded DeepSeek Flash id with current Pro flagship QA flagged that deepseek/deepseek-v4-flash-0731 has been superseded in the live Novita catalog; deepseek/deepseek-v4-pro-0813 is now the current serving DeepSeek flagship. Swap the second Novita model catalog entry accordingly. --------- Co-authored-by: jax-novita <jax-novita@users.noreply.github.com> Co-authored-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
This commit is contained in:
13 files changed
+274
-7
No files matched your search
@@ -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`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -58,6 +58,21 @@ describe('listProviderModels — OpenAI-compatible providers', () => {
|
||||
expect((init as { headers: Record<string, string> }).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<string, string> }).headers.Authorization)
|
||||
.toBe('Bearer sk_novita-test-key');
|
||||
});
|
||||
});
|
||||
|
||||
describe('listProviderModels — Anthropic wire', () => {
|
||||
|
||||
+49
-2
@@ -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<LLMProvider, ModelInfo[]> = {
|
||||
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,
|
||||
|
||||
@@ -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(/\/+$/, '');
|
||||
|
||||
+32
-2
@@ -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<LLMEvents> {
|
||||
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.');
|
||||
|
||||
@@ -335,6 +335,53 @@ async function setupNanoGPTKey(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
async function setupNovitaKey(): Promise<boolean> {
|
||||
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<boolean> {
|
||||
console.log('');
|
||||
showInfo('OpenAI provides access to GPT models.');
|
||||
@@ -627,6 +674,10 @@ async function setupApiKeys(): Promise<void> {
|
||||
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<void> {
|
||||
case 'nanogpt':
|
||||
await setupNanoGPTKey();
|
||||
break;
|
||||
case 'novita':
|
||||
await setupNovitaKey();
|
||||
break;
|
||||
case 'litellm':
|
||||
await setupLiteLLMProxy();
|
||||
break;
|
||||
@@ -676,6 +730,7 @@ async function setupProvider(): Promise<void> {
|
||||
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([
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user