From a1a8cad2b3e7a0129c5972be70775961f6777f36 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Fri, 13 Mar 2026 16:28:47 +0800 Subject: [PATCH 1/4] feat(novita): add Novita AI provider preset - Add novita preset with OpenAI-compatible endpoint - Default model: deepseek/deepseek-v3.2 - Endpoint: https://api.novita.ai/openai - API key via NOVITA_API_KEY env var --- src/shared/provider-preset-catalog.ts | 15 ++++++++ .../unit/api/provider-presets-novita.test.ts | 35 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 tests/unit/api/provider-presets-novita.test.ts diff --git a/src/shared/provider-preset-catalog.ts b/src/shared/provider-preset-catalog.ts index a1704d0d..b64d22d4 100644 --- a/src/shared/provider-preset-catalog.ts +++ b/src/shared/provider-preset-catalog.ts @@ -21,6 +21,7 @@ export const PROVIDER_PRESET_IDS = [ 'deepseek', 'qwen', 'ollama-cloud', + 'novita', ] as const; export type ProviderPresetId = (typeof PROVIDER_PRESET_IDS)[number]; @@ -254,6 +255,20 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [ badge: 'Cloud', icon: '/icons/ollama.svg', }, + { + id: 'novita', + name: 'Novita AI', + description: 'OpenAI-compatible API (Llama, Mistral, Qwen, and more)', + baseUrl: 'https://api.novita.ai/openai', + defaultProfileName: 'novita', + defaultModel: 'deepseek/deepseek-v3.2', + apiKeyPlaceholder: 'YOUR_NOVITA_API_KEY', + apiKeyHint: 'Get your API key at novita.ai', + category: 'alternative', + requiresApiKey: true, + badge: 'OpenAI-compatible', + icon: '/icons/novita.svg', + }, ]; function clonePresetDefinition(preset: ProviderPresetDefinition): ProviderPresetDefinition { diff --git a/tests/unit/api/provider-presets-novita.test.ts b/tests/unit/api/provider-presets-novita.test.ts new file mode 100644 index 00000000..976737b5 --- /dev/null +++ b/tests/unit/api/provider-presets-novita.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'bun:test'; +import { getPresetById, isValidPresetId } from '../../../src/api/services/provider-presets'; + +describe('provider-presets-novita', () => { + it('resolves novita preset id', () => { + const preset = getPresetById('novita'); + expect(preset?.id).toBe('novita'); + expect(preset?.baseUrl).toBe('https://api.novita.ai/openai'); + expect(preset?.defaultProfileName).toBe('novita'); + }); + + it('resolves novita preset with expected model IDs', () => { + const preset = getPresetById('novita'); + expect(preset?.defaultModel).toBe('deepseek/deepseek-v3.2'); + }); + + it('validates novita preset requires API key', () => { + const preset = getPresetById('novita'); + expect(preset?.requiresApiKey).toBe(true); + }); + + it('treats novita as a valid preset id', () => { + expect(isValidPresetId('novita')).toBe(true); + }); + + it('handles whitespace in novita preset id', () => { + const preset = getPresetById(' novita '); + expect(preset?.id).toBe('novita'); + }); + + it('handles uppercase novita preset id', () => { + const preset = getPresetById('NOVITA'); + expect(preset?.id).toBe('novita'); + }); +}); From 94fa96aa81f83a29aedde2abaa369ebe33d58f8f Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 17 Mar 2026 07:02:47 -0400 Subject: [PATCH 2/4] fix: correct novita preset endpoint and asset handling --- README.md | 1 + src/shared/provider-preset-catalog.ts | 7 +++---- tests/unit/api/provider-presets-novita.test.ts | 2 +- tests/unit/api/provider-presets.test.ts | 17 ++++++++++++++++- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 366d4df2..954e51da 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ The dashboard provides visual management for all account types: | **Azure Foundry** | API Key | `ccs foundry` | Claude via Microsoft Azure | | **Minimax** | API Key | `ccs mm` | M2 series, 1M context | | **DeepSeek** | API Key | `ccs deepseek` | V3.2 and R1 reasoning | +| **Novita AI** | API Key | `ccs api create --preset novita` | Anthropic-compatible Novita endpoint for Claude Code | | **Qwen (OAuth)** | OAuth | `ccs qwen` | Qwen Code via CLIProxy | | **Qwen API** | API Key | `ccs api create --preset qwen` | DashScope Anthropic-compatible API | | **Alibaba Coding Plan** | API Key | `ccs api create --preset alibaba-coding-plan` | Model Studio Coding Plan endpoint | diff --git a/src/shared/provider-preset-catalog.ts b/src/shared/provider-preset-catalog.ts index b64d22d4..0ca546a0 100644 --- a/src/shared/provider-preset-catalog.ts +++ b/src/shared/provider-preset-catalog.ts @@ -258,16 +258,15 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [ { id: 'novita', name: 'Novita AI', - description: 'OpenAI-compatible API (Llama, Mistral, Qwen, and more)', - baseUrl: 'https://api.novita.ai/openai', + description: 'Anthropic-compatible API for Claude Code and CCS profiles', + baseUrl: 'https://api.novita.ai/anthropic', defaultProfileName: 'novita', defaultModel: 'deepseek/deepseek-v3.2', apiKeyPlaceholder: 'YOUR_NOVITA_API_KEY', apiKeyHint: 'Get your API key at novita.ai', category: 'alternative', requiresApiKey: true, - badge: 'OpenAI-compatible', - icon: '/icons/novita.svg', + badge: 'Anthropic-compatible', }, ]; diff --git a/tests/unit/api/provider-presets-novita.test.ts b/tests/unit/api/provider-presets-novita.test.ts index 976737b5..33557503 100644 --- a/tests/unit/api/provider-presets-novita.test.ts +++ b/tests/unit/api/provider-presets-novita.test.ts @@ -5,7 +5,7 @@ describe('provider-presets-novita', () => { it('resolves novita preset id', () => { const preset = getPresetById('novita'); expect(preset?.id).toBe('novita'); - expect(preset?.baseUrl).toBe('https://api.novita.ai/openai'); + expect(preset?.baseUrl).toBe('https://api.novita.ai/anthropic'); expect(preset?.defaultProfileName).toBe('novita'); }); diff --git a/tests/unit/api/provider-presets.test.ts b/tests/unit/api/provider-presets.test.ts index 2246ed6e..9f5d03e2 100644 --- a/tests/unit/api/provider-presets.test.ts +++ b/tests/unit/api/provider-presets.test.ts @@ -1,5 +1,11 @@ +import { existsSync } from 'fs'; +import { resolve } from 'path'; import { describe, expect, it } from 'bun:test'; -import { getPresetById, isValidPresetId } from '../../../src/api/services/provider-presets'; +import { + PROVIDER_PRESETS, + getPresetById, + isValidPresetId, +} from '../../../src/api/services/provider-presets'; describe('provider-presets', () => { it('resolves Alibaba Coding Plan preset id', () => { @@ -54,4 +60,13 @@ describe('provider-presets', () => { const preset = getPresetById('qwen'); expect(preset?.defaultProfileName).toBe('qwen-api'); }); + + it('only references provider preset icons that exist in ui/public', () => { + for (const preset of PROVIDER_PRESETS) { + if (!preset.icon) continue; + + const iconPath = resolve(import.meta.dir, '../../../ui/public', preset.icon.replace(/^\/+/, '')); + expect(existsSync(iconPath)).toBe(true); + } + }); }); From eeadff3e52fe5f154ad647a990fef8603be2bc51 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 17 Mar 2026 07:20:46 -0400 Subject: [PATCH 3/4] fix: add novita preset icon asset --- src/shared/provider-preset-catalog.ts | 1 + ui/public/icons/novita.svg | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 ui/public/icons/novita.svg diff --git a/src/shared/provider-preset-catalog.ts b/src/shared/provider-preset-catalog.ts index 0ca546a0..bdb16441 100644 --- a/src/shared/provider-preset-catalog.ts +++ b/src/shared/provider-preset-catalog.ts @@ -267,6 +267,7 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [ category: 'alternative', requiresApiKey: true, badge: 'Anthropic-compatible', + icon: '/icons/novita.svg', }, ]; diff --git a/ui/public/icons/novita.svg b/ui/public/icons/novita.svg new file mode 100644 index 00000000..356b6d69 --- /dev/null +++ b/ui/public/icons/novita.svg @@ -0,0 +1,9 @@ + + Novita AI + + From 334f3697f939f81032965f31007ece2cedb33018 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 17 Mar 2026 08:13:50 -0400 Subject: [PATCH 4/4] test: cover invalid novita preset ids --- tests/unit/api/provider-presets-novita.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unit/api/provider-presets-novita.test.ts b/tests/unit/api/provider-presets-novita.test.ts index 33557503..b820e536 100644 --- a/tests/unit/api/provider-presets-novita.test.ts +++ b/tests/unit/api/provider-presets-novita.test.ts @@ -32,4 +32,9 @@ describe('provider-presets-novita', () => { const preset = getPresetById('NOVITA'); expect(preset?.id).toBe('novita'); }); + + it('does not resolve partial or invalid novita ids', () => { + expect(getPresetById('novita-invalid')).toBeUndefined(); + expect(isValidPresetId('novita-invalid')).toBe(false); + }); });