From 69f056e4a7b4f3f8c2400ba98d5d963dcac40f27 Mon Sep 17 00:00:00 2001 From: AhmadHammad21 Date: Fri, 22 May 2026 15:56:26 +0300 Subject: [PATCH 1/3] feat(llm): add Claude Code subscription backend and isolate AWS into providers - Auto-detect the Claude Code CLI and use its subscription OAuth login as the Anthropic LLM backend (no API key needed). LiteLLM handles sk-ant-oat tokens; system prompts are shaped with the required Claude Code identity block. - Add agent/llm/ package: backend resolver + pluggable detector registry. - Surface the active LLM backend in the init wizard and Settings. - Restructure cloud-specific code under providers/ behind a CloudProvider protocol; move AWS tools/context/permissions/poller/events into providers/aws/. - Add CLOUD_PROVIDER setting (aws|azure|gcp); azure/gcp are stubs. --- .env.example | 10 ++ docs/llm_providers.md | 18 +++ frontend/src/components/LlmBackendCard.tsx | 50 ++++++ frontend/src/pages/InitPage.tsx | 24 ++- frontend/src/pages/SettingsPage.tsx | 12 +- frontend/src/types.ts | 9 ++ src/agent/core.py | 55 ++++--- src/agent/llm/__init__.py | 23 +++ src/agent/llm/backend.py | 149 ++++++++++++++++++ src/agent/llm/detectors/__init__.py | 14 ++ src/agent/llm/detectors/base.py | 34 ++++ src/agent/llm/detectors/claude_code.py | 131 +++++++++++++++ src/agent/llm/identity.py | 31 ++++ src/agent/summarizer.py | 8 +- src/api/app.py | 8 +- src/api/routers/chat.py | 5 +- src/api/routers/init.py | 11 +- src/api/routers/settings.py | 3 +- src/cli/ask.py | 11 +- src/cli/report.py | 2 +- src/config/appsettings.py | 9 ++ src/providers/__init__.py | 24 +++ src/providers/aws/__init__.py | 47 ++++++ .../aws/context.py} | 0 .../aws}/event_consumer.py | 2 +- src/{agent => providers/aws}/event_infra.py | 0 .../aws/permissions.py} | 0 src/{agent => providers/aws}/poller.py | 6 +- src/providers/aws/tools/__init__.py | 1 + src/{ => providers/aws}/tools/cloudtrail.py | 0 src/{ => providers/aws}/tools/cloudwatch.py | 0 src/{ => providers/aws}/tools/ec2.py | 0 src/{ => providers/aws}/tools/ecs.py | 0 src/{ => providers/aws}/tools/iam.py | 0 src/{ => providers/aws}/tools/lambda_.py | 0 src/{ => providers/aws}/tools/rds.py | 0 src/providers/azure/__init__.py | 35 ++++ src/providers/base.py | 36 +++++ src/providers/gcp/__init__.py | 35 ++++ tests/integration/test_dlq_logic.py | 18 +-- tests/integration/test_poller.py | 8 +- tests/test_tools/test_cloudwatch.py | 2 +- tests/test_tools/test_ecs.py | 2 +- tests/test_tools/test_lambda.py | 2 +- tests/test_tools/test_rds.py | 2 +- 45 files changed, 770 insertions(+), 67 deletions(-) create mode 100644 frontend/src/components/LlmBackendCard.tsx create mode 100644 src/agent/llm/__init__.py create mode 100644 src/agent/llm/backend.py create mode 100644 src/agent/llm/detectors/__init__.py create mode 100644 src/agent/llm/detectors/base.py create mode 100644 src/agent/llm/detectors/claude_code.py create mode 100644 src/agent/llm/identity.py create mode 100644 src/providers/__init__.py create mode 100644 src/providers/aws/__init__.py rename src/{agent/context_collectors.py => providers/aws/context.py} (100%) rename src/{agent => providers/aws}/event_consumer.py (99%) rename src/{agent => providers/aws}/event_infra.py (100%) rename src/{agent/permission_checker.py => providers/aws/permissions.py} (100%) rename src/{agent => providers/aws}/poller.py (98%) create mode 100644 src/providers/aws/tools/__init__.py rename src/{ => providers/aws}/tools/cloudtrail.py (100%) rename src/{ => providers/aws}/tools/cloudwatch.py (100%) rename src/{ => providers/aws}/tools/ec2.py (100%) rename src/{ => providers/aws}/tools/ecs.py (100%) rename src/{ => providers/aws}/tools/iam.py (100%) rename src/{ => providers/aws}/tools/lambda_.py (100%) rename src/{ => providers/aws}/tools/rds.py (100%) create mode 100644 src/providers/azure/__init__.py create mode 100644 src/providers/base.py create mode 100644 src/providers/gcp/__init__.py diff --git a/.env.example b/.env.example index 95e5a1b..8914953 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,12 @@ LLM_MODEL=openrouter/openai/gpt-4o OPENROUTER_API_KEY=sk-or-... +# Claude Code auto-detection (default: true) +# When enabled and LLM_MODEL/API keys are not explicitly set, the app will detect +# the Claude Code CLI (~/.claude/) and use it as the Anthropic LLM backend automatically. +# Set to false to disable. +# CLAUDE_CODE_AUTODETECT=true + # Anthropic direct # LLM_MODEL=anthropic/claude-3-5-sonnet-20241022 # ANTHROPIC_API_KEY=sk-ant-... @@ -24,6 +30,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..8b57f18 100644 --- a/docs/llm_providers.md +++ b/docs/llm_providers.md @@ -22,6 +22,24 @@ LiteLLM routes to Anthropic API → same agent graph, same tools ## Supported providers (examples) +### Claude Code (zero-config, auto-detected) + +If you have [Claude Code](https://claude.ai/code) installed and authenticated, OpenDevOps detects it automatically — no `.env` changes required. + +**How it works:** +1. On startup, the app checks if the `claude` CLI is in your PATH +2. It reads `~/.claude/settings.json` for your configured model slot +3. It picks up your auth from `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN`, or Claude Code's OAuth credential file + +The active LLM backend is shown in **Settings → Environment** and in the **setup wizard**. + +To disable auto-detection: +```bash +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. +

+ )} +
+ )} +