diff --git a/npm/packages/ruvbot/.env.example b/npm/packages/ruvbot/.env.example index a80234a15..bc3ca8067 100644 --- a/npm/packages/ruvbot/.env.example +++ b/npm/packages/ruvbot/.env.example @@ -30,6 +30,10 @@ NODE_ENV=development # Anthropic Claude (RECOMMENDED) ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxx +# Novita AI (OpenAI-compatible endpoint) +NOVITA_API_KEY=nv-xxxxxxxxxxxxxxxxxxxxx +NOVITA_MODEL=deepseek/deepseek-v3.2 + # OpenRouter (alternative - access to multiple models) OPENROUTER_API_KEY=sk-or-xxxxxxxxxxxxxxxxxxxxx OPENROUTER_DEFAULT_MODEL=anthropic/claude-3.5-sonnet diff --git a/npm/packages/ruvbot/README.md b/npm/packages/ruvbot/README.md index 18120e40e..2e52c92a8 100644 --- a/npm/packages/ruvbot/README.md +++ b/npm/packages/ruvbot/README.md @@ -223,6 +223,10 @@ export OPENROUTER_API_KEY=sk-or-xxx # Option 2: Anthropic Direct export ANTHROPIC_API_KEY=sk-ant-xxx +# Option 3: Novita (OpenAI-compatible) +export NOVITA_API_KEY=nv-xxx +export NOVITA_MODEL=deepseek/deepseek-v3.2 + # Slack Integration (optional) export SLACK_BOT_TOKEN=xoxb-xxx export SLACK_SIGNING_SECRET=xxx @@ -1364,7 +1368,7 @@ All 52 Clawdbot skills are compatible with RuvBot. Simply copy your `skills/` di ``` [RuvBot] LLM not configured. Received: "..." ``` -Solution: Set `OPENROUTER_API_KEY` or `ANTHROPIC_API_KEY` environment variable. +Solution: Set `NOVITA_API_KEY`, `OPENROUTER_API_KEY`, or `ANTHROPIC_API_KEY` environment variable. **Agent not found** ``` diff --git a/npm/packages/ruvbot/package.json b/npm/packages/ruvbot/package.json index 17efeeb7c..c11a6da4c 100644 --- a/npm/packages/ruvbot/package.json +++ b/npm/packages/ruvbot/package.json @@ -94,6 +94,7 @@ }, "dependencies": { "@anthropic-ai/sdk": "^0.20.0", + "openai": "^5.20.3", "aidefence": "^2.1.1", "commander": "^12.0.0", "chalk": "^5.3.0", diff --git a/npm/packages/ruvbot/src/RuvBot.js b/npm/packages/ruvbot/src/RuvBot.js index 58df2dfda..fa1914c90 100644 --- a/npm/packages/ruvbot/src/RuvBot.js +++ b/npm/packages/ruvbot/src/RuvBot.js @@ -336,10 +336,19 @@ class RuvBot extends eventemitter3_1.EventEmitter { // Initialize LLM provider based on configuration const { provider, apiKey, model } = config.llm; // Check for available API keys in priority order + const novitaKey = process.env.NOVITA_API_KEY; const openrouterKey = process.env.OPENROUTER_API_KEY; const anthropicKey = process.env.ANTHROPIC_API_KEY || apiKey; const googleAIKey = process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY; - if (openrouterKey) { + if (novitaKey) { + this.llmProvider = (0, index_js_1.createNovitaProvider)({ + apiKey: novitaKey, + baseUrl: 'https://api.novita.ai/openai', + model: model || process.env.NOVITA_MODEL || 'deepseek/deepseek-v3.2', + }); + this.logger.info({ provider: 'novita', model: model || process.env.NOVITA_MODEL || 'deepseek/deepseek-v3.2' }, 'LLM provider initialized'); + } + else if (openrouterKey) { // Use OpenRouter for Gemini 2.5 and other models this.llmProvider = (0, index_js_1.createOpenRouterProvider)({ apiKey: openrouterKey, @@ -372,7 +381,7 @@ class RuvBot extends eventemitter3_1.EventEmitter { this.logger.info({ provider: 'anthropic', model }, 'LLM provider initialized'); } else { - this.logger.warn({}, 'No LLM API key found. Set GOOGLE_AI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY'); + this.logger.warn({}, 'No LLM API key found. Set NOVITA_API_KEY, GOOGLE_AI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY'); } // TODO: Initialize memory manager, skill registry, etc. } @@ -538,6 +547,7 @@ class RuvBot extends eventemitter3_1.EventEmitter { To enable AI responses, please set one of these environment variables: +- \`NOVITA_API_KEY\` - Novita OpenAI-compatible endpoint (\`https://api.novita.ai/openai\`) - \`GOOGLE_AI_API_KEY\` - Get from [Google AI Studio](https://aistudio.google.com/app/apikey) - \`ANTHROPIC_API_KEY\` - Get from [Anthropic Console](https://console.anthropic.com/) - \`OPENROUTER_API_KEY\` - Get from [OpenRouter](https://openrouter.ai/) @@ -604,4 +614,4 @@ function createRuvBotFromEnv() { return new RuvBot(); } exports.default = RuvBot; -//# sourceMappingURL=RuvBot.js.map \ No newline at end of file +//# sourceMappingURL=RuvBot.js.map diff --git a/npm/packages/ruvbot/src/RuvBot.ts b/npm/packages/ruvbot/src/RuvBot.ts index 8fdd0c1f9..c016e5d8a 100644 --- a/npm/packages/ruvbot/src/RuvBot.ts +++ b/npm/packages/ruvbot/src/RuvBot.ts @@ -30,6 +30,7 @@ import { createAnthropicProvider, createOpenRouterProvider, createGoogleAIProvider, + createNovitaProvider, } from './integration/providers/index.js'; type BotState = BotStatus; @@ -477,11 +478,22 @@ export class RuvBot extends EventEmitter { const { provider, apiKey, model } = config.llm; // Check for available API keys in priority order + const novitaKey = process.env.NOVITA_API_KEY; const openrouterKey = process.env.OPENROUTER_API_KEY; const anthropicKey = process.env.ANTHROPIC_API_KEY || apiKey; const googleAIKey = process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY; - if (openrouterKey) { + if (novitaKey) { + this.llmProvider = createNovitaProvider({ + apiKey: novitaKey, + baseUrl: 'https://api.novita.ai/openai', + model: model || process.env.NOVITA_MODEL || 'deepseek/deepseek-v3.2', + }); + this.logger.info( + { provider: 'novita', model: model || process.env.NOVITA_MODEL || 'deepseek/deepseek-v3.2' }, + 'LLM provider initialized' + ); + } else if (openrouterKey) { // Use OpenRouter for Gemini 2.5 and other models this.llmProvider = createOpenRouterProvider({ apiKey: openrouterKey, @@ -510,7 +522,10 @@ export class RuvBot extends EventEmitter { }); this.logger.info({ provider: 'anthropic', model }, 'LLM provider initialized'); } else { - this.logger.warn({}, 'No LLM API key found. Set GOOGLE_AI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY'); + this.logger.warn( + {}, + 'No LLM API key found. Set NOVITA_API_KEY, GOOGLE_AI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY' + ); } // TODO: Initialize memory manager, skill registry, etc. @@ -700,6 +715,7 @@ export class RuvBot extends EventEmitter { To enable AI responses, please set one of these environment variables: +- \`NOVITA_API_KEY\` - Novita OpenAI-compatible endpoint (\`https://api.novita.ai/openai\`) - \`GOOGLE_AI_API_KEY\` - Get from [Google AI Studio](https://aistudio.google.com/app/apikey) - \`ANTHROPIC_API_KEY\` - Get from [Anthropic Console](https://console.anthropic.com/) - \`OPENROUTER_API_KEY\` - Get from [OpenRouter](https://openrouter.ai/) diff --git a/npm/packages/ruvbot/src/api/public/index.html b/npm/packages/ruvbot/src/api/public/index.html index 535c09c1e..c5203169f 100644 --- a/npm/packages/ruvbot/src/api/public/index.html +++ b/npm/packages/ruvbot/src/api/public/index.html @@ -918,7 +918,7 @@

Welcome to RuvBot

if (status.llm?.configured) { console.log('[RuvBot] LLM configured:', status.llm.provider, status.llm.model); } else { - console.warn('[RuvBot] LLM not configured! Check ANTHROPIC_API_KEY or OPENROUTER_API_KEY'); + console.warn('[RuvBot] LLM not configured! Check NOVITA_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY'); } } catch (err) { console.warn('[RuvBot] Health check failed:', err); diff --git a/npm/packages/ruvbot/src/core/BotConfig.d.ts b/npm/packages/ruvbot/src/core/BotConfig.d.ts index 66e5eb603..833169032 100644 --- a/npm/packages/ruvbot/src/core/BotConfig.d.ts +++ b/npm/packages/ruvbot/src/core/BotConfig.d.ts @@ -28,7 +28,7 @@ export declare const MemoryConfigSchema: z.ZodObject<{ efSearch?: number | undefined; }>; export declare const LLMConfigSchema: z.ZodObject<{ - provider: z.ZodDefault>; + provider: z.ZodDefault>; model: z.ZodDefault; apiKey: z.ZodOptional; baseUrl: z.ZodOptional; @@ -36,7 +36,7 @@ export declare const LLMConfigSchema: z.ZodObject<{ maxTokens: z.ZodDefault; streaming: z.ZodDefault; }, "strip", z.ZodTypeAny, { - provider: "anthropic" | "openai" | "local" | "google" | "ruvllm"; + provider: "anthropic" | "openai" | "google" | "novita" | "local" | "ruvllm"; model: string; streaming: boolean; temperature: number; @@ -216,7 +216,7 @@ export declare const BotConfigSchema: z.ZodObject<{ efSearch?: number | undefined; }>>; llm: z.ZodDefault>; + provider: z.ZodDefault>; model: z.ZodDefault; apiKey: z.ZodOptional; baseUrl: z.ZodOptional; @@ -224,7 +224,7 @@ export declare const BotConfigSchema: z.ZodObject<{ maxTokens: z.ZodDefault; streaming: z.ZodDefault; }, "strip", z.ZodTypeAny, { - provider: "anthropic" | "openai" | "local" | "google" | "ruvllm"; + provider: "anthropic" | "openai" | "google" | "novita" | "local" | "ruvllm"; model: string; streaming: boolean; temperature: number; @@ -458,7 +458,7 @@ export declare const BotConfigSchema: z.ZodObject<{ connectionString?: string | undefined; }; llm: { - provider: "anthropic" | "openai" | "local" | "google" | "ruvllm"; + provider: "anthropic" | "openai" | "google" | "novita" | "local" | "ruvllm"; model: string; streaming: boolean; temperature: number; @@ -608,4 +608,4 @@ export declare class ConfigManager { */ toJSON(): string; } -//# sourceMappingURL=BotConfig.d.ts.map \ No newline at end of file +//# sourceMappingURL=BotConfig.d.ts.map diff --git a/npm/packages/ruvbot/src/core/BotConfig.js b/npm/packages/ruvbot/src/core/BotConfig.js index d15c4f7cf..73f35f89f 100644 --- a/npm/packages/ruvbot/src/core/BotConfig.js +++ b/npm/packages/ruvbot/src/core/BotConfig.js @@ -18,7 +18,7 @@ exports.MemoryConfigSchema = zod_1.z.object({ m: zod_1.z.number().int().min(4).max(64).default(16), }); exports.LLMConfigSchema = zod_1.z.object({ - provider: zod_1.z.enum(['anthropic', 'openai', 'google', 'local', 'ruvllm']).default('anthropic'), + provider: zod_1.z.enum(['anthropic', 'openai', 'google', 'novita', 'local', 'ruvllm']).default('anthropic'), model: zod_1.z.string().default('claude-sonnet-4-20250514'), apiKey: zod_1.z.string().optional(), baseUrl: zod_1.z.string().url().optional(), @@ -161,7 +161,13 @@ class ConfigManager { const storageConfig = {}; const loggingConfig = {}; // LLM configuration - if (process.env.ANTHROPIC_API_KEY) { + if (process.env.NOVITA_API_KEY) { + llmConfig.provider = 'novita'; + llmConfig.apiKey = process.env.NOVITA_API_KEY; + llmConfig.baseUrl = 'https://api.novita.ai/openai'; + llmConfig.model = process.env.NOVITA_MODEL || 'deepseek/deepseek-v3.2'; + } + else if (process.env.ANTHROPIC_API_KEY) { llmConfig.provider = 'anthropic'; llmConfig.apiKey = process.env.ANTHROPIC_API_KEY; } @@ -220,4 +226,4 @@ class ConfigManager { } } exports.ConfigManager = ConfigManager; -//# sourceMappingURL=BotConfig.js.map \ No newline at end of file +//# sourceMappingURL=BotConfig.js.map diff --git a/npm/packages/ruvbot/src/core/BotConfig.ts b/npm/packages/ruvbot/src/core/BotConfig.ts index 39b39c295..25349f1d0 100644 --- a/npm/packages/ruvbot/src/core/BotConfig.ts +++ b/npm/packages/ruvbot/src/core/BotConfig.ts @@ -20,7 +20,7 @@ export const MemoryConfigSchema = z.object({ }); export const LLMConfigSchema = z.object({ - provider: z.enum(['anthropic', 'openai', 'google', 'local', 'ruvllm']).default('anthropic'), + provider: z.enum(['anthropic', 'openai', 'google', 'novita', 'local', 'ruvllm']).default('anthropic'), model: z.string().default('claude-sonnet-4-20250514'), apiKey: z.string().optional(), baseUrl: z.string().url().optional(), @@ -186,7 +186,12 @@ export class ConfigManager { const loggingConfig: Partial> = {}; // LLM configuration - if (process.env.ANTHROPIC_API_KEY) { + if (process.env.NOVITA_API_KEY) { + llmConfig.provider = 'novita'; + llmConfig.apiKey = process.env.NOVITA_API_KEY; + llmConfig.baseUrl = 'https://api.novita.ai/openai'; + llmConfig.model = process.env.NOVITA_MODEL || 'deepseek/deepseek-v3.2'; + } else if (process.env.ANTHROPIC_API_KEY) { llmConfig.provider = 'anthropic'; llmConfig.apiKey = process.env.ANTHROPIC_API_KEY; } else if (process.env.OPENAI_API_KEY) { diff --git a/npm/packages/ruvbot/src/core/types.d.ts b/npm/packages/ruvbot/src/core/types.d.ts index 72553424c..8697dc2e4 100644 --- a/npm/packages/ruvbot/src/core/types.d.ts +++ b/npm/packages/ruvbot/src/core/types.d.ts @@ -174,7 +174,7 @@ export interface LLMConfig { maxTokens?: number; streaming?: boolean; } -export type LLMProvider = 'anthropic' | 'openai' | 'google' | 'local' | 'ruvllm'; +export type LLMProvider = 'anthropic' | 'openai' | 'google' | 'novita' | 'local' | 'ruvllm'; export interface LLMRequest { messages: LLMMessage[]; systemPrompt?: string; @@ -245,4 +245,4 @@ export interface LLMOrchestrator { stream(request: LLMRequest): AsyncIterable; embed(text: string): Promise; } -//# sourceMappingURL=types.d.ts.map \ No newline at end of file +//# sourceMappingURL=types.d.ts.map diff --git a/npm/packages/ruvbot/src/core/types.ts b/npm/packages/ruvbot/src/core/types.ts index 9a7b53668..b8bded775 100644 --- a/npm/packages/ruvbot/src/core/types.ts +++ b/npm/packages/ruvbot/src/core/types.ts @@ -226,7 +226,7 @@ export interface LLMConfig { streaming?: boolean; } -export type LLMProvider = 'anthropic' | 'openai' | 'google' | 'local' | 'ruvllm'; +export type LLMProvider = 'anthropic' | 'openai' | 'google' | 'novita' | 'local' | 'ruvllm'; export interface LLMRequest { messages: LLMMessage[]; diff --git a/npm/packages/ruvbot/src/integration/providers/NovitaProvider.ts b/npm/packages/ruvbot/src/integration/providers/NovitaProvider.ts new file mode 100644 index 000000000..83140d554 --- /dev/null +++ b/npm/packages/ruvbot/src/integration/providers/NovitaProvider.ts @@ -0,0 +1,246 @@ +/** + * NovitaProvider - Novita OpenAI-Compatible LLM Integration + * + * Uses the official OpenAI SDK against Novita's OpenAI-compatible endpoint. + */ + +import OpenAI from 'openai'; + +import type { + LLMProvider, + Message, + CompletionOptions, + StreamOptions, + Completion, + Token, + ModelInfo, + Tool, + ToolCall, +} from './index.js'; + +export interface NovitaConfig { + apiKey: string; + baseUrl?: string; + model?: string; + maxRetries?: number; + timeout?: number; +} + +export type NovitaModel = + | 'deepseek/deepseek-v3.2' + | 'minimax/minimax-m2.5' + | 'zai-org/glm-5' + | string; + +type NovitaMessage = { + role: 'user' | 'assistant' | 'system'; + content: string; +}; + +const MODEL_INFO: Record = { + 'deepseek/deepseek-v3.2': { + id: 'deepseek/deepseek-v3.2', + name: 'DeepSeek V3.2', + maxTokens: 8192, + contextWindow: 64000, + }, + 'minimax/minimax-m2.5': { + id: 'minimax/minimax-m2.5', + name: 'MiniMax M2.5', + maxTokens: 8192, + contextWindow: 128000, + }, + 'zai-org/glm-5': { + id: 'zai-org/glm-5', + name: 'GLM-5', + maxTokens: 8192, + contextWindow: 128000, + }, +}; + +export class NovitaProvider implements LLMProvider { + private readonly config: Required; + private readonly client: OpenAI; + private readonly model: NovitaModel; + + constructor(config: NovitaConfig) { + this.config = { + apiKey: config.apiKey, + baseUrl: config.baseUrl ?? 'https://api.novita.ai/openai', + model: config.model ?? 'deepseek/deepseek-v3.2', + maxRetries: config.maxRetries ?? 3, + timeout: config.timeout ?? 120000, + }; + this.model = this.config.model; + this.client = new OpenAI({ + apiKey: this.config.apiKey, + baseURL: this.config.baseUrl, + maxRetries: this.config.maxRetries, + timeout: this.config.timeout, + }); + } + + async complete(messages: Message[], options?: CompletionOptions): Promise { + const modelInfo = this.getModel(); + const response = await this.client.chat.completions.create({ + model: this.model, + messages: this.convertMessages(messages), + max_tokens: options?.maxTokens ?? modelInfo.maxTokens, + temperature: options?.temperature ?? 0.7, + top_p: options?.topP, + stop: options?.stopSequences, + tools: options?.tools ? this.convertTools(options.tools) : undefined, + }); + + const choice = response.choices[0]; + const finishReason = this.mapFinishReason(choice?.finish_reason); + const toolCalls = (choice?.message.tool_calls ?? []).map((toolCall) => ({ + id: toolCall.id, + name: toolCall.function.name, + input: JSON.parse(toolCall.function.arguments || '{}'), + })); + + return { + content: choice?.message.content ?? '', + finishReason, + usage: { + inputTokens: response.usage?.prompt_tokens ?? 0, + outputTokens: response.usage?.completion_tokens ?? 0, + }, + toolCalls: toolCalls.length > 0 ? toolCalls : undefined, + }; + } + + async *stream(messages: Message[], options?: StreamOptions): AsyncGenerator { + const modelInfo = this.getModel(); + const stream = await this.client.chat.completions.create({ + model: this.model, + messages: this.convertMessages(messages), + max_tokens: options?.maxTokens ?? modelInfo.maxTokens, + temperature: options?.temperature ?? 0.7, + top_p: options?.topP, + stop: options?.stopSequences, + tools: options?.tools ? this.convertTools(options.tools) : undefined, + stream: true, + }); + + let fullContent = ''; + let inputTokens = 0; + let outputTokens = 0; + let finishReason: Completion['finishReason'] = 'stop'; + const toolCalls: ToolCall[] = []; + const pendingToolCalls: Map = new Map(); + + for await (const chunk of stream) { + const choice = chunk.choices[0]; + if (!choice) continue; + + const token = choice.delta.content; + if (token) { + fullContent += token; + options?.onToken?.(token); + yield { type: 'text', text: token }; + } + + if (choice.delta.tool_calls) { + for (const tc of choice.delta.tool_calls) { + const idx = tc.index ?? 0; + if (!pendingToolCalls.has(idx)) { + pendingToolCalls.set(idx, { id: '', name: '', arguments: '' }); + } + const pending = pendingToolCalls.get(idx)!; + if (tc.id) pending.id = tc.id; + if (tc.function?.name) pending.name = tc.function.name; + if (tc.function?.arguments) pending.arguments += tc.function.arguments; + } + } + + if (choice.finish_reason) { + finishReason = this.mapFinishReason(choice.finish_reason); + } + + if (chunk.usage) { + inputTokens = chunk.usage.prompt_tokens ?? inputTokens; + outputTokens = chunk.usage.completion_tokens ?? outputTokens; + } + } + + for (const pending of pendingToolCalls.values()) { + if (!pending.id || !pending.name) continue; + try { + const input = JSON.parse(pending.arguments || '{}'); + const toolUse = { id: pending.id, name: pending.name, input }; + toolCalls.push(toolUse); + yield { type: 'tool_use', toolUse }; + } catch { + // Ignore invalid function argument payloads. + } + } + + return { + content: fullContent, + finishReason, + usage: { inputTokens, outputTokens }, + toolCalls: toolCalls.length > 0 ? toolCalls : undefined, + }; + } + + async countTokens(text: string): Promise { + return Math.ceil(text.length / 4); + } + + getModel(): ModelInfo { + return MODEL_INFO[this.model] ?? { + id: this.model, + name: this.model, + maxTokens: 4096, + contextWindow: 32000, + }; + } + + async isHealthy(): Promise { + try { + await this.client.chat.completions.create({ + model: this.model, + messages: [{ role: 'user', content: 'ping' }], + max_tokens: 1, + }); + return true; + } catch { + return false; + } + } + + private mapFinishReason(reason: string | null | undefined): Completion['finishReason'] { + if (reason === 'length') return 'length'; + if (reason === 'tool_calls') return 'tool_use'; + return 'stop'; + } + + private convertMessages(messages: Message[]): NovitaMessage[] { + return messages.map((msg) => ({ + role: msg.role, + content: msg.content, + })); + } + + private convertTools(tools: Tool[]): Array<{ + type: 'function'; + function: { name: string; description: string; parameters: Record }; + }> { + return tools.map((tool) => ({ + type: 'function', + function: { + name: tool.name, + description: tool.description, + parameters: tool.parameters, + }, + })); + } +} + +export function createNovitaProvider(config: NovitaConfig): NovitaProvider { + return new NovitaProvider(config); +} + +export default NovitaProvider; diff --git a/npm/packages/ruvbot/src/integration/providers/index.ts b/npm/packages/ruvbot/src/integration/providers/index.ts index 843296bf7..05d2bbd23 100644 --- a/npm/packages/ruvbot/src/integration/providers/index.ts +++ b/npm/packages/ruvbot/src/integration/providers/index.ts @@ -96,3 +96,10 @@ export { type GoogleAIConfig, type GoogleAIModel, } from './GoogleAIProvider.js'; + +export { + NovitaProvider, + createNovitaProvider, + type NovitaConfig, + type NovitaModel, +} from './NovitaProvider.js'; diff --git a/npm/packages/ruvbot/src/server.js b/npm/packages/ruvbot/src/server.js index 0e4f316b5..08eaab23c 100644 --- a/npm/packages/ruvbot/src/server.js +++ b/npm/packages/ruvbot/src/server.js @@ -192,12 +192,15 @@ async function handleStatus(ctx) { const config = bot.getConfig(); // Check LLM configuration const hasAnthropicKey = !!(process.env.ANTHROPIC_API_KEY || config.llm?.apiKey); + const hasNovitaKey = !!process.env.NOVITA_API_KEY; const hasOpenRouterKey = !!process.env.OPENROUTER_API_KEY; const hasGoogleAIKey = !!(process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY); - const hasAnyKey = hasAnthropicKey || hasOpenRouterKey || hasGoogleAIKey; + const hasAnyKey = hasAnthropicKey || hasNovitaKey || hasOpenRouterKey || hasGoogleAIKey; // Determine active provider let activeProvider = 'none'; - if (hasOpenRouterKey) + if (hasNovitaKey) + activeProvider = 'novita'; + else if (hasOpenRouterKey) activeProvider = 'openrouter'; else if (hasGoogleAIKey) activeProvider = 'google-ai'; @@ -214,6 +217,7 @@ async function handleStatus(ctx) { environment: { nodeEnv: NODE_ENV, hasAnthropicKey, + hasNovitaKey, hasOpenRouterKey, hasGoogleAIKey, }, @@ -627,4 +631,4 @@ startServer().catch((error) => { logger.error({ err: error, errorMessage }, 'Failed to start server'); process.exit(1); }); -//# sourceMappingURL=server.js.map \ No newline at end of file +//# sourceMappingURL=server.js.map diff --git a/npm/packages/ruvbot/src/server.ts b/npm/packages/ruvbot/src/server.ts index 569769fe5..d0e751f12 100644 --- a/npm/packages/ruvbot/src/server.ts +++ b/npm/packages/ruvbot/src/server.ts @@ -226,13 +226,15 @@ async function handleStatus(ctx: RequestContext): Promise { // Check LLM configuration const hasAnthropicKey = !!(process.env.ANTHROPIC_API_KEY || config.llm?.apiKey); + const hasNovitaKey = !!process.env.NOVITA_API_KEY; const hasOpenRouterKey = !!process.env.OPENROUTER_API_KEY; const hasGoogleAIKey = !!(process.env.GOOGLE_AI_API_KEY || process.env.GEMINI_API_KEY); - const hasAnyKey = hasAnthropicKey || hasOpenRouterKey || hasGoogleAIKey; + const hasAnyKey = hasAnthropicKey || hasNovitaKey || hasOpenRouterKey || hasGoogleAIKey; // Determine active provider let activeProvider = 'none'; - if (hasOpenRouterKey) activeProvider = 'openrouter'; + if (hasNovitaKey) activeProvider = 'novita'; + else if (hasOpenRouterKey) activeProvider = 'openrouter'; else if (hasGoogleAIKey) activeProvider = 'google-ai'; else if (hasAnthropicKey) activeProvider = 'anthropic'; @@ -247,6 +249,7 @@ async function handleStatus(ctx: RequestContext): Promise { environment: { nodeEnv: NODE_ENV, hasAnthropicKey, + hasNovitaKey, hasOpenRouterKey, hasGoogleAIKey, },