diff --git a/.env.example b/.env.example index 95e5a1b..c039120 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,18 @@ -# LLM — use LiteLLM model string format (provider/model) -# OpenRouter (default — 200+ models via one key) -LLM_MODEL=openrouter/openai/gpt-4o -OPENROUTER_API_KEY=sk-or-... +# ─── LLM provider ───────────────────────────────────────────────────────────── +# Pick ONE option. Uses LiteLLM model string format (provider/model). +# +# Option A — Claude Code (zero-config): if the `claude` CLI is installed and logged +# in, it is auto-detected and your subscription is used as the LLM. For this to work, +# leave LLM_MODEL, OPENROUTER_API_KEY, ANTHROPIC_API_KEY and LLM_API_BASE UNSET below. +# Set CLAUDE_CODE_AUTODETECT=false to turn detection off. +# CLAUDE_CODE_AUTODETECT=true + +# Option B — OpenRouter (200+ models via one key). Uncomment both lines to use it +# (this takes priority over Claude Code auto-detection). +# LLM_MODEL=openrouter/openai/gpt-4o +# OPENROUTER_API_KEY=sk-or-... -# Anthropic direct +# Anthropic direct (API key, not subscription) # LLM_MODEL=anthropic/claude-3-5-sonnet-20241022 # ANTHROPIC_API_KEY=sk-ant-... @@ -24,6 +33,10 @@ OPENROUTER_API_KEY=sk-or-... # LLM_API_BASE=https://your-endpoint/v1 # LLM_API_KEY=your-key +# Cloud provider — selects the active provider (aws | azure | gcp). Default: aws. +# Azure and GCP are stubs; only aws is implemented today. +# CLOUD_PROVIDER=aws + # AWS (use IAM role or explicit keys) AWS_REGION=us-east-1 AWS_PROFILE=devops-agent-readonly # optional — use named profile (local dev) diff --git a/docs/llm_providers.md b/docs/llm_providers.md index 4a2f070..0a504e0 100644 --- a/docs/llm_providers.md +++ b/docs/llm_providers.md @@ -22,6 +22,32 @@ LiteLLM routes to Anthropic API → same agent graph, same tools ## Supported providers (examples) +### Claude Code (zero-config, uses your subscription) + +If you have [Claude Code](https://claude.ai/code) installed and logged in, OpenDevOps can use your **existing Claude subscription** as the LLM — no API key, no `.env` changes. + +> **Important — leave these unset.** Auto-detection only activates when you have **not** explicitly configured another provider. To use Claude Code, make sure your `.env` does **not** set any of: +> - `LLM_MODEL` +> - `OPENROUTER_API_KEY` +> - `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` +> - `LLM_API_BASE` +> +> If any of those are present, that provider wins and Claude Code is skipped. The simplest setup is an empty (or absent) `.env` — Claude Code is picked up automatically. + +**How it works:** +1. On startup the app checks whether the `claude` CLI is in your `PATH`. +2. It reads `~/.claude/settings.json` for your selected model (e.g. `sonnet` → `anthropic/claude-sonnet-4-6`). +3. It reuses Claude Code's subscription login — the OAuth token in `~/.claude/.credentials.json` (`sk-ant-oat…`). LiteLLM sends it as a Bearer token with the OAuth beta header automatically. + +The active backend is shown in **Settings → Environment** and in the **setup wizard**. + +**Notes:** +- Rate limits are shared with your interactive Claude Code usage — both draw from the same subscription quota. +- The OAuth token expires periodically; running any `claude` command refreshes it. +- To force a different provider even when Claude Code is installed, set `LLM_MODEL` + the matching key, or disable detection entirely with `CLAUDE_CODE_AUTODETECT=false`. + +--- + ### OpenRouter (default — 200+ models via one key) ```bash LLM_MODEL=openrouter/openai/gpt-4o diff --git a/frontend/src/components/LlmBackendCard.tsx b/frontend/src/components/LlmBackendCard.tsx new file mode 100644 index 0000000..bd3fa7c --- /dev/null +++ b/frontend/src/components/LlmBackendCard.tsx @@ -0,0 +1,50 @@ +import { CheckCircle, XCircle, AlertTriangle, Cpu } from 'lucide-react'; +import { cn } from '../lib/utils'; +import type { LlmBackendInfo } from '../types'; + +interface Props { + backend: LlmBackendInfo; + className?: string; +} + +const SOURCE_STYLE: Record = { + claude_code: { color: 'text-amber-700 dark:text-amber-300', bg: 'bg-amber-50 dark:bg-amber-500/10', border: 'border-amber-200 dark:border-amber-500/20' }, + anthropic: { color: 'text-orange-700 dark:text-orange-300', bg: 'bg-orange-50 dark:bg-orange-500/10', border: 'border-orange-200 dark:border-orange-500/20' }, + openrouter: { color: 'text-purple-700 dark:text-purple-300', bg: 'bg-purple-50 dark:bg-purple-500/10', border: 'border-purple-200 dark:border-purple-500/20' }, + openai: { color: 'text-green-700 dark:text-green-300', bg: 'bg-green-50 dark:bg-green-500/10', border: 'border-green-200 dark:border-green-500/20' }, + groq: { color: 'text-blue-700 dark:text-blue-300', bg: 'bg-blue-50 dark:bg-blue-500/10', border: 'border-blue-200 dark:border-blue-500/20' }, + custom: { color: 'text-indigo-700 dark:text-indigo-300', bg: 'bg-indigo-50 dark:bg-indigo-500/10', border: 'border-indigo-200 dark:border-indigo-500/20' }, + claude_code_no_auth: { color: 'text-amber-700 dark:text-amber-300', bg: 'bg-amber-50 dark:bg-amber-500/10', border: 'border-amber-200 dark:border-amber-500/20' }, + default: { color: 'text-gray-500 dark:text-[#71717A]', bg: 'bg-gray-50 dark:bg-[#18181B]', border: 'border-gray-200 dark:border-[#27272A]' }, +}; + +function StatusIcon({ configured, source }: { configured: boolean; source: string }) { + if (source === 'claude_code_no_auth') return ; + if (source === 'default') return ; + if (configured) return ; + return ; +} + +export default function LlmBackendCard({ backend, className }: Props) { + const style = SOURCE_STYLE[backend.source] ?? SOURCE_STYLE.default; + + return ( +
+
+
+ +
+
+
+ + {backend.display_name} +
+

{backend.detail}

+ {backend.configured && ( +

{backend.model}

+ )} +
+
+
+ ); +} diff --git a/frontend/src/pages/InitPage.tsx b/frontend/src/pages/InitPage.tsx index e07c18c..5688197 100644 --- a/frontend/src/pages/InitPage.tsx +++ b/frontend/src/pages/InitPage.tsx @@ -4,6 +4,8 @@ import { ArrowRight, ArrowLeft, Loader2, Terminal, Eye, EyeOff, CheckCircle, XCi import { cn } from '../lib/utils'; import { useAuth } from '../context/AuthContext'; import { apiFetch } from '../lib/api'; +import LlmBackendCard from '../components/LlmBackendCard'; +import type { LlmBackendInfo } from '../types'; const inp = 'w-full text-base text-gray-900 dark:text-white bg-gray-50 dark:bg-[#18181B] border border-gray-200 dark:border-[#27272A] rounded-xl px-4 py-3.5 outline-none focus:border-indigo-500 dark:focus:border-indigo-400 focus:ring-2 focus:ring-indigo-500/10 transition-all placeholder:text-gray-400 dark:placeholder:text-[#52525B]'; @@ -56,7 +58,8 @@ export default function InitPage() { const [showPw, setShowPw] = useState(false); // Step 2 - const [region, setRegion] = useState('us-east-1'); + const [region, setRegion] = useState('us-east-1'); + const [llmBackend, setLlmBackend] = useState(null); // Step 3 const [perms, setPerms] = useState>({}); @@ -77,6 +80,7 @@ export default function InitPage() { .then(r => r.json()) .then(d => { setRegion(d.aws_region || 'us-east-1'); + if (d.llm_backend) setLlmBackend(d.llm_backend as LlmBackendInfo); if (d.initialized && (d.has_user || !d.auth_enabled)) { navigate('/', { replace: true }); return; @@ -252,9 +256,25 @@ export default function InitPage() { {step === 2 && ( <>

AWS Configuration

-

+

Configure the AWS region and optional SNS topic for alert delivery.

+ + {/* LLM Backend status */} + {llmBackend && ( +
+

LLM Backend

+ + {!llmBackend.configured && ( +

+ You can set LLM_MODEL and an API key in .env, or install{' '} + Claude Code{' '} + for zero-config setup. See docs/llm_providers.md. +

+ )} +
+ )} +