diff --git a/README.md b/README.md index a0382846..31d4cc0b 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,8 @@ The dashboard provides visual management for all account types: | **Kiro** | OAuth | `ccs kiro` | AWS CodeWhisperer (Claude-powered) | | **Antigravity** | OAuth | `ccs agy` | Alternative routing | | **OpenRouter** | API Key | `ccs openrouter` | 300+ models, unified API | +| **Ollama** | Local | `ccs ollama` | Local open-source models, privacy | +| **Ollama Cloud** | API Key | `ccs ollama-cloud` | Cloud-hosted open-source models | | **GLM** | API Key | `ccs glm` | Cost-optimized execution | | **Kimi** | API Key | `ccs kimi` | Long-context, thinking mode | | **Azure Foundry** | API Key | `ccs foundry` | Claude via Microsoft Azure | @@ -104,6 +106,8 @@ The dashboard provides visual management for all account types: **OpenRouter Integration** (v7.0.0): CCS v7.0.0 adds OpenRouter with interactive model picker, dynamic discovery, and tier mapping (opus/sonnet/haiku). Create via `ccs api create --preset openrouter` or dashboard. +**Ollama Integration**: Run local open-source models (qwen3-coder, gpt-oss:20b) with full privacy. Use `ccs api create --preset ollama` - requires [Ollama v0.14.0+](https://ollama.com) installed. For cloud models, use `ccs api create --preset ollama-cloud`. + **Azure Foundry**: Use `ccs api create --preset foundry` to set up Claude via Microsoft Azure AI Foundry. Requires Azure resource and API key from [ai.azure.com](https://ai.azure.com). ![OpenRouter API Profiles](assets/screenshots/api-profiles-openrouter.webp) @@ -131,6 +135,7 @@ ccs codex # OpenAI Codex (OAuth) ccs kiro # Kiro/AWS CodeWhisperer (OAuth) ccs ghcp # GitHub Copilot (OAuth device flow) ccs agy # Antigravity (OAuth) +ccs ollama # Local Ollama (no API key needed) ccs glm # GLM (API key) ``` @@ -145,7 +150,10 @@ ccs work "design the authentication system" # Terminal 2: Execution (GLM - cost optimized) ccs glm "implement the user service from the plan" -# Terminal 3: Review (Gemini) +# Terminal 3: Local testing (Ollama - offline, privacy) +ccs ollama "run tests and generate coverage report" + +# Terminal 4: Review (Gemini) ccs gemini "review the implementation for security issues" ``` diff --git a/config/base-ollama-cloud.settings.json b/config/base-ollama-cloud.settings.json new file mode 100644 index 00000000..a574e88b --- /dev/null +++ b/config/base-ollama-cloud.settings.json @@ -0,0 +1,10 @@ +{ + "env": { + "ANTHROPIC_BASE_URL": "https://ollama.com", + "ANTHROPIC_AUTH_TOKEN": "YOUR_OLLAMA_CLOUD_API_KEY_HERE", + "ANTHROPIC_MODEL": "glm-4.7:cloud", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "qwen3-coder:480b", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "glm-4.7:cloud", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "minimax-m2.1:cloud" + } +} diff --git a/config/base-ollama.settings.json b/config/base-ollama.settings.json new file mode 100644 index 00000000..8c14903e --- /dev/null +++ b/config/base-ollama.settings.json @@ -0,0 +1,10 @@ +{ + "env": { + "ANTHROPIC_BASE_URL": "http://localhost:11434", + "ANTHROPIC_AUTH_TOKEN": "ollama", + "ANTHROPIC_MODEL": "qwen3-coder", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "gpt-oss:120b", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "gpt-oss:20b", + "ANTHROPIC_DEFAULT_HAIKU_MODEL": "qwen3-coder" + } +} diff --git a/package.json b/package.json index 0657cb48..b985c35d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kaitranntt/ccs", - "version": "7.25.0", + "version": "7.25.0-dev.2", "description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6", "keywords": [ "cli", diff --git a/src/api/services/provider-presets.ts b/src/api/services/provider-presets.ts index 82223565..510246e4 100644 --- a/src/api/services/provider-presets.ts +++ b/src/api/services/provider-presets.ts @@ -17,6 +17,8 @@ export interface ProviderPreset { apiKeyPlaceholder: string; apiKeyHint: string; category: PresetCategory; + /** Whether API key is required (default: true, set false for local providers) */ + requiresApiKey: boolean; /** Additional env vars for thinking mode, etc. */ extraEnv?: Record; /** Enable always thinking mode */ @@ -42,6 +44,19 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ apiKeyPlaceholder: 'sk-or-...', apiKeyHint: 'Get your API key at openrouter.ai/keys', category: 'recommended', + requiresApiKey: true, + }, + { + id: 'ollama', + name: 'Ollama (Local)', + description: 'Local open-source models via Ollama (32K+ context)', + baseUrl: 'http://localhost:11434', + defaultProfileName: 'ollama', + defaultModel: 'qwen3-coder', + apiKeyPlaceholder: 'ollama', + apiKeyHint: 'Install Ollama from ollama.com - no API key needed for local', + category: 'recommended', + requiresApiKey: false, }, // Alternative providers { @@ -54,6 +69,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ apiKeyPlaceholder: 'ghp_...', apiKeyHint: 'Get your API key from Z.AI', category: 'alternative', + requiresApiKey: true, }, { id: 'glmt', @@ -65,6 +81,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ apiKeyPlaceholder: 'ghp_...', apiKeyHint: 'Same API key as GLM', category: 'alternative', + requiresApiKey: true, extraEnv: { ANTHROPIC_TEMPERATURE: '0.2', ANTHROPIC_MAX_TOKENS: '65536', @@ -85,6 +102,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ apiKeyPlaceholder: 'sk-...', apiKeyHint: 'Get your API key from Moonshot AI', category: 'alternative', + requiresApiKey: true, alwaysThinkingEnabled: true, }, { @@ -97,6 +115,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ apiKeyPlaceholder: 'YOUR_AZURE_API_KEY', apiKeyHint: 'Create resource at ai.azure.com, get API key from Keys tab', category: 'alternative', + requiresApiKey: true, }, { id: 'mm', @@ -108,6 +127,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ apiKeyPlaceholder: 'YOUR_MINIMAX_API_KEY_HERE', apiKeyHint: 'Get your API key at platform.minimax.io', category: 'alternative', + requiresApiKey: true, }, { id: 'deepseek', @@ -119,6 +139,7 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ apiKeyPlaceholder: 'sk-...', apiKeyHint: 'Get your API key at platform.deepseek.com', category: 'alternative', + requiresApiKey: true, }, { id: 'qwen', @@ -130,6 +151,19 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ apiKeyPlaceholder: 'sk-...', apiKeyHint: 'Get your API key from Alibaba Cloud Model Studio', category: 'alternative', + requiresApiKey: true, + }, + { + id: 'ollama-cloud', + name: 'Ollama Cloud', + description: 'Ollama cloud models via direct API (glm-4.7:cloud, minimax-m2.1:cloud)', + baseUrl: 'https://ollama.com', + defaultProfileName: 'ollama-cloud', + defaultModel: 'glm-4.7:cloud', + apiKeyPlaceholder: 'YOUR_OLLAMA_CLOUD_API_KEY', + apiKeyHint: 'Get your API key at ollama.com', + category: 'alternative', + requiresApiKey: true, }, ]; diff --git a/src/commands/api-command.ts b/src/commands/api-command.ts index 1c51b9fa..df38438e 100644 --- a/src/commands/api-command.ts +++ b/src/commands/api-command.ts @@ -181,9 +181,21 @@ async function handleCreate(args: string[]): Promise { console.log(dim('Note: For OpenRouter, ANTHROPIC_API_KEY should be empty.')); } - // Step 3: API Key + // Step 3: API Key (skip if preset has requiresApiKey: false) let apiKey = parsedArgs.apiKey; - if (!apiKey) { + + if (preset?.requiresApiKey === false) { + // Preset doesn't require API key (e.g., local Ollama) + if (parsedArgs.apiKey) { + console.log(dim('Note: Using provided API key for local Ollama (optional)')); + apiKey = parsedArgs.apiKey; + } else { + console.log(info('No API key required for local Ollama')); + // Sentinel value 'ollama' matches config/base-ollama.settings.json template + // This is not a valid API key, just a placeholder for local-only providers + apiKey = 'ollama'; + } + } else if (!apiKey) { const keyPrompt = preset?.apiKeyHint ? `API Key (${preset.apiKeyHint})` : 'API Key'; apiKey = await InteractivePrompt.password(keyPrompt); if (!apiKey) { @@ -426,7 +438,7 @@ async function showHelp(): Promise { console.log(''); console.log(subheader('Options')); console.log( - ` ${color('--preset ', 'command')} Use provider preset (openrouter, glm, glmt, kimi, foundry, mm, deepseek, qwen)` + ` ${color('--preset ', 'command')} Use provider preset (openrouter, ollama, ollama-cloud, glm, glmt, kimi, foundry, mm, deepseek, qwen)` ); console.log(` ${color('--base-url ', 'command')} API base URL (create)`); console.log(` ${color('--api-key ', 'command')} API key (create)`); @@ -438,6 +450,12 @@ async function showHelp(): Promise { console.log( ` ${color('openrouter', 'command')} OpenRouter - 349+ models (Claude, GPT, Gemini, Llama)` ); + console.log( + ` ${color('ollama', 'command')} Ollama - Local open-source models (no API key)` + ); + console.log( + ` ${color('ollama-cloud', 'command')} Ollama Cloud - glm-4.7:cloud, qwen3-coder:480b` + ); console.log(` ${color('glm', 'command')} GLM - Claude via Z.AI`); console.log(` ${color('glmt', 'command')} GLMT - GLM with Thinking mode`); console.log(` ${color('kimi', 'command')} Kimi - Moonshot AI reasoning model`); diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 51079d1b..0d1bc261 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -127,6 +127,8 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['ccs glm', 'GLM 4.6 (API key required)'], ['ccs glmt', 'GLM with thinking mode'], ['ccs kimi', 'Kimi for Coding (API key)'], + ['ccs ollama', 'Local Ollama (http://localhost:11434)'], + ['ccs ollama-cloud', 'Ollama Cloud (API key required)'], ['', ''], // Spacer ['ccs api create', 'Create custom API profile'], ['ccs api remove', 'Remove an API profile'], diff --git a/ui/public/icons/azure.svg b/ui/public/icons/azure.svg new file mode 100644 index 00000000..41f503f5 --- /dev/null +++ b/ui/public/icons/azure.svg @@ -0,0 +1 @@ +AzureAI \ No newline at end of file diff --git a/ui/public/icons/deepseek.svg b/ui/public/icons/deepseek.svg new file mode 100644 index 00000000..3fc23024 --- /dev/null +++ b/ui/public/icons/deepseek.svg @@ -0,0 +1 @@ +DeepSeek \ No newline at end of file diff --git a/ui/public/icons/kimi.svg b/ui/public/icons/kimi.svg new file mode 100644 index 00000000..ec5db53a --- /dev/null +++ b/ui/public/icons/kimi.svg @@ -0,0 +1 @@ +Kimi \ No newline at end of file diff --git a/ui/public/icons/minimax.svg b/ui/public/icons/minimax.svg new file mode 100644 index 00000000..2a60bd47 --- /dev/null +++ b/ui/public/icons/minimax.svg @@ -0,0 +1 @@ +Minimax \ No newline at end of file diff --git a/ui/public/icons/ollama.svg b/ui/public/icons/ollama.svg new file mode 100644 index 00000000..c5e666a0 --- /dev/null +++ b/ui/public/icons/ollama.svg @@ -0,0 +1 @@ +Ollama diff --git a/ui/public/icons/zai.svg b/ui/public/icons/zai.svg new file mode 100644 index 00000000..04ba2d98 --- /dev/null +++ b/ui/public/icons/zai.svg @@ -0,0 +1 @@ +Z.ai \ No newline at end of file diff --git a/ui/src/components/profiles/profile-create-dialog.tsx b/ui/src/components/profiles/profile-create-dialog.tsx index 37f92035..2c3d283a 100644 --- a/ui/src/components/profiles/profile-create-dialog.tsx +++ b/ui/src/components/profiles/profile-create-dialog.tsx @@ -47,7 +47,7 @@ const schema = z.object({ .min(1, 'Name is required') .regex(/^[a-zA-Z][a-zA-Z0-9._-]*$/, 'Must start with letter, only letters/numbers/.-_'), baseUrl: z.string().url('Invalid URL format'), - apiKey: z.string().min(1, 'API key is required'), + apiKey: z.string(), // Validation handled conditionally in onSubmit model: z.string().optional(), opusModel: z.string().optional(), sonnetModel: z.string().optional(), @@ -207,9 +207,16 @@ export function ProfileCreateDialog({ }, [baseUrlValue, selectedPreset]); const onSubmit = async (data: FormData) => { + // Validate API key - required unless preset has requiresApiKey: false + if (currentPreset?.requiresApiKey !== false && !data.apiKey) { + toast.error('API key is required'); + return; + } // Use user-provided baseUrl (allows customization of preset URLs) const finalData = { ...data, + // Use provided API key, or empty string if not provided (for optional auth providers) + apiKey: data.apiKey || '', }; try { await createMutation.mutateAsync(finalData); @@ -367,17 +374,27 @@ export function ProfileCreateDialog({ )} - {/* API Key */} + {/* API Key - optional for presets that don't require it */}