diff --git a/v3/@claude-flow/providers/src/__tests__/novita-provider.test.ts b/v3/@claude-flow/providers/src/__tests__/novita-provider.test.ts new file mode 100644 index 0000000000..3dca72bebc --- /dev/null +++ b/v3/@claude-flow/providers/src/__tests__/novita-provider.test.ts @@ -0,0 +1,59 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { NovitaProvider } from '../novita-provider.js'; +import { consoleLogger } from '../base-provider.js'; + +describe('NovitaProvider', () => { + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it('uses Novita OpenAI-compatible endpoint and returns novita provider label', async () => { + const fetchMock = vi.fn(async (input: RequestInfo | URL) => { + expect(String(input)).toBe('https://api.novita.ai/openai/chat/completions'); + return new Response( + JSON.stringify({ + id: 'cmpl-test', + object: 'chat.completion', + created: Date.now(), + model: 'gpt-4o-mini', + choices: [ + { + index: 0, + message: { role: 'assistant', content: 'hello from novita' }, + finish_reason: 'stop', + }, + ], + usage: { + prompt_tokens: 5, + completion_tokens: 3, + total_tokens: 8, + }, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ); + }); + + vi.stubGlobal('fetch', fetchMock); + + const provider = new NovitaProvider({ + config: { + provider: 'novita', + apiKey: 'test-key', + model: 'gpt-4o-mini', + }, + logger: consoleLogger, + }); + + await provider.initialize(); + const response = await provider.complete({ + messages: [{ role: 'user', content: 'hello' }], + }); + + expect(response.provider).toBe('novita'); + expect(response.content).toBe('hello from novita'); + expect(fetchMock).toHaveBeenCalledTimes(1); + + provider.destroy(); + }); +}); diff --git a/v3/@claude-flow/providers/src/index.ts b/v3/@claude-flow/providers/src/index.ts index 335742ca42..7afcd56f63 100644 --- a/v3/@claude-flow/providers/src/index.ts +++ b/v3/@claude-flow/providers/src/index.ts @@ -31,6 +31,7 @@ export type { BaseProviderOptions, ILogger } from './base-provider.js'; // Export providers export { AnthropicProvider } from './anthropic-provider.js'; export { OpenAIProvider } from './openai-provider.js'; +export { NovitaProvider } from './novita-provider.js'; export { GoogleProvider } from './google-provider.js'; export { CohereProvider } from './cohere-provider.js'; export { OllamaProvider } from './ollama-provider.js'; diff --git a/v3/@claude-flow/providers/src/novita-provider.ts b/v3/@claude-flow/providers/src/novita-provider.ts new file mode 100644 index 0000000000..189b5fc70d --- /dev/null +++ b/v3/@claude-flow/providers/src/novita-provider.ts @@ -0,0 +1,25 @@ +/** + * V3 Novita Provider + * + * OpenAI-compatible provider configured for Novita endpoint. + * + * @module @claude-flow/providers/novita-provider + */ + +import { BaseProviderOptions } from './base-provider.js'; +import { OpenAIProvider } from './openai-provider.js'; + +const NOVITA_API_URL = 'https://api.novita.ai/openai'; + +export class NovitaProvider extends OpenAIProvider { + constructor(options: BaseProviderOptions) { + super({ + ...options, + config: { + ...options.config, + provider: 'novita', + apiUrl: options.config.apiUrl || NOVITA_API_URL, + }, + }); + } +} diff --git a/v3/@claude-flow/providers/src/openai-provider.ts b/v3/@claude-flow/providers/src/openai-provider.ts index b3e1de1a52..456faaaafc 100644 --- a/v3/@claude-flow/providers/src/openai-provider.ts +++ b/v3/@claude-flow/providers/src/openai-provider.ts @@ -79,7 +79,7 @@ interface OpenAIResponse { } export class OpenAIProvider extends BaseProvider { - readonly name: LLMProvider = 'openai'; + readonly name: LLMProvider; readonly capabilities: ProviderCapabilities = { supportedModels: [ 'gpt-4o', @@ -168,16 +168,17 @@ export class OpenAIProvider extends BaseProvider { }, }; - private baseUrl: string = 'https://api.openai.com/v1'; + protected baseUrl: string = 'https://api.openai.com/v1'; private headers: Record = {}; constructor(options: BaseProviderOptions) { super(options); + this.name = options.config.provider === 'novita' ? 'novita' : 'openai'; } protected async doInitialize(): Promise { if (!this.config.apiKey) { - throw new AuthenticationError('OpenAI API key is required', 'openai'); + throw new AuthenticationError(`${this.name} API key is required`, this.name); } this.baseUrl = this.config.apiUrl || 'https://api.openai.com/v1'; @@ -433,7 +434,7 @@ export class OpenAIProvider extends BaseProvider { return { id: data.id, model: model as LLMModel, - provider: 'openai', + provider: this.name, content: choice.message.content || '', toolCalls: choice.message.tool_calls, usage: { @@ -465,22 +466,22 @@ export class OpenAIProvider extends BaseProvider { switch (response.status) { case 401: - throw new AuthenticationError(message, 'openai', errorData); + throw new AuthenticationError(message, this.name, errorData); case 429: const retryAfter = response.headers.get('retry-after'); throw new RateLimitError( message, - 'openai', + this.name, retryAfter ? parseInt(retryAfter) : undefined, errorData ); case 404: - throw new ModelNotFoundError(this.config.model, 'openai', errorData); + throw new ModelNotFoundError(this.config.model, this.name, errorData); default: throw new LLMProviderError( message, `OPENAI_${response.status}`, - 'openai', + this.name, response.status, response.status >= 500, errorData diff --git a/v3/@claude-flow/providers/src/provider-manager.ts b/v3/@claude-flow/providers/src/provider-manager.ts index 506185afa6..a9d3570c24 100644 --- a/v3/@claude-flow/providers/src/provider-manager.ts +++ b/v3/@claude-flow/providers/src/provider-manager.ts @@ -31,6 +31,7 @@ import { import { BaseProviderOptions, ILogger, consoleLogger } from './base-provider.js'; import { AnthropicProvider } from './anthropic-provider.js'; import { OpenAIProvider } from './openai-provider.js'; +import { NovitaProvider } from './novita-provider.js'; import { GoogleProvider } from './google-provider.js'; import { CohereProvider } from './cohere-provider.js'; import { OllamaProvider } from './ollama-provider.js'; @@ -119,6 +120,8 @@ export class ProviderManager extends EventEmitter { return new AnthropicProvider(options); case 'openai': return new OpenAIProvider(options); + case 'novita': + return new NovitaProvider(options); case 'google': return new GoogleProvider(options); case 'cohere': diff --git a/v3/@claude-flow/providers/src/types.ts b/v3/@claude-flow/providers/src/types.ts index f9d23d25e1..47670b8f3c 100644 --- a/v3/@claude-flow/providers/src/types.ts +++ b/v3/@claude-flow/providers/src/types.ts @@ -14,6 +14,7 @@ import { EventEmitter } from 'events'; export type LLMProvider = | 'anthropic' | 'openai' + | 'novita' | 'google' | 'cohere' | 'ollama'