diff --git a/README.md b/README.md index 45aec431..8eecda7c 100644 --- a/README.md +++ b/README.md @@ -109,10 +109,14 @@ 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 | -| **Qwen** | API Key | `ccs qwen` | Alibaba Cloud, qwen3-coder | +| **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 | **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. +**Alibaba Coding Plan Integration**: Configure via `ccs api create --preset alibaba-coding-plan` (or preset alias `alibaba`) with Coding Plan keys (`sk-sp-...`) and endpoint `https://coding-intl.dashscope.aliyuncs.com/apps/anthropic`. + **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). @@ -143,9 +147,11 @@ ccs cursor # Cursor IDE integration (token import + local daemon) ccs kiro # Kiro/AWS CodeWhisperer (OAuth) ccs ghcp # GitHub Copilot (OAuth device flow) ccs agy # Antigravity (OAuth) +ccs qwen # Qwen Code (OAuth via CLIProxy) ccs ollama # Local Ollama (no API key needed) ccs glm # GLM (API key) ccs km # Kimi API profile (API key) +ccs api create --preset alibaba-coding-plan # Alibaba Coding Plan profile ``` ### Droid Alias (`argv[0]` pattern) diff --git a/src/commands/api-command.ts b/src/commands/api-command.ts index cdbb2f43..f44f6f49 100644 --- a/src/commands/api-command.ts +++ b/src/commands/api-command.ts @@ -659,6 +659,8 @@ async function showHelp(): Promise { console.log(''); console.log(` ${dim('# Quick setup with preset')}`); console.log(` ${color('ccs api create --preset openrouter', 'command')}`); + console.log(` ${color('ccs api create --preset alibaba-coding-plan', 'command')}`); + console.log(` ${color('ccs api create --preset alibaba', 'command')} ${dim('# alias')}`); console.log(` ${color('ccs api create --preset glm', 'command')}`); console.log(''); console.log(` ${dim('# Create with name')}`); diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 25c53208..8c849d36 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -133,6 +133,10 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['ccs glm', 'GLM 5 (API key required)'], ['ccs glmt', 'GLM with thinking mode'], ['ccs km', 'Kimi for Coding (API key)'], + [ + 'ccs api create --preset alibaba-coding-plan', + 'Alibaba Coding Plan (Anthropic-compatible API key)', + ], ['ccs ollama', 'Local Ollama (http://localhost:11434)'], ['ccs ollama-cloud', 'Ollama Cloud (API key required)'], ['', ''], // Spacer @@ -184,7 +188,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); ['ccs gemini', 'Google Gemini (gemini-2.5-pro or 3-pro)'], ['ccs codex', 'OpenAI Codex (supports -medium/-high/-xhigh model suffixes)'], ['ccs agy', 'Antigravity (Claude/Gemini models)'], - ['ccs qwen', 'Qwen Code (qwen3-coder)'], + ['ccs qwen', 'Qwen Code OAuth (CLIProxy)'], ['ccs kimi', 'Kimi (Moonshot AI K2/K2.5 models)'], ['ccs kiro', 'Kiro (AWS CodeWhisperer Claude models)'], ['ccs ghcp', 'GitHub Copilot (OAuth via CLIProxy Plus)'], diff --git a/src/shared/provider-preset-catalog.ts b/src/shared/provider-preset-catalog.ts index b6c017e9..52f0eedc 100644 --- a/src/shared/provider-preset-catalog.ts +++ b/src/shared/provider-preset-catalog.ts @@ -9,6 +9,7 @@ export type PresetCategory = 'recommended' | 'alternative'; export const PROVIDER_PRESET_IDS = [ 'openrouter', + 'alibaba-coding-plan', 'ollama', 'glm', 'glmt', @@ -51,6 +52,8 @@ export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api'; */ export const PROVIDER_PRESET_ALIASES: Readonly> = Object.freeze({ kimi: 'km', + alibaba: 'alibaba-coding-plan', + acp: 'alibaba-coding-plan', }); const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [ @@ -69,6 +72,21 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [ featured: true, icon: '/icons/openrouter.svg', }, + { + id: 'alibaba-coding-plan', + name: 'Alibaba Coding Plan', + description: 'Alibaba Cloud Coding Plan via Anthropic-compatible endpoint', + baseUrl: 'https://coding-intl.dashscope.aliyuncs.com/apps/anthropic', + defaultProfileName: 'alibaba-plan', + defaultModel: 'qwen3-coder-plus', + apiKeyPlaceholder: 'sk-sp-...', + apiKeyHint: 'Get your Coding Plan key from Alibaba Cloud Model Studio', + category: 'recommended', + requiresApiKey: true, + badge: 'Coding Plan', + featured: true, + icon: '/assets/providers/alibabacloud-color.svg', + }, { id: 'ollama', name: 'Ollama (Local)', @@ -183,7 +201,7 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [ name: 'Qwen', description: 'Alibaba Cloud - Qwen3 models (256K-1M context, thinking support)', baseUrl: 'https://dashscope-intl.aliyuncs.com/apps/anthropic', - defaultProfileName: 'qwen', + defaultProfileName: 'qwen-api', defaultModel: 'qwen3-coder-plus', apiKeyPlaceholder: 'sk-...', apiKeyHint: 'Get your API key from Alibaba Cloud Model Studio', diff --git a/tests/unit/api/provider-presets.test.ts b/tests/unit/api/provider-presets.test.ts index b51576c2..4b7c2355 100644 --- a/tests/unit/api/provider-presets.test.ts +++ b/tests/unit/api/provider-presets.test.ts @@ -2,6 +2,22 @@ import { describe, expect, it } from 'bun:test'; import { getPresetById, isValidPresetId } from '../../../src/api/services/provider-presets'; describe('provider-presets', () => { + it('resolves Alibaba Coding Plan preset id', () => { + const preset = getPresetById('alibaba-coding-plan'); + expect(preset?.id).toBe('alibaba-coding-plan'); + expect(preset?.baseUrl).toBe('https://coding-intl.dashscope.aliyuncs.com/apps/anthropic'); + expect(preset?.defaultProfileName).toBe('alibaba-plan'); + }); + + it('resolves alibaba alias to Alibaba Coding Plan preset', () => { + const preset = getPresetById('alibaba'); + expect(preset?.id).toBe('alibaba-coding-plan'); + }); + + it('treats alibaba alias as a valid preset id', () => { + expect(isValidPresetId('alibaba')).toBe(true); + }); + it('resolves canonical km preset id', () => { const preset = getPresetById('km'); expect(preset?.id).toBe('km'); @@ -25,4 +41,9 @@ describe('provider-presets', () => { it('treats legacy kimi alias as a valid preset id', () => { expect(isValidPresetId('kimi')).toBe(true); }); + + it('uses non-reserved default profile name for qwen API preset', () => { + const preset = getPresetById('qwen'); + expect(preset?.defaultProfileName).toBe('qwen-api'); + }); }); diff --git a/ui/public/assets/providers/alibabacloud-color.svg b/ui/public/assets/providers/alibabacloud-color.svg new file mode 100644 index 00000000..56c20781 --- /dev/null +++ b/ui/public/assets/providers/alibabacloud-color.svg @@ -0,0 +1 @@ +AlibabaCloud \ No newline at end of file diff --git a/ui/src/components/layout/app-sidebar.tsx b/ui/src/components/layout/app-sidebar.tsx index d596687c..b3479645 100644 --- a/ui/src/components/layout/app-sidebar.tsx +++ b/ui/src/components/layout/app-sidebar.tsx @@ -79,7 +79,7 @@ function buildNavGroups(t: (key: string) => string): SidebarGroupDef[] { path: '/providers', icon: Key, label: t('nav.apiProfiles'), - badge: { text: 'OpenRouter', icon: '/icons/openrouter.svg' }, + badge: { text: 'Featured', icon: '/icons/openrouter.svg' }, }, { path: '/cliproxy', diff --git a/ui/src/components/profiles/alibaba-coding-plan-promo-card.tsx b/ui/src/components/profiles/alibaba-coding-plan-promo-card.tsx new file mode 100644 index 00000000..cd13282f --- /dev/null +++ b/ui/src/components/profiles/alibaba-coding-plan-promo-card.tsx @@ -0,0 +1,43 @@ +/** + * Alibaba Coding Plan Promo Card + * Permanent promotional card for Alibaba Coding Plan in providers sidebar. + */ + +import { Button } from '@/components/ui/button'; +import { CloudCog } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; + +interface AlibabaCodingPlanPromoCardProps { + onCreateClick: () => void; +} + +export function AlibabaCodingPlanPromoCard({ onCreateClick }: AlibabaCodingPlanPromoCardProps) { + const { t } = useTranslation(); + + return ( +
+
+
+ +
+
+

+ {t('alibabaCodingPlanPromo.title')} +

+

+ {t('alibabaCodingPlanPromo.subtitle')} +

+
+ +
+
+ ); +} diff --git a/ui/src/components/profiles/index.ts b/ui/src/components/profiles/index.ts index 432cbca3..d3724660 100644 --- a/ui/src/components/profiles/index.ts +++ b/ui/src/components/profiles/index.ts @@ -19,5 +19,6 @@ export { OpenRouterBanner } from './openrouter-banner'; export { OpenRouterModelPicker } from './openrouter-model-picker'; export { OpenRouterPromoCard } from './openrouter-promo-card'; export { OpenRouterQuickStart } from './openrouter-quick-start'; +export { AlibabaCodingPlanPromoCard } from './alibaba-coding-plan-promo-card'; export { ModelTierMapping } from './model-tier-mapping'; export type { TierMapping } from './model-tier-mapping'; diff --git a/ui/src/components/profiles/openrouter-quick-start.tsx b/ui/src/components/profiles/openrouter-quick-start.tsx index 5cdf62c7..76c75c88 100644 --- a/ui/src/components/profiles/openrouter-quick-start.tsx +++ b/ui/src/components/profiles/openrouter-quick-start.tsx @@ -8,16 +8,18 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; import { useOpenRouterReady } from '@/hooks/use-openrouter-models'; -import { Sparkles, ExternalLink, ArrowRight, Zap } from 'lucide-react'; +import { Sparkles, ExternalLink, ArrowRight, Zap, CloudCog, KeyRound } from 'lucide-react'; import { useTranslation } from 'react-i18next'; interface OpenRouterQuickStartProps { onOpenRouterClick: () => void; + onAlibabaCodingPlanClick: () => void; onCustomClick: () => void; } export function OpenRouterQuickStart({ onOpenRouterClick, + onAlibabaCodingPlanClick, onCustomClick, }: OpenRouterQuickStartProps) { const { t } = useTranslation(); @@ -84,6 +86,65 @@ export function OpenRouterQuickStart({ + {/* Alibaba Coding Plan Card */} + + +
+
+ Alibaba Coding Plan +
+ + {t('alibabaCodingPlanQuickStart.recommended')} + +
+ {t('alibabaCodingPlanQuickStart.title')} + + {t('alibabaCodingPlanQuickStart.description')} + +
+ +
+
+ + {t('alibabaCodingPlanQuickStart.featureEndpoint')} +
+
+ + {t('alibabaCodingPlanQuickStart.featureKeyFormat')} +
+
+ + + +

+ {t('alibabaCodingPlanQuickStart.readGuideAt')}{' '} + + Alibaba Cloud Model Studio + + +

+
+
+ {/* Divider */}
diff --git a/ui/src/components/profiles/profile-create-dialog.tsx b/ui/src/components/profiles/profile-create-dialog.tsx index ff9fe648..0131b28a 100644 --- a/ui/src/components/profiles/profile-create-dialog.tsx +++ b/ui/src/components/profiles/profile-create-dialog.tsx @@ -72,7 +72,7 @@ interface ProfileCreateDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onSuccess: (name: string) => void; - initialMode?: 'normal' | 'openrouter'; + initialMode?: 'normal' | 'openrouter' | 'alibaba-coding-plan'; } // Common URL mistakes to warn about @@ -180,7 +180,8 @@ export function ProfileCreateDialog({ setSelectedPreset(CUSTOM_PRESET_ID); applyPresetToForm(null); } else { - const defaultPreset = getPresetById(DEFAULT_PRESET_ID); + const presetId = initialMode === 'openrouter' ? DEFAULT_PRESET_ID : initialMode; + const defaultPreset = getPresetById(presetId); if (defaultPreset) { setSelectedPreset(defaultPreset.id); applyPresetToForm(defaultPreset); diff --git a/ui/src/lib/i18n.ts b/ui/src/lib/i18n.ts index 7738cb44..41f1268c 100644 --- a/ui/src/lib/i18n.ts +++ b/ui/src/lib/i18n.ts @@ -32,7 +32,7 @@ const resources = { system: 'System', health: 'Health', settings: 'Settings', - openrouterTooltip: '349+ models via OpenRouter', + openrouterTooltip: 'Featured: OpenRouter + Alibaba Coding Plan', }, home: { profiles: 'Profiles', @@ -240,6 +240,21 @@ const resources = { or: 'or', createCustomProfile: 'Create Custom API Profile', }, + alibabaCodingPlanQuickStart: { + recommended: 'Recommended', + title: 'Try Alibaba Coding Plan', + description: + 'Use Alibaba Cloud Coding Plan through an Anthropic-compatible endpoint for coding workloads.', + featureEndpoint: 'Anthropic-compatible endpoint', + featureKeyFormat: 'Dedicated sk-sp API keys', + createAlibabaProfile: 'Create Alibaba Coding Plan Profile', + readGuideAt: 'Read setup guide at', + }, + alibabaCodingPlanPromo: { + title: 'Alibaba Coding Plan', + subtitle: 'Model Studio Coding Plan via Anthropic endpoint', + add: 'Add', + }, credentialHealth: { title: 'Credential Health', ready: 'Ready', @@ -1164,7 +1179,7 @@ const resources = { system: '系统', health: '健康', settings: '设置', - openrouterTooltip: '通过 OpenRouter 提供 349+ 模型', + openrouterTooltip: '精选:OpenRouter + Alibaba Coding Plan', }, home: { profiles: '配置', @@ -1368,6 +1383,20 @@ const resources = { or: '或', createCustomProfile: '创建自定义 API 配置', }, + alibabaCodingPlanQuickStart: { + recommended: '推荐', + title: '试用阿里云 Coding Plan', + description: '通过 Anthropic 兼容端点接入阿里云 Model Studio Coding Plan。', + featureEndpoint: 'Anthropic 兼容端点', + featureKeyFormat: '专用 sk-sp API Key', + createAlibabaProfile: '创建 Alibaba Coding Plan 配置', + readGuideAt: '查看配置文档:', + }, + alibabaCodingPlanPromo: { + title: 'Alibaba Coding Plan', + subtitle: '通过 Anthropic 端点接入 Model Studio Coding Plan', + add: '添加', + }, credentialHealth: { title: '凭据健康状态', ready: '就绪', @@ -2254,7 +2283,7 @@ const resources = { system: 'Hệ thống', health: 'Sức khỏe', settings: 'Cài đặt', - openrouterTooltip: 'Hơn 349 mô hình qua OpenRouter', + openrouterTooltip: 'Nổi bật: OpenRouter + Alibaba Coding Plan', }, home: { profiles: 'Hồ sơ', @@ -2464,6 +2493,21 @@ const resources = { or: 'hoặc', createCustomProfile: 'Tạo hồ sơ API tùy chỉnh', }, + alibabaCodingPlanQuickStart: { + recommended: 'Đề xuất', + title: 'Dùng thử Alibaba Coding Plan', + description: + 'Sử dụng Alibaba Cloud Coding Plan qua endpoint tương thích Anthropic cho tác vụ lập trình.', + featureEndpoint: 'Endpoint tương thích Anthropic', + featureKeyFormat: 'Khóa API sk-sp chuyên dụng', + createAlibabaProfile: 'Tạo hồ sơ Alibaba Coding Plan', + readGuideAt: 'Xem hướng dẫn tại', + }, + alibabaCodingPlanPromo: { + title: 'Alibaba Coding Plan', + subtitle: 'Model Studio Coding Plan qua endpoint Anthropic', + add: 'Thêm', + }, credentialHealth: { title: 'Tình trạng thông tin xác thực', ready: 'Sẵn sàng', diff --git a/ui/src/pages/api.tsx b/ui/src/pages/api.tsx index 3f6c88df..bbc0384f 100644 --- a/ui/src/pages/api.tsx +++ b/ui/src/pages/api.tsx @@ -1,8 +1,3 @@ -/** - * API Profiles Page - Master-Detail Layout - * Comprehensive profile management with inline editing - */ - import { useState, useMemo } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -23,6 +18,7 @@ import { ProfileCreateDialog } from '@/components/profiles/profile-create-dialog import { OpenRouterBanner } from '@/components/profiles/openrouter-banner'; import { OpenRouterQuickStart } from '@/components/profiles/openrouter-quick-start'; import { OpenRouterPromoCard } from '@/components/profiles/openrouter-promo-card'; +import { AlibabaCodingPlanPromoCard } from '@/components/profiles/alibaba-coding-plan-promo-card'; import { useProfiles, useDeleteProfile } from '@/hooks/use-profiles'; import { useOpenRouterModels } from '@/hooks/use-openrouter-models'; import { ConfirmDialog } from '@/components/shared/confirm-dialog'; @@ -38,30 +34,22 @@ export function ApiPage() { const [selectedProfile, setSelectedProfile] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [isCreateDialogOpen, setCreateDialogOpen] = useState(false); - const [createMode, setCreateMode] = useState<'normal' | 'openrouter'>('normal'); + const [createMode, setCreateMode] = useState<'normal' | 'openrouter' | 'alibaba-coding-plan'>( + 'normal' + ); const [deleteConfirm, setDeleteConfirm] = useState(null); const [editorHasChanges, setEditorHasChanges] = useState(false); const [pendingSwitch, setPendingSwitch] = useState(null); - // Prefetch OpenRouter models when page loads (lazy - won't block render) useOpenRouterModels(); - - // Memoize profiles to maintain stable reference const profiles = useMemo(() => data?.profiles || [], [data?.profiles]); - - // Filter profiles by search const filteredProfiles = useMemo( () => profiles.filter((p) => p.name.toLowerCase().includes(searchQuery.toLowerCase())), [profiles, searchQuery] ); - - // selectedProfile is null by default - user must click to select - // This allows OpenRouterQuickStart to show as the default right panel const selectedProfileData = selectedProfile ? profiles.find((p) => p.name === selectedProfile) : null; - - // Handle profile deletion const handleDelete = (name: string) => { deleteMutation.mutate(name, { onSuccess: () => { @@ -73,18 +61,14 @@ export function ApiPage() { }); }; - // Handle create success const handleCreateSuccess = (name: string) => { setCreateDialogOpen(false); - // Use the same unsaved changes check as profile selection if (editorHasChanges && selectedProfile !== null) { setPendingSwitch(name); } else { setSelectedProfile(name); } }; - - // Handle profile selection with unsaved changes check const handleProfileSelect = (name: string) => { if (editorHasChanges && selectedProfile !== name) { setPendingSwitch(name); @@ -95,14 +79,9 @@ export function ApiPage() { return (
- {/* OpenRouter Announcement Banner */} setCreateDialogOpen(true)} /> - - {/* Main Content */}
- {/* Left Panel - Profiles List */}
- {/* Header */}
@@ -120,7 +99,6 @@ export function ApiPage() {
- {/* Search */}
- {/* Profile List */} {isLoading ? (
@@ -197,7 +174,6 @@ export function ApiPage() { )} - {/* Footer Stats */} {profiles.length > 0 && (
@@ -212,16 +188,20 @@ export function ApiPage() {
)} - {/* OpenRouter Promo - always visible */} { setCreateMode('openrouter'); setCreateDialogOpen(true); }} /> + { + setCreateMode('alibaba-coding-plan'); + setCreateDialogOpen(true); + }} + />
- {/* Right Panel - Editor or QuickStart */}
{selectedProfileData ? ( { + setCreateMode('alibaba-coding-plan'); + setCreateDialogOpen(true); + }} onCustomClick={() => { setCreateMode('normal'); setCreateDialogOpen(true); @@ -246,7 +230,6 @@ export function ApiPage() {
- {/* Create Dialog */} - {/* Delete Confirmation */} setDeleteConfirm(null)} /> - {/* Unsaved Changes Confirmation */} - {/* Status indicator */} {profile.configured ? ( ) : ( )} - {/* Profile info */}
{profile.name}
@@ -335,7 +313,6 @@ function ProfileListItem({
- {/* Actions */}