diff --git a/README.md b/README.md index 6fba155f..d4f724af 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,8 @@ CCS gives you one stable command surface while letting you switch between: - multiple Claude subscriptions and isolated account contexts - OAuth providers like Codex, xAI/Grok, Kiro, Claude, Kimi, and more, with legacy Copilot compatibility for existing setups -- API and local-model profiles like GLM, Kimi, OpenRouter, Ollama, llama.cpp, - Novita, Fireworks AI, and Alibaba Coding Plan +- API and local-model profiles like GLM, Kimi, OpenRouter, OrcaRouter, Ollama, + llama.cpp, Novita, Fireworks AI, and Alibaba Coding Plan Qwen Code account linking is not available in the bundled CLIProxy runtime yet; use an API-key Qwen profile such as Alibaba Coding Plan for Qwen models. diff --git a/eslint-rules/throw-error-baseline.json b/eslint-rules/throw-error-baseline.json index 9fa25647..e1be244b 100644 --- a/eslint-rules/throw-error-baseline.json +++ b/eslint-rules/throw-error-baseline.json @@ -220,13 +220,13 @@ "src/shared/claude-extension-setup.ts:247", "src/shared/claude-extension-setup.ts:257", "src/shared/claude-extension-setup.ts:317", - "src/shared/provider-preset-catalog.ts:339", - "src/shared/provider-preset-catalog.ts:342", - "src/shared/provider-preset-catalog.ts:351", - "src/shared/provider-preset-catalog.ts:354", - "src/shared/provider-preset-catalog.ts:357", - "src/shared/provider-preset-catalog.ts:362", - "src/shared/provider-preset-catalog.ts:365", + "src/shared/provider-preset-catalog.ts:356", + "src/shared/provider-preset-catalog.ts:359", + "src/shared/provider-preset-catalog.ts:368", + "src/shared/provider-preset-catalog.ts:371", + "src/shared/provider-preset-catalog.ts:374", + "src/shared/provider-preset-catalog.ts:379", + "src/shared/provider-preset-catalog.ts:382", "src/shared/toml-object.ts:23", "src/targets/codex-adapter.ts:103", "src/targets/codex-adapter.ts:275", diff --git a/src/shared/provider-preset-catalog.ts b/src/shared/provider-preset-catalog.ts index 5880588a..ab2db76d 100644 --- a/src/shared/provider-preset-catalog.ts +++ b/src/shared/provider-preset-catalog.ts @@ -10,6 +10,7 @@ export type ProviderPresetTarget = 'claude' | 'droid'; export const PROVIDER_PRESET_IDS = [ 'openrouter', + 'orcarouter', 'alibaba-coding-plan', 'huggingface', 'tuningengines', @@ -53,6 +54,7 @@ export interface ProviderPresetDefinition { } export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1'; +export const ORCAROUTER_BASE_URL = 'https://api.orcarouter.ai/v1'; /** * Legacy aliases mapped to canonical preset IDs. @@ -84,6 +86,21 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [ featured: true, icon: '/icons/openrouter.svg', }, + { + id: 'orcarouter', + name: 'OrcaRouter', + description: '188+ models from OpenAI, Anthropic, Google, Meta via auto-routing gateway', + baseUrl: ORCAROUTER_BASE_URL, + defaultProfileName: 'orcarouter', + defaultModel: 'openai/gpt-5.5', + apiKeyPlaceholder: 'sk-orca-...', + apiKeyHint: 'Get your API key at orcarouter.ai/console', + category: 'recommended', + requiresApiKey: true, + badge: '188+ models', + featured: true, + icon: '/icons/orcarouter.svg', + }, { id: 'alibaba-coding-plan', name: 'Alibaba Coding Plan', diff --git a/tests/unit/api/provider-presets-orcarouter.test.ts b/tests/unit/api/provider-presets-orcarouter.test.ts new file mode 100644 index 00000000..b4cac1a3 --- /dev/null +++ b/tests/unit/api/provider-presets-orcarouter.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'bun:test'; +import { getPresetById, isValidPresetId } from '../../../src/api/services/provider-presets'; +import { PROVIDER_PRESET_IDS } from '../../../src/shared/provider-preset-catalog'; + +describe('provider-presets-orcarouter', () => { + it('resolves orcarouter preset id', () => { + const preset = getPresetById('orcarouter'); + expect(preset?.id).toBe('orcarouter'); + expect(preset?.baseUrl).toBe('https://api.orcarouter.ai/v1'); + expect(preset?.defaultProfileName).toBe('orcarouter'); + }); + + it('registers orcarouter in PROVIDER_PRESET_IDS', () => { + expect(PROVIDER_PRESET_IDS).toContain('orcarouter'); + }); + + it('uses the OpenAI-compatible base URL with a /v1 suffix', () => { + const preset = getPresetById('orcarouter'); + expect(preset?.baseUrl).toBe('https://api.orcarouter.ai/v1'); + expect(preset?.baseUrl.endsWith('/v1')).toBe(true); + }); + + it('pins a provider/model default (openai/gpt-5.5)', () => { + const preset = getPresetById('orcarouter'); + expect(preset?.defaultModel).toBe('openai/gpt-5.5'); + }); + + it('validates orcarouter preset requires an API key with the sk-orca- placeholder', () => { + const preset = getPresetById('orcarouter'); + expect(preset?.requiresApiKey).toBe(true); + expect(preset?.apiKeyPlaceholder).toBe('sk-orca-...'); + }); + + it('is a recommended, featured provider', () => { + const preset = getPresetById('orcarouter'); + expect(preset?.category).toBe('recommended'); + expect(preset?.featured).toBe(true); + }); + + it('treats orcarouter as a valid preset id', () => { + expect(isValidPresetId('orcarouter')).toBe(true); + }); + + it('handles whitespace in orcarouter preset id', () => { + const preset = getPresetById(' orcarouter '); + expect(preset?.id).toBe('orcarouter'); + }); + + it('handles uppercase orcarouter preset id', () => { + const preset = getPresetById('ORCAROUTER'); + expect(preset?.id).toBe('orcarouter'); + }); + + it('does not resolve partial or invalid orcarouter ids', () => { + expect(getPresetById('orcarouter-invalid')).toBeUndefined(); + expect(isValidPresetId('orcarouter-invalid')).toBe(false); + }); +}); diff --git a/ui/public/icons/orcarouter.svg b/ui/public/icons/orcarouter.svg new file mode 100644 index 00000000..09c5ee0e --- /dev/null +++ b/ui/public/icons/orcarouter.svg @@ -0,0 +1 @@ +OrcaRouter \ No newline at end of file