diff --git a/ui/src/components/profiles/index.ts b/ui/src/components/profiles/index.ts
index db201102..432cbca3 100644
--- a/ui/src/components/profiles/index.ts
+++ b/ui/src/components/profiles/index.ts
@@ -15,6 +15,9 @@ export type { Settings, SettingsResponse, ProfileEditorProps } from './editor';
// OpenRouter components
export { OpenRouterBadge } from './openrouter-badge';
+export { OpenRouterBanner } from './openrouter-banner';
export { OpenRouterModelPicker } from './openrouter-model-picker';
+export { OpenRouterPromoCard } from './openrouter-promo-card';
+export { OpenRouterQuickStart } from './openrouter-quick-start';
export { ModelTierMapping } from './model-tier-mapping';
export type { TierMapping } from './model-tier-mapping';
diff --git a/ui/src/components/profiles/openrouter-banner.tsx b/ui/src/components/profiles/openrouter-banner.tsx
new file mode 100644
index 00000000..1a34f287
--- /dev/null
+++ b/ui/src/components/profiles/openrouter-banner.tsx
@@ -0,0 +1,84 @@
+/**
+ * OpenRouter Feature Banner
+ * Dismissible announcement banner for OpenRouter integration
+ */
+
+/* eslint-disable react-hooks/set-state-in-effect */
+import { useState, useEffect } from 'react';
+import { X, Sparkles, ExternalLink } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { useOpenRouterReady } from '@/hooks/use-openrouter-models';
+
+const BANNER_DISMISSED_KEY = 'ccs:openrouter-banner-dismissed';
+
+interface OpenRouterBannerProps {
+ onCreateClick?: () => void;
+}
+
+export function OpenRouterBanner({ onCreateClick }: OpenRouterBannerProps) {
+ const [dismissed, setDismissed] = useState(true); // Start hidden to avoid flash
+ const { modelCount, isLoading } = useOpenRouterReady();
+
+ // Check localStorage on mount
+ useEffect(() => {
+ const isDismissed = localStorage.getItem(BANNER_DISMISSED_KEY) === 'true';
+ setDismissed(isDismissed);
+ }, []);
+
+ const handleDismiss = () => {
+ localStorage.setItem(BANNER_DISMISSED_KEY, 'true');
+ setDismissed(true);
+ };
+
+ if (dismissed) return null;
+
+ return (
+
+
+
+
+
+
+
+
NEW: OpenRouter Integration
+
+ Browse {isLoading ? '300+' : `${modelCount}+`} models from OpenAI, Anthropic, Google,
+ Meta and more.
+
+
+
+
+
+ {onCreateClick && (
+
+ Try it now
+
+ )}
+
+ Learn more
+
+
+
+
+ Dismiss
+
+
+
+
+ );
+}
diff --git a/ui/src/components/profiles/openrouter-promo-card.tsx b/ui/src/components/profiles/openrouter-promo-card.tsx
new file mode 100644
index 00000000..881f3368
--- /dev/null
+++ b/ui/src/components/profiles/openrouter-promo-card.tsx
@@ -0,0 +1,41 @@
+/**
+ * OpenRouter Promo Card
+ * Permanent promotional card for OpenRouter - always visible in sidebar footer
+ */
+
+import { Button } from '@/components/ui/button';
+import { useOpenRouterReady } from '@/hooks/use-openrouter-models';
+import { Zap } from 'lucide-react';
+
+interface OpenRouterPromoCardProps {
+ onCreateClick: () => void;
+}
+
+export function OpenRouterPromoCard({ onCreateClick }: OpenRouterPromoCardProps) {
+ const { modelCount, isLoading } = useOpenRouterReady();
+
+ return (
+
+
+
+
+
+
+
OpenRouter
+
+ {isLoading ? '300+' : `${modelCount}+`} models available
+
+
+
+
+ Add
+
+
+
+ );
+}
diff --git a/ui/src/components/profiles/openrouter-quick-start.tsx b/ui/src/components/profiles/openrouter-quick-start.tsx
new file mode 100644
index 00000000..c320bbbc
--- /dev/null
+++ b/ui/src/components/profiles/openrouter-quick-start.tsx
@@ -0,0 +1,98 @@
+/**
+ * OpenRouter Quick Start Card
+ * Prominent CTA for new users to create OpenRouter profile
+ */
+
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+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';
+
+interface OpenRouterQuickStartProps {
+ onOpenRouterClick: () => void;
+ onCustomClick: () => void;
+}
+
+export function OpenRouterQuickStart({
+ onOpenRouterClick,
+ onCustomClick,
+}: OpenRouterQuickStartProps) {
+ const { modelCount, isLoading } = useOpenRouterReady();
+
+ return (
+
+
+ {/* Main OpenRouter Card */}
+
+
+
+
+
+
+
+ Recommended
+
+
+ Start with OpenRouter
+
+ Access {isLoading ? '300+' : `${modelCount}+`} models from OpenAI, Anthropic, Google,
+ Meta and more - all through one API.
+
+
+
+ {/* Key Features */}
+
+
+
+ One API, all providers
+
+
+
+ Model tier mapping
+
+
+
+
+ Create OpenRouter Profile
+
+
+
+
+ Get your API key at{' '}
+
+ openrouter.ai/keys
+
+
+
+
+
+
+ {/* Divider */}
+
+
+ or
+
+
+
+ {/* Custom Option */}
+
+ Create Custom API Profile
+
+
+
+ );
+}
diff --git a/ui/src/lib/provider-presets.ts b/ui/src/lib/provider-presets.ts
new file mode 100644
index 00000000..8a0ade76
--- /dev/null
+++ b/ui/src/lib/provider-presets.ts
@@ -0,0 +1,85 @@
+/**
+ * Provider Presets Configuration
+ * Pre-configured templates for common API providers
+ */
+
+export interface ProviderPreset {
+ id: string;
+ name: string;
+ description: string;
+ baseUrl: string;
+ defaultProfileName: string;
+ badge?: string;
+ featured?: boolean;
+ icon?: string;
+ defaultModel?: string;
+ requiresApiKey: boolean;
+ apiKeyPlaceholder: string;
+ apiKeyHint?: string;
+}
+
+export const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
+
+export const PROVIDER_PRESETS: ProviderPreset[] = [
+ {
+ id: 'openrouter',
+ name: 'OpenRouter',
+ description: '349+ models from OpenAI, Anthropic, Google, Meta',
+ baseUrl: OPENROUTER_BASE_URL,
+ defaultProfileName: 'openrouter',
+ badge: '349+ models',
+ featured: true,
+ icon: '/icons/openrouter.svg',
+ defaultModel: 'anthropic/claude-sonnet-4',
+ requiresApiKey: true,
+ apiKeyPlaceholder: 'sk-or-...',
+ apiKeyHint: 'Get your API key at openrouter.ai/keys',
+ },
+ {
+ id: 'glm',
+ name: 'GLM',
+ description: 'Claude via Z.AI (GitHub Copilot)',
+ baseUrl: 'https://api.z.ai/api/anthropic',
+ defaultProfileName: 'glm',
+ badge: 'Free',
+ defaultModel: 'glm-4.6',
+ requiresApiKey: true,
+ apiKeyPlaceholder: 'ghp_...',
+ apiKeyHint: 'Get your API key from Z.AI',
+ },
+ {
+ id: 'glmt',
+ name: 'GLMT',
+ description: 'GLM with Thinking mode support',
+ baseUrl: 'https://api.z.ai/api/coding/paas/v4/chat/completions',
+ defaultProfileName: 'glmt',
+ badge: 'Thinking',
+ defaultModel: 'glm-4.6',
+ requiresApiKey: true,
+ apiKeyPlaceholder: 'ghp_...',
+ apiKeyHint: 'Same API key as GLM',
+ },
+ {
+ id: 'kimi',
+ name: 'Kimi',
+ description: 'Moonshot AI - Fast reasoning model',
+ baseUrl: 'https://api.kimi.com/coding/',
+ defaultProfileName: 'kimi',
+ badge: 'Reasoning',
+ defaultModel: 'kimi-k2-thinking-turbo',
+ requiresApiKey: true,
+ apiKeyPlaceholder: 'sk-...',
+ apiKeyHint: 'Get your API key from Moonshot AI',
+ },
+];
+
+/** Get preset by ID */
+export function getPresetById(id: string): ProviderPreset | undefined {
+ return PROVIDER_PRESETS.find((p) => p.id === id);
+}
+
+/** Check if a URL matches a known preset */
+export function detectPresetFromUrl(baseUrl: string): ProviderPreset | undefined {
+ const normalizedUrl = baseUrl.toLowerCase().trim();
+ return PROVIDER_PRESETS.find((p) => normalizedUrl.includes(p.baseUrl.toLowerCase()));
+}