1 line
263 KiB
JSON
1 line
263 KiB
JSON
{"repo": "cased/kit", "files": {"src/kit/pr_review/config.py": {"80009db2e11dc405d0c9e170fa1641cdedf811a3": {"t": 1785674490.1259768, "n": 18305, "h": "1c0b17609e35ec9f", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, List, Optional\n\nimport yaml # type: ignore\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n OLLAMA = \"ollama\"\n GOOGLE = \"google\"\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\ndef _detect_provider_from_model(model_name: str) -> Optional[LLMProvider]:\n \"\"\"Detect LLM provider from model name.\"\"\"\n model_lower = model_name.lower()\n\n # Strip common prefixes first\n prefixes_to_strip = [\n \"vertex_ai/\",\n \"openrouter/\",\n \"together/\",\n \"groq/\",\n \"fireworks/\",\n \"perplexity/\",\n \"replicate/\",\n \"bedrock/\",\n \"azure/\",\n ]\n\n stripped_model = model_lower\n for prefix in prefixes_to_strip:\n if stripped_model.startswith(prefix):\n stripped_model = stripped_model[len(prefix) :]\n break\n\n # OpenAI model patterns (includes GPT-5 mini/nano and O3 models)\n # Check exact matches first\n if stripped_model == \"o3\":\n return LLMProvider.OPENAI\n\n # Then check prefix/substring patterns\n openai_patterns = [\"gpt-\", \"o1-\", \"o3-\", \"text-davinci\", \"text-curie\", \"text-babbage\", \"text-ada\", \"grok-\"]\n if any(pattern in stripped_model for pattern in openai_patterns):\n return LLMProvider.OPENAI\n\n # Anthropic model patterns\n anthropic_patterns = [\"claude-\", \"haiku\", \"sonnet\", \"opus\"]\n if any(pattern in stripped_model for pattern in anthropic_patterns):\n return LLMProvider.ANTHROPIC\n\n # Google model patterns\n google_patterns = [\"gemini-\", \"gemini\", \"bison\", \"gecko\", \"palm\"]\n if any(pattern in stripped_model for pattern in google_patterns):\n return LLMProvider.GOOGLE\n\n # Ollama model patterns - popular models available in Ollama\n ollama_patterns = [\n \"llama\",\n \"mistral\",\n \"codellama\",\n \"deepseek\",\n \"qwen\",\n \"phi\",\n \"gemma\",\n \"wizardcoder\",\n \"starcoder\",\n \"codegemma\",\n \"solar\",\n \"nous-hermes\",\n \"openchat\",\n \"zephyr\",\n \"orca\",\n \"vicuna\",\n \"alpaca\",\n \"devstral\",\n ]\n if any(pattern in stripped_model for pattern in ollama_patterns):\n return LLMProvider.OLLAMA\n\n return None\n\n\ndef _is_placeholder_token(token: Optional[str]) -> bool:\n \"\"\"Check if a token is a placeholder that should be ignored.\"\"\"\n if not token:\n return True\n\n # Common placeholder patterns\n placeholder_patterns = [\n \"your_token_here\",\n \"your_api_key_here\",\n \"your_key_here\",\n \"replace_with_your_token\",\n \"sk-your_api_key_here\",\n \"ghp_your_token_here\",\n \"sk-ant-your_key\",\n ]\n\n token_lower = token.lower()\n return any(pattern in token_lower for pattern in placeholder_patterns)\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n api_base_url: Optional[str] = None # For local LLMs or custom OpenAI endpoints\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n # Output control\n quiet: bool = False # Suppress status output for plain mode\n # Priority filtering\n priority_filter: Optional[List[str]] = None # [\"high\", \"medium\", \"low\"] or subset\n max_review_size_mb: float = 5.0 # Default 5MB limit (was 1MB hardcoded)\n # Custom context profile\n profile: Optional[str] = None # Profile name to use\n profile_context: Optional[str] = None # Loaded profile context\n # Existing repository path (skips cloning when provided)\n repo_path: Optional[str] = None # Path to existing repository to use for analysis\n # Local review settings\n save_reviews: bool = False # Save local reviews to .kit/reviews/\n llm_provider: Optional[str] = None # Convenience accessor for llm.provider\n llm_model: Optional[str] = None # Convenience accessor for llm.model\n llm_api_key: Optional[str] = None # Convenience accessor for llm.api_key\n llm_api_base_url: Optional[str] = None # Convenience accessor for llm.api_base_url\n llm_max_tokens: Optional[int] = None # Convenience accessor for llm.max_tokens\n\n @classmethod\n def from_file(\n cls,\n config_path: Optional[str] = None,\n profile: Optional[str] = None,\n repo_path: Optional[str] = None,\n model_hint: Optional[str] = None,\n ) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\n\n Args:\n config_path: Path to config file\n profile: Profile name to load custom context from\n repo_path: Path to existing repository to use for analysis\n model_hint: Model name to help auto-detect provider\n \"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n config_github_token = config_data.get(\"github\", {}).get(\"token\")\n if _is_placeholder_token(config_github_token):\n config_github_token = None # Treat placeholder as missing\n\n github_token = config_github_token or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\")\n\n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {}) or {} # Handle None or empty llm section\n\n # Try to detect provider from model hint first\n detected_provider = None\n if model_hint:\n detected_provider = _detect_provider_from_model(model_hint)\n\n # Get provider from config, environment, detected, or default\n provider_str = (\n llm_data.get(\"provider\")\n or os.getenv(\"LLM_PROVIDER\")\n or (detected_provider.value if detected_provider else None)\n or \"anthropic\"\n )\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-5\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n elif provider == LLMProvider.GOOGLE:\n default_model = \"gemini-2.5-flash\"\n api_key_env = \"KIT_GOOGLE_API_KEY or GOOGLE_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_GOOGLE_API_KEY\") or os.getenv(\"GOOGLE_API_KEY\")\n elif provider == LLMProvider.OLLAMA:\n default_model = \"qwen2.5-coder:latest\" # Latest code-specialized model\n api_key_env = \"None (Ollama doesn't require API keys)\"\n # Ollama doesn't need an API key, but we'll use a placeholder for consistency\n api_key = llm_data.get(\"api_key\", \"ollama\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n # Ollama doesn't require API keys, so skip validation for it\n if not api_key and provider != LLMProvider.OLLAMA:\n # Provide a more helpful error message based on the provider\n if provider == LLMProvider.ANTHROPIC and not (config_data.get(\"llm\") or {}).get(\"provider\"):\n # User didn't specify a provider, defaulted to Anthropic\n raise ValueError(\n f\"LLM API key required. To use Anthropic (default), set {api_key_env} environment variable. \"\n f\"To use a different provider, set 'llm.provider' to 'openai', 'google', or 'ollama' in ~/.kit/review-config.yaml\"\n )\n else:\n # User explicitly chose this provider\n raise ValueError(\n f\"LLM API key required for {provider.value}. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=str(api_key),\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n api_base_url=llm_data.get(\"api_base_url\"),\n )\n\n # Set default base URLs for local providers\n if llm_config.provider == LLMProvider.OLLAMA and not llm_config.api_base_url:\n llm_config.api_base_url = \"http://localhost:11434\" # Default Ollama API endpoint\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n # Validate priority_filter if present in config\n priority_filter = review_data.get(\"priority_filter\", None)\n if priority_filter is not None:\n try:\n from .priority_utils import Priority\n\n priority_filter = Priority.validate_priorities(priority_filter)\n except (ValueError, TypeError) as e:\n raise ValueError(f\"Invalid priority_filter in config file: {e}. Valid priorities: high, medium, low\")\n\n # Load profile context if profile is specified\n profile_context = None\n if profile:\n try:\n from .profile_manager import ProfileManager\n\n profile_manager = ProfileManager()\n profile_obj = profile_manager.get_profile(profile)\n profile_context = profile_obj.context\n except Exception as e:\n raise ValueError(f\"Failed to load profile '{profile}': {e}\")\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n quiet=review_data.get(\"quiet\", False),\n priority_filter=priority_filter,\n max_review_size_mb=review_data.get(\"max_review_size_mb\", 5.0),\n profile=profile,\n profile_context=profile_context,\n repo_path=repo_path,\n save_reviews=review_data.get(\"save_reviews\", False),\n # Convenience accessors for LLM config\n llm_provider=llm_config.provider.value,\n llm_model=llm_config.model,\n llm_api_key=llm_config.api_key,\n llm_api_base_url=llm_config.api_base_url,\n llm_max_tokens=llm_config.max_tokens,\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\", \"google\"\n \"model\": \"claude-sonnet-4-5\", # or \"gpt-4.1\", \"gemini-2.5-flash\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n # For custom OpenAI compatible providers (e.g., Together AI, OpenRouter, etc.)\n # \"api_base_url\": \"https://api.together.xyz/v1\", # Example: Together AI\n # \"api_base_url\": \"https://openrouter.ai/api/v1\", # Example: OpenRouter\n # \"api_base_url\": \"http://localhost:8000/v1\", # Example: Local OpenAI API server\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Priority filtering (can also be set via --priority CLI flag)\n # \"priority_filter\": [\"high\", \"medium\"], # Only show high and medium priority issues\n # \"priority_filter\": [\"high\"], # Only show high priority issues\n # Performance and safety limits\n \"max_review_size_mb\": 5.0, # Maximum review text size in MB (prevents DoS)\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-sonnet-4-5\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n # Add a commented-out example for local Ollama usage\n local_ollama_example = \"\"\"\\\n# Example Ollama configuration (completely free local AI):\n# llm:\n# provider: ollama\n# model: \"qwen2.5-coder:latest\" # Or deepseek-r1:latest, gemma3:latest, etc.\n# api_base_url: \"http://localhost:11434\" # Default Ollama endpoint\n# api_key: \"ollama\" # Placeholder (Ollama doesn't use API keys)\n# max_tokens: 2000\n\n# Example Google Gemini configuration:\n# llm:\n# provider: google\n# model: \"gemini-2.5-flash\" # Or \"gemini-2.5-pro\" for more complex reasoning, \"gemini-2.0-flash-lite\" for speed\n# api_key: \"AIzaSy...\" # Get from https://aistudio.google.com/apikey\n# max_tokens: 4000\n\n# Example OpenAI compatible provider configurations:\n#\n# Together AI (https://together.ai/):\n# llm:\n# provider: openai\n# model: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"\n# api_key: \"your_together_api_key\"\n# api_base_url: \"https://api.together.xyz/v1\"\n# max_tokens: 4000\n#\n# OpenRouter (https://openrouter.ai/):\n# llm:\n# provider: openai\n# model: \"anthropic/claude-3.5-sonnet\"\n# api_key: \"your_openrouter_api_key\"\n# api_base_url: \"https://openrouter.ai/api/v1\"\n# max_tokens: 4000\n#\n# Local OpenAI API server (e.g., text-generation-webui, vLLM):\n# llm:\n# provider: openai\n# model: \"local-model-name\"\n# api_key: \"not-used\" # Local servers often don't require API keys\n# api_base_url: \"http://localhost:8000/v1\"\n# max_tokens: 4000\n#\n# Groq (https://groq.com/):\n# llm:\n# provider: openai\n# model: \"llama-3.3-70b-versatile\"\n# api_key: \"your_groq_api_key\"\n# api_base_url: \"https://api.groq.com/openai/v1\"\n# max_tokens: 4000\n#\n# Grok/X.AI (https://x.ai/api):\n# llm:\n# provider: openai\n# model: \"grok-4-1-fast-reasoning\" # or \"grok-code-fast-1\", \"grok-4\", etc.\n# api_key: \"your_xai_api_key\"\n# api_base_url: \"https://api.x.ai/v1\"\n# max_tokens: 4000\n\"\"\"\n\n with open(config_path, \"a\") as f:\n f.write(\"\\n\" + local_ollama_example)\n\n return config_path\n"}, "fe84144a145ea9fa41244d55fa6ec2a0e58ffd1d": {"t": 1785768249.059861, "n": 18305, "h": "1c0b17609e35ec9f", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, List, Optional\n\nimport yaml # type: ignore\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n OLLAMA = \"ollama\"\n GOOGLE = \"google\"\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\ndef _detect_provider_from_model(model_name: str) -> Optional[LLMProvider]:\n \"\"\"Detect LLM provider from model name.\"\"\"\n model_lower = model_name.lower()\n\n # Strip common prefixes first\n prefixes_to_strip = [\n \"vertex_ai/\",\n \"openrouter/\",\n \"together/\",\n \"groq/\",\n \"fireworks/\",\n \"perplexity/\",\n \"replicate/\",\n \"bedrock/\",\n \"azure/\",\n ]\n\n stripped_model = model_lower\n for prefix in prefixes_to_strip:\n if stripped_model.startswith(prefix):\n stripped_model = stripped_model[len(prefix) :]\n break\n\n # OpenAI model patterns (includes GPT-5 mini/nano and O3 models)\n # Check exact matches first\n if stripped_model == \"o3\":\n return LLMProvider.OPENAI\n\n # Then check prefix/substring patterns\n openai_patterns = [\"gpt-\", \"o1-\", \"o3-\", \"text-davinci\", \"text-curie\", \"text-babbage\", \"text-ada\", \"grok-\"]\n if any(pattern in stripped_model for pattern in openai_patterns):\n return LLMProvider.OPENAI\n\n # Anthropic model patterns\n anthropic_patterns = [\"claude-\", \"haiku\", \"sonnet\", \"opus\"]\n if any(pattern in stripped_model for pattern in anthropic_patterns):\n return LLMProvider.ANTHROPIC\n\n # Google model patterns\n google_patterns = [\"gemini-\", \"gemini\", \"bison\", \"gecko\", \"palm\"]\n if any(pattern in stripped_model for pattern in google_patterns):\n return LLMProvider.GOOGLE\n\n # Ollama model patterns - popular models available in Ollama\n ollama_patterns = [\n \"llama\",\n \"mistral\",\n \"codellama\",\n \"deepseek\",\n \"qwen\",\n \"phi\",\n \"gemma\",\n \"wizardcoder\",\n \"starcoder\",\n \"codegemma\",\n \"solar\",\n \"nous-hermes\",\n \"openchat\",\n \"zephyr\",\n \"orca\",\n \"vicuna\",\n \"alpaca\",\n \"devstral\",\n ]\n if any(pattern in stripped_model for pattern in ollama_patterns):\n return LLMProvider.OLLAMA\n\n return None\n\n\ndef _is_placeholder_token(token: Optional[str]) -> bool:\n \"\"\"Check if a token is a placeholder that should be ignored.\"\"\"\n if not token:\n return True\n\n # Common placeholder patterns\n placeholder_patterns = [\n \"your_token_here\",\n \"your_api_key_here\",\n \"your_key_here\",\n \"replace_with_your_token\",\n \"sk-your_api_key_here\",\n \"ghp_your_token_here\",\n \"sk-ant-your_key\",\n ]\n\n token_lower = token.lower()\n return any(pattern in token_lower for pattern in placeholder_patterns)\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n api_base_url: Optional[str] = None # For local LLMs or custom OpenAI endpoints\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n # Output control\n quiet: bool = False # Suppress status output for plain mode\n # Priority filtering\n priority_filter: Optional[List[str]] = None # [\"high\", \"medium\", \"low\"] or subset\n max_review_size_mb: float = 5.0 # Default 5MB limit (was 1MB hardcoded)\n # Custom context profile\n profile: Optional[str] = None # Profile name to use\n profile_context: Optional[str] = None # Loaded profile context\n # Existing repository path (skips cloning when provided)\n repo_path: Optional[str] = None # Path to existing repository to use for analysis\n # Local review settings\n save_reviews: bool = False # Save local reviews to .kit/reviews/\n llm_provider: Optional[str] = None # Convenience accessor for llm.provider\n llm_model: Optional[str] = None # Convenience accessor for llm.model\n llm_api_key: Optional[str] = None # Convenience accessor for llm.api_key\n llm_api_base_url: Optional[str] = None # Convenience accessor for llm.api_base_url\n llm_max_tokens: Optional[int] = None # Convenience accessor for llm.max_tokens\n\n @classmethod\n def from_file(\n cls,\n config_path: Optional[str] = None,\n profile: Optional[str] = None,\n repo_path: Optional[str] = None,\n model_hint: Optional[str] = None,\n ) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\n\n Args:\n config_path: Path to config file\n profile: Profile name to load custom context from\n repo_path: Path to existing repository to use for analysis\n model_hint: Model name to help auto-detect provider\n \"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n config_github_token = config_data.get(\"github\", {}).get(\"token\")\n if _is_placeholder_token(config_github_token):\n config_github_token = None # Treat placeholder as missing\n\n github_token = config_github_token or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\")\n\n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {}) or {} # Handle None or empty llm section\n\n # Try to detect provider from model hint first\n detected_provider = None\n if model_hint:\n detected_provider = _detect_provider_from_model(model_hint)\n\n # Get provider from config, environment, detected, or default\n provider_str = (\n llm_data.get(\"provider\")\n or os.getenv(\"LLM_PROVIDER\")\n or (detected_provider.value if detected_provider else None)\n or \"anthropic\"\n )\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-5\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n elif provider == LLMProvider.GOOGLE:\n default_model = \"gemini-2.5-flash\"\n api_key_env = \"KIT_GOOGLE_API_KEY or GOOGLE_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_GOOGLE_API_KEY\") or os.getenv(\"GOOGLE_API_KEY\")\n elif provider == LLMProvider.OLLAMA:\n default_model = \"qwen2.5-coder:latest\" # Latest code-specialized model\n api_key_env = \"None (Ollama doesn't require API keys)\"\n # Ollama doesn't need an API key, but we'll use a placeholder for consistency\n api_key = llm_data.get(\"api_key\", \"ollama\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n # Ollama doesn't require API keys, so skip validation for it\n if not api_key and provider != LLMProvider.OLLAMA:\n # Provide a more helpful error message based on the provider\n if provider == LLMProvider.ANTHROPIC and not (config_data.get(\"llm\") or {}).get(\"provider\"):\n # User didn't specify a provider, defaulted to Anthropic\n raise ValueError(\n f\"LLM API key required. To use Anthropic (default), set {api_key_env} environment variable. \"\n f\"To use a different provider, set 'llm.provider' to 'openai', 'google', or 'ollama' in ~/.kit/review-config.yaml\"\n )\n else:\n # User explicitly chose this provider\n raise ValueError(\n f\"LLM API key required for {provider.value}. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=str(api_key),\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n api_base_url=llm_data.get(\"api_base_url\"),\n )\n\n # Set default base URLs for local providers\n if llm_config.provider == LLMProvider.OLLAMA and not llm_config.api_base_url:\n llm_config.api_base_url = \"http://localhost:11434\" # Default Ollama API endpoint\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n # Validate priority_filter if present in config\n priority_filter = review_data.get(\"priority_filter\", None)\n if priority_filter is not None:\n try:\n from .priority_utils import Priority\n\n priority_filter = Priority.validate_priorities(priority_filter)\n except (ValueError, TypeError) as e:\n raise ValueError(f\"Invalid priority_filter in config file: {e}. Valid priorities: high, medium, low\")\n\n # Load profile context if profile is specified\n profile_context = None\n if profile:\n try:\n from .profile_manager import ProfileManager\n\n profile_manager = ProfileManager()\n profile_obj = profile_manager.get_profile(profile)\n profile_context = profile_obj.context\n except Exception as e:\n raise ValueError(f\"Failed to load profile '{profile}': {e}\")\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n quiet=review_data.get(\"quiet\", False),\n priority_filter=priority_filter,\n max_review_size_mb=review_data.get(\"max_review_size_mb\", 5.0),\n profile=profile,\n profile_context=profile_context,\n repo_path=repo_path,\n save_reviews=review_data.get(\"save_reviews\", False),\n # Convenience accessors for LLM config\n llm_provider=llm_config.provider.value,\n llm_model=llm_config.model,\n llm_api_key=llm_config.api_key,\n llm_api_base_url=llm_config.api_base_url,\n llm_max_tokens=llm_config.max_tokens,\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\", \"google\"\n \"model\": \"claude-sonnet-4-5\", # or \"gpt-4.1\", \"gemini-2.5-flash\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n # For custom OpenAI compatible providers (e.g., Together AI, OpenRouter, etc.)\n # \"api_base_url\": \"https://api.together.xyz/v1\", # Example: Together AI\n # \"api_base_url\": \"https://openrouter.ai/api/v1\", # Example: OpenRouter\n # \"api_base_url\": \"http://localhost:8000/v1\", # Example: Local OpenAI API server\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Priority filtering (can also be set via --priority CLI flag)\n # \"priority_filter\": [\"high\", \"medium\"], # Only show high and medium priority issues\n # \"priority_filter\": [\"high\"], # Only show high priority issues\n # Performance and safety limits\n \"max_review_size_mb\": 5.0, # Maximum review text size in MB (prevents DoS)\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-sonnet-4-5\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n # Add a commented-out example for local Ollama usage\n local_ollama_example = \"\"\"\\\n# Example Ollama configuration (completely free local AI):\n# llm:\n# provider: ollama\n# model: \"qwen2.5-coder:latest\" # Or deepseek-r1:latest, gemma3:latest, etc.\n# api_base_url: \"http://localhost:11434\" # Default Ollama endpoint\n# api_key: \"ollama\" # Placeholder (Ollama doesn't use API keys)\n# max_tokens: 2000\n\n# Example Google Gemini configuration:\n# llm:\n# provider: google\n# model: \"gemini-2.5-flash\" # Or \"gemini-2.5-pro\" for more complex reasoning, \"gemini-2.0-flash-lite\" for speed\n# api_key: \"AIzaSy...\" # Get from https://aistudio.google.com/apikey\n# max_tokens: 4000\n\n# Example OpenAI compatible provider configurations:\n#\n# Together AI (https://together.ai/):\n# llm:\n# provider: openai\n# model: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"\n# api_key: \"your_together_api_key\"\n# api_base_url: \"https://api.together.xyz/v1\"\n# max_tokens: 4000\n#\n# OpenRouter (https://openrouter.ai/):\n# llm:\n# provider: openai\n# model: \"anthropic/claude-3.5-sonnet\"\n# api_key: \"your_openrouter_api_key\"\n# api_base_url: \"https://openrouter.ai/api/v1\"\n# max_tokens: 4000\n#\n# Local OpenAI API server (e.g., text-generation-webui, vLLM):\n# llm:\n# provider: openai\n# model: \"local-model-name\"\n# api_key: \"not-used\" # Local servers often don't require API keys\n# api_base_url: \"http://localhost:8000/v1\"\n# max_tokens: 4000\n#\n# Groq (https://groq.com/):\n# llm:\n# provider: openai\n# model: \"llama-3.3-70b-versatile\"\n# api_key: \"your_groq_api_key\"\n# api_base_url: \"https://api.groq.com/openai/v1\"\n# max_tokens: 4000\n#\n# Grok/X.AI (https://x.ai/api):\n# llm:\n# provider: openai\n# model: \"grok-4-1-fast-reasoning\" # or \"grok-code-fast-1\", \"grok-4\", etc.\n# api_key: \"your_xai_api_key\"\n# api_base_url: \"https://api.x.ai/v1\"\n# max_tokens: 4000\n\"\"\"\n\n with open(config_path, \"a\") as f:\n f.write(\"\\n\" + local_ollama_example)\n\n return config_path\n"}, "8de5f876ebb216c9d06b81fcc297196079b90561": {"t": 1785768252.0782928, "n": 18083, "h": "a027d828b7c16028", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, List, Optional\n\nimport yaml # type: ignore\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n OLLAMA = \"ollama\"\n GOOGLE = \"google\"\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\ndef _detect_provider_from_model(model_name: str) -> Optional[LLMProvider]:\n \"\"\"Detect LLM provider from model name.\"\"\"\n model_lower = model_name.lower()\n\n # Strip common prefixes first\n prefixes_to_strip = [\n \"vertex_ai/\",\n \"openrouter/\",\n \"together/\",\n \"groq/\",\n \"fireworks/\",\n \"perplexity/\",\n \"replicate/\",\n \"bedrock/\",\n \"azure/\",\n ]\n\n stripped_model = model_lower\n for prefix in prefixes_to_strip:\n if stripped_model.startswith(prefix):\n stripped_model = stripped_model[len(prefix) :]\n break\n\n # OpenAI model patterns (includes GPT-5 mini/nano and O3 models)\n # Check exact matches first\n if stripped_model == \"o3\":\n return LLMProvider.OPENAI\n\n # Then check prefix/substring patterns\n openai_patterns = [\"gpt-\", \"o1-\", \"o3-\", \"text-davinci\", \"text-curie\", \"text-babbage\", \"text-ada\"]\n if any(pattern in stripped_model for pattern in openai_patterns):\n return LLMProvider.OPENAI\n\n # Anthropic model patterns\n anthropic_patterns = [\"claude-\", \"haiku\", \"sonnet\", \"opus\"]\n if any(pattern in stripped_model for pattern in anthropic_patterns):\n return LLMProvider.ANTHROPIC\n\n # Google model patterns\n google_patterns = [\"gemini-\", \"gemini\", \"bison\", \"gecko\", \"palm\"]\n if any(pattern in stripped_model for pattern in google_patterns):\n return LLMProvider.GOOGLE\n\n # Ollama model patterns - popular models available in Ollama\n ollama_patterns = [\n \"llama\",\n \"mistral\",\n \"codellama\",\n \"deepseek\",\n \"qwen\",\n \"phi\",\n \"gemma\",\n \"wizardcoder\",\n \"starcoder\",\n \"codegemma\",\n \"solar\",\n \"nous-hermes\",\n \"openchat\",\n \"zephyr\",\n \"orca\",\n \"vicuna\",\n \"alpaca\",\n \"devstral\",\n ]\n if any(pattern in stripped_model for pattern in ollama_patterns):\n return LLMProvider.OLLAMA\n\n return None\n\n\ndef _is_placeholder_token(token: Optional[str]) -> bool:\n \"\"\"Check if a token is a placeholder that should be ignored.\"\"\"\n if not token:\n return True\n\n # Common placeholder patterns\n placeholder_patterns = [\n \"your_token_here\",\n \"your_api_key_here\",\n \"your_key_here\",\n \"replace_with_your_token\",\n \"sk-your_api_key_here\",\n \"ghp_your_token_here\",\n \"sk-ant-your_key\",\n ]\n\n token_lower = token.lower()\n return any(pattern in token_lower for pattern in placeholder_patterns)\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n api_base_url: Optional[str] = None # For local LLMs or custom OpenAI endpoints\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n # Output control\n quiet: bool = False # Suppress status output for plain mode\n # Priority filtering\n priority_filter: Optional[List[str]] = None # [\"high\", \"medium\", \"low\"] or subset\n max_review_size_mb: float = 5.0 # Default 5MB limit (was 1MB hardcoded)\n # Custom context profile\n profile: Optional[str] = None # Profile name to use\n profile_context: Optional[str] = None # Loaded profile context\n # Existing repository path (skips cloning when provided)\n repo_path: Optional[str] = None # Path to existing repository to use for analysis\n # Local review settings\n save_reviews: bool = False # Save local reviews to .kit/reviews/\n llm_provider: Optional[str] = None # Convenience accessor for llm.provider\n llm_model: Optional[str] = None # Convenience accessor for llm.model\n llm_api_key: Optional[str] = None # Convenience accessor for llm.api_key\n llm_api_base_url: Optional[str] = None # Convenience accessor for llm.api_base_url\n llm_max_tokens: Optional[int] = None # Convenience accessor for llm.max_tokens\n\n @classmethod\n def from_file(\n cls,\n config_path: Optional[str] = None,\n profile: Optional[str] = None,\n repo_path: Optional[str] = None,\n model_hint: Optional[str] = None,\n ) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\n\n Args:\n config_path: Path to config file\n profile: Profile name to load custom context from\n repo_path: Path to existing repository to use for analysis\n model_hint: Model name to help auto-detect provider\n \"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n config_github_token = config_data.get(\"github\", {}).get(\"token\")\n if _is_placeholder_token(config_github_token):\n config_github_token = None # Treat placeholder as missing\n\n github_token = config_github_token or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\")\n\n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {}) or {} # Handle None or empty llm section\n\n # Try to detect provider from model hint first\n detected_provider = None\n if model_hint:\n detected_provider = _detect_provider_from_model(model_hint)\n\n # Get provider from config, environment, detected, or default\n provider_str = (\n llm_data.get(\"provider\")\n or os.getenv(\"LLM_PROVIDER\")\n or (detected_provider.value if detected_provider else None)\n or \"anthropic\"\n )\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n elif provider == LLMProvider.GOOGLE:\n default_model = \"gemini-2.5-flash\"\n api_key_env = \"KIT_GOOGLE_API_KEY or GOOGLE_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_GOOGLE_API_KEY\") or os.getenv(\"GOOGLE_API_KEY\")\n elif provider == LLMProvider.OLLAMA:\n default_model = \"qwen2.5-coder:latest\" # Latest code-specialized model\n api_key_env = \"None (Ollama doesn't require API keys)\"\n # Ollama doesn't need an API key, but we'll use a placeholder for consistency\n api_key = llm_data.get(\"api_key\", \"ollama\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n # Ollama doesn't require API keys, so skip validation for it\n if not api_key and provider != LLMProvider.OLLAMA:\n # Provide a more helpful error message based on the provider\n if provider == LLMProvider.ANTHROPIC and not (config_data.get(\"llm\") or {}).get(\"provider\"):\n # User didn't specify a provider, defaulted to Anthropic\n raise ValueError(\n f\"LLM API key required. To use Anthropic (default), set {api_key_env} environment variable. \"\n f\"To use a different provider, set 'llm.provider' to 'openai', 'google', or 'ollama' in ~/.kit/review-config.yaml\"\n )\n else:\n # User explicitly chose this provider\n raise ValueError(\n f\"LLM API key required for {provider.value}. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=str(api_key),\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n api_base_url=llm_data.get(\"api_base_url\"),\n )\n\n # Set default base URLs for local providers\n if llm_config.provider == LLMProvider.OLLAMA and not llm_config.api_base_url:\n llm_config.api_base_url = \"http://localhost:11434\" # Default Ollama API endpoint\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n # Validate priority_filter if present in config\n priority_filter = review_data.get(\"priority_filter\", None)\n if priority_filter is not None:\n try:\n from .priority_utils import Priority\n\n priority_filter = Priority.validate_priorities(priority_filter)\n except (ValueError, TypeError) as e:\n raise ValueError(f\"Invalid priority_filter in config file: {e}. Valid priorities: high, medium, low\")\n\n # Load profile context if profile is specified\n profile_context = None\n if profile:\n try:\n from .profile_manager import ProfileManager\n\n profile_manager = ProfileManager()\n profile_obj = profile_manager.get_profile(profile)\n profile_context = profile_obj.context\n except Exception as e:\n raise ValueError(f\"Failed to load profile '{profile}': {e}\")\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n quiet=review_data.get(\"quiet\", False),\n priority_filter=priority_filter,\n max_review_size_mb=review_data.get(\"max_review_size_mb\", 5.0),\n profile=profile,\n profile_context=profile_context,\n repo_path=repo_path,\n save_reviews=review_data.get(\"save_reviews\", False),\n # Convenience accessors for LLM config\n llm_provider=llm_config.provider.value,\n llm_model=llm_config.model,\n llm_api_key=llm_config.api_key,\n llm_api_base_url=llm_config.api_base_url,\n llm_max_tokens=llm_config.max_tokens,\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\", \"google\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\", \"gemini-2.5-flash\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n # For custom OpenAI compatible providers (e.g., Together AI, OpenRouter, etc.)\n # \"api_base_url\": \"https://api.together.xyz/v1\", # Example: Together AI\n # \"api_base_url\": \"https://openrouter.ai/api/v1\", # Example: OpenRouter\n # \"api_base_url\": \"http://localhost:8000/v1\", # Example: Local OpenAI API server\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Priority filtering (can also be set via --priority CLI flag)\n # \"priority_filter\": [\"high\", \"medium\"], # Only show high and medium priority issues\n # \"priority_filter\": [\"high\"], # Only show high priority issues\n # Performance and safety limits\n \"max_review_size_mb\": 5.0, # Maximum review text size in MB (prevents DoS)\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n # Add a commented-out example for local Ollama usage\n local_ollama_example = \"\"\"\\\n# Example Ollama configuration (completely free local AI):\n# llm:\n# provider: ollama\n# model: \"qwen2.5-coder:latest\" # Or deepseek-r1:latest, gemma3:latest, etc.\n# api_base_url: \"http://localhost:11434\" # Default Ollama endpoint\n# api_key: \"ollama\" # Placeholder (Ollama doesn't use API keys)\n# max_tokens: 2000\n\n# Example Google Gemini configuration:\n# llm:\n# provider: google\n# model: \"gemini-2.5-flash\" # Or \"gemini-2.5-pro\" for more complex reasoning, \"gemini-2.0-flash-lite\" for speed\n# api_key: \"AIzaSy...\" # Get from https://aistudio.google.com/apikey\n# max_tokens: 4000\n\n# Example OpenAI compatible provider configurations:\n#\n# Together AI (https://together.ai/):\n# llm:\n# provider: openai\n# model: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"\n# api_key: \"your_together_api_key\"\n# api_base_url: \"https://api.together.xyz/v1\"\n# max_tokens: 4000\n#\n# OpenRouter (https://openrouter.ai/):\n# llm:\n# provider: openai\n# model: \"anthropic/claude-3.5-sonnet\"\n# api_key: \"your_openrouter_api_key\"\n# api_base_url: \"https://openrouter.ai/api/v1\"\n# max_tokens: 4000\n#\n# Local OpenAI API server (e.g., text-generation-webui, vLLM):\n# llm:\n# provider: openai\n# model: \"local-model-name\"\n# api_key: \"not-used\" # Local servers often don't require API keys\n# api_base_url: \"http://localhost:8000/v1\"\n# max_tokens: 4000\n#\n# Groq (https://groq.com/):\n# llm:\n# provider: openai\n# model: \"llama-3.3-70b-versatile\"\n# api_key: \"your_groq_api_key\"\n# api_base_url: \"https://api.groq.com/openai/v1\"\n# max_tokens: 4000\n\"\"\"\n\n with open(config_path, \"a\") as f:\n f.write(\"\\n\" + local_ollama_example)\n\n return config_path\n"}, "5db16e9dc6952bab5d6996025afe3d39a67e0462": {"t": 1785768254.6210916, "n": 18456, "h": "88598484325bcdd1", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, List, Optional\n\nimport yaml\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n OLLAMA = \"ollama\"\n GOOGLE = \"google\"\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\ndef _detect_provider_from_model(model_name: str) -> Optional[LLMProvider]:\n \"\"\"Detect LLM provider from model name.\"\"\"\n model_lower = model_name.lower()\n\n # Strip common prefixes first\n prefixes_to_strip = [\n \"vertex_ai/\",\n \"openrouter/\",\n \"together/\",\n \"groq/\",\n \"fireworks/\",\n \"perplexity/\",\n \"replicate/\",\n \"bedrock/\",\n \"azure/\",\n ]\n\n stripped_model = model_lower\n for prefix in prefixes_to_strip:\n if stripped_model.startswith(prefix):\n stripped_model = stripped_model[len(prefix) :]\n break\n\n # OpenAI model patterns (includes GPT-5 mini/nano and O3 models)\n # Check exact matches first\n if stripped_model == \"o3\":\n return LLMProvider.OPENAI\n\n # Then check prefix/substring patterns\n openai_patterns = [\"gpt-\", \"o1-\", \"o3-\", \"text-davinci\", \"text-curie\", \"text-babbage\", \"text-ada\"]\n if any(pattern in stripped_model for pattern in openai_patterns):\n return LLMProvider.OPENAI\n\n # Anthropic model patterns\n anthropic_patterns = [\"claude-\", \"haiku\", \"sonnet\", \"opus\"]\n if any(pattern in stripped_model for pattern in anthropic_patterns):\n return LLMProvider.ANTHROPIC\n\n # Google model patterns\n google_patterns = [\"gemini-\", \"gemini\", \"bison\", \"gecko\", \"palm\"]\n if any(pattern in stripped_model for pattern in google_patterns):\n return LLMProvider.GOOGLE\n\n # Ollama model patterns - popular models available in Ollama\n ollama_patterns = [\n \"llama\",\n \"mistral\",\n \"codellama\",\n \"deepseek\",\n \"qwen\",\n \"phi\",\n \"gemma\",\n \"wizardcoder\",\n \"starcoder\",\n \"codegemma\",\n \"solar\",\n \"nous-hermes\",\n \"openchat\",\n \"zephyr\",\n \"orca\",\n \"vicuna\",\n \"alpaca\",\n \"devstral\",\n ]\n if any(pattern in stripped_model for pattern in ollama_patterns):\n return LLMProvider.OLLAMA\n\n return None\n\n\ndef _is_placeholder_token(token: Optional[str]) -> bool:\n \"\"\"Check if a token is a placeholder that should be ignored.\"\"\"\n if not token:\n return True\n\n # Common placeholder patterns\n placeholder_patterns = [\n \"your_token_here\",\n \"your_api_key_here\",\n \"your_key_here\",\n \"replace_with_your_token\",\n \"sk-your_api_key_here\",\n \"ghp_your_token_here\",\n \"sk-ant-your_key\",\n ]\n\n token_lower = token.lower()\n return any(pattern in token_lower for pattern in placeholder_patterns)\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n temperature: float = 0.1\n api_base_url: Optional[str] = None # For local LLMs or custom OpenAI endpoints\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n # Output control\n quiet: bool = False # Suppress status output for plain mode\n # Priority filtering\n priority_filter: Optional[List[str]] = None # [\"high\", \"medium\", \"low\"] or subset\n max_review_size_mb: float = 5.0 # Default 5MB limit (was 1MB hardcoded)\n # Custom context profile\n profile: Optional[str] = None # Profile name to use\n profile_context: Optional[str] = None # Loaded profile context\n # Existing repository path (skips cloning when provided)\n repo_path: Optional[str] = None # Path to existing repository to use for analysis\n # Local review settings\n save_reviews: bool = False # Save local reviews to .kit/reviews/\n llm_provider: Optional[str] = None # Convenience accessor for llm.provider\n llm_model: Optional[str] = None # Convenience accessor for llm.model\n llm_api_key: Optional[str] = None # Convenience accessor for llm.api_key\n llm_api_base_url: Optional[str] = None # Convenience accessor for llm.api_base_url\n llm_temperature: Optional[float] = None # Convenience accessor for llm.temperature\n llm_max_tokens: Optional[int] = None # Convenience accessor for llm.max_tokens\n\n @classmethod\n def from_file(\n cls,\n config_path: Optional[str] = None,\n profile: Optional[str] = None,\n repo_path: Optional[str] = None,\n model_hint: Optional[str] = None,\n ) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\n\n Args:\n config_path: Path to config file\n profile: Profile name to load custom context from\n repo_path: Path to existing repository to use for analysis\n model_hint: Model name to help auto-detect provider\n \"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n config_github_token = config_data.get(\"github\", {}).get(\"token\")\n if _is_placeholder_token(config_github_token):\n config_github_token = None # Treat placeholder as missing\n\n github_token = config_github_token or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\")\n\n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {}) or {} # Handle None or empty llm section\n\n # Try to detect provider from model hint first\n detected_provider = None\n if model_hint:\n detected_provider = _detect_provider_from_model(model_hint)\n\n # Get provider from config, environment, detected, or default\n provider_str = (\n llm_data.get(\"provider\")\n or os.getenv(\"LLM_PROVIDER\")\n or (detected_provider.value if detected_provider else None)\n or \"anthropic\"\n )\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n elif provider == LLMProvider.GOOGLE:\n default_model = \"gemini-2.5-flash\"\n api_key_env = \"KIT_GOOGLE_API_KEY or GOOGLE_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_GOOGLE_API_KEY\") or os.getenv(\"GOOGLE_API_KEY\")\n elif provider == LLMProvider.OLLAMA:\n default_model = \"qwen2.5-coder:latest\" # Latest code-specialized model\n api_key_env = \"None (Ollama doesn't require API keys)\"\n # Ollama doesn't need an API key, but we'll use a placeholder for consistency\n api_key = llm_data.get(\"api_key\", \"ollama\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n # Ollama doesn't require API keys, so skip validation for it\n if not api_key and provider != LLMProvider.OLLAMA:\n # Provide a more helpful error message based on the provider\n if provider == LLMProvider.ANTHROPIC and not (config_data.get(\"llm\") or {}).get(\"provider\"):\n # User didn't specify a provider, defaulted to Anthropic\n raise ValueError(\n f\"LLM API key required. To use Anthropic (default), set {api_key_env} environment variable. \"\n f\"To use a different provider, set 'llm.provider' to 'openai', 'google', or 'ollama' in ~/.kit/review-config.yaml\"\n )\n else:\n # User explicitly chose this provider\n raise ValueError(\n f\"LLM API key required for {provider.value}. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=str(api_key),\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n temperature=llm_data.get(\"temperature\", 0.1),\n api_base_url=llm_data.get(\"api_base_url\"),\n )\n\n # Set default base URLs for local providers\n if llm_config.provider == LLMProvider.OLLAMA and not llm_config.api_base_url:\n llm_config.api_base_url = \"http://localhost:11434\" # Default Ollama API endpoint\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n # Validate priority_filter if present in config\n priority_filter = review_data.get(\"priority_filter\", None)\n if priority_filter is not None:\n try:\n from .priority_utils import Priority\n\n priority_filter = Priority.validate_priorities(priority_filter)\n except (ValueError, TypeError) as e:\n raise ValueError(f\"Invalid priority_filter in config file: {e}. Valid priorities: high, medium, low\")\n\n # Load profile context if profile is specified\n profile_context = None\n if profile:\n try:\n from .profile_manager import ProfileManager\n\n profile_manager = ProfileManager()\n profile_obj = profile_manager.get_profile(profile)\n profile_context = profile_obj.context\n except Exception as e:\n raise ValueError(f\"Failed to load profile '{profile}': {e}\")\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n quiet=review_data.get(\"quiet\", False),\n priority_filter=priority_filter,\n max_review_size_mb=review_data.get(\"max_review_size_mb\", 5.0),\n profile=profile,\n profile_context=profile_context,\n repo_path=repo_path,\n save_reviews=review_data.get(\"save_reviews\", False),\n # Convenience accessors for LLM config\n llm_provider=llm_config.provider.value,\n llm_model=llm_config.model,\n llm_api_key=llm_config.api_key,\n llm_api_base_url=llm_config.api_base_url,\n llm_temperature=llm_config.temperature,\n llm_max_tokens=llm_config.max_tokens,\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\", \"google\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\", \"gemini-2.5-flash\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n \"temperature\": 0.1,\n # For custom OpenAI compatible providers (e.g., Together AI, OpenRouter, etc.)\n # \"api_base_url\": \"https://api.together.xyz/v1\", # Example: Together AI\n # \"api_base_url\": \"https://openrouter.ai/api/v1\", # Example: OpenRouter\n # \"api_base_url\": \"http://localhost:8000/v1\", # Example: Local OpenAI API server\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Priority filtering (can also be set via --priority CLI flag)\n # \"priority_filter\": [\"high\", \"medium\"], # Only show high and medium priority issues\n # \"priority_filter\": [\"high\"], # Only show high priority issues\n # Performance and safety limits\n \"max_review_size_mb\": 5.0, # Maximum review text size in MB (prevents DoS)\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n # Add a commented-out example for local Ollama usage\n local_ollama_example = \"\"\"\\\n# Example Ollama configuration (completely free local AI):\n# llm:\n# provider: ollama\n# model: \"qwen2.5-coder:latest\" # Or deepseek-r1:latest, gemma3:latest, etc.\n# api_base_url: \"http://localhost:11434\" # Default Ollama endpoint\n# api_key: \"ollama\" # Placeholder (Ollama doesn't use API keys)\n# max_tokens: 2000\n# temperature: 0.1\n\n# Example Google Gemini configuration:\n# llm:\n# provider: google\n# model: \"gemini-2.5-flash\" # Or \"gemini-2.5-pro\" for more complex reasoning, \"gemini-2.0-flash-lite\" for speed\n# api_key: \"AIzaSy...\" # Get from https://aistudio.google.com/apikey\n# max_tokens: 4000\n# temperature: 0.1\n\n# Example OpenAI compatible provider configurations:\n#\n# Together AI (https://together.ai/):\n# llm:\n# provider: openai\n# model: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"\n# api_key: \"your_together_api_key\"\n# api_base_url: \"https://api.together.xyz/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# OpenRouter (https://openrouter.ai/):\n# llm:\n# provider: openai\n# model: \"anthropic/claude-3.5-sonnet\"\n# api_key: \"your_openrouter_api_key\"\n# api_base_url: \"https://openrouter.ai/api/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Local OpenAI API server (e.g., text-generation-webui, vLLM):\n# llm:\n# provider: openai\n# model: \"local-model-name\"\n# api_key: \"not-used\" # Local servers often don't require API keys\n# api_base_url: \"http://localhost:8000/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Groq (https://groq.com/):\n# llm:\n# provider: openai\n# model: \"llama-3.3-70b-versatile\"\n# api_key: \"your_groq_api_key\"\n# api_base_url: \"https://api.groq.com/openai/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n\"\"\"\n\n with open(config_path, \"a\") as f:\n f.write(\"\\n\" + local_ollama_example)\n\n return config_path\n"}, "3378c5267899b385b6fcc1b0c211fd4314616b66": {"t": 1785768257.8098896, "n": 18294, "h": "57e14e724be71ba1", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, List, Optional\n\nimport yaml\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n OLLAMA = \"ollama\"\n GOOGLE = \"google\"\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\ndef _detect_provider_from_model(model_name: str) -> Optional[LLMProvider]:\n \"\"\"Detect LLM provider from model name.\"\"\"\n model_lower = model_name.lower()\n\n # Strip common prefixes first\n prefixes_to_strip = [\n \"vertex_ai/\",\n \"openrouter/\",\n \"together/\",\n \"groq/\",\n \"fireworks/\",\n \"perplexity/\",\n \"replicate/\",\n \"bedrock/\",\n \"azure/\",\n ]\n\n stripped_model = model_lower\n for prefix in prefixes_to_strip:\n if stripped_model.startswith(prefix):\n stripped_model = stripped_model[len(prefix) :]\n break\n\n # OpenAI model patterns\n openai_patterns = [\"gpt-\", \"o1-\", \"text-davinci\", \"text-curie\", \"text-babbage\", \"text-ada\"]\n if any(pattern in stripped_model for pattern in openai_patterns):\n return LLMProvider.OPENAI\n\n # Anthropic model patterns\n anthropic_patterns = [\"claude-\", \"haiku\", \"sonnet\", \"opus\"]\n if any(pattern in stripped_model for pattern in anthropic_patterns):\n return LLMProvider.ANTHROPIC\n\n # Google model patterns\n google_patterns = [\"gemini-\", \"gemini\", \"bison\", \"gecko\", \"palm\"]\n if any(pattern in stripped_model for pattern in google_patterns):\n return LLMProvider.GOOGLE\n\n # Ollama model patterns - popular models available in Ollama\n ollama_patterns = [\n \"llama\",\n \"mistral\",\n \"codellama\",\n \"deepseek\",\n \"qwen\",\n \"phi\",\n \"gemma\",\n \"wizardcoder\",\n \"starcoder\",\n \"codegemma\",\n \"solar\",\n \"nous-hermes\",\n \"openchat\",\n \"zephyr\",\n \"orca\",\n \"vicuna\",\n \"alpaca\",\n \"devstral\",\n ]\n if any(pattern in stripped_model for pattern in ollama_patterns):\n return LLMProvider.OLLAMA\n\n return None\n\n\ndef _is_placeholder_token(token: Optional[str]) -> bool:\n \"\"\"Check if a token is a placeholder that should be ignored.\"\"\"\n if not token:\n return True\n\n # Common placeholder patterns\n placeholder_patterns = [\n \"your_token_here\",\n \"your_api_key_here\",\n \"your_key_here\",\n \"replace_with_your_token\",\n \"sk-your_api_key_here\",\n \"ghp_your_token_here\",\n \"sk-ant-your_key\",\n ]\n\n token_lower = token.lower()\n return any(pattern in token_lower for pattern in placeholder_patterns)\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n temperature: float = 0.1\n api_base_url: Optional[str] = None # For local LLMs or custom OpenAI endpoints\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n # Output control\n quiet: bool = False # Suppress status output for plain mode\n # Priority filtering\n priority_filter: Optional[List[str]] = None # [\"high\", \"medium\", \"low\"] or subset\n max_review_size_mb: float = 5.0 # Default 5MB limit (was 1MB hardcoded)\n # Custom context profile\n profile: Optional[str] = None # Profile name to use\n profile_context: Optional[str] = None # Loaded profile context\n # Existing repository path (skips cloning when provided)\n repo_path: Optional[str] = None # Path to existing repository to use for analysis\n # Local review settings\n save_reviews: bool = False # Save local reviews to .kit/reviews/\n llm_provider: Optional[str] = None # Convenience accessor for llm.provider\n llm_model: Optional[str] = None # Convenience accessor for llm.model\n llm_api_key: Optional[str] = None # Convenience accessor for llm.api_key\n llm_api_base_url: Optional[str] = None # Convenience accessor for llm.api_base_url\n llm_temperature: Optional[float] = None # Convenience accessor for llm.temperature\n llm_max_tokens: Optional[int] = None # Convenience accessor for llm.max_tokens\n\n @classmethod\n def from_file(\n cls,\n config_path: Optional[str] = None,\n profile: Optional[str] = None,\n repo_path: Optional[str] = None,\n model_hint: Optional[str] = None,\n ) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\n\n Args:\n config_path: Path to config file\n profile: Profile name to load custom context from\n repo_path: Path to existing repository to use for analysis\n model_hint: Model name to help auto-detect provider\n \"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n config_github_token = config_data.get(\"github\", {}).get(\"token\")\n if _is_placeholder_token(config_github_token):\n config_github_token = None # Treat placeholder as missing\n\n github_token = config_github_token or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\")\n\n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {}) or {} # Handle None or empty llm section\n\n # Try to detect provider from model hint first\n detected_provider = None\n if model_hint:\n detected_provider = _detect_provider_from_model(model_hint)\n\n # Get provider from config, environment, detected, or default\n provider_str = (\n llm_data.get(\"provider\")\n or os.getenv(\"LLM_PROVIDER\")\n or (detected_provider.value if detected_provider else None)\n or \"anthropic\"\n )\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n elif provider == LLMProvider.GOOGLE:\n default_model = \"gemini-2.5-flash\" # Updated to latest model\n api_key_env = \"KIT_GOOGLE_API_KEY or GOOGLE_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_GOOGLE_API_KEY\") or os.getenv(\"GOOGLE_API_KEY\")\n elif provider == LLMProvider.OLLAMA:\n default_model = \"qwen2.5-coder:latest\" # Latest code-specialized model\n api_key_env = \"None (Ollama doesn't require API keys)\"\n # Ollama doesn't need an API key, but we'll use a placeholder for consistency\n api_key = llm_data.get(\"api_key\", \"ollama\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n # Ollama doesn't require API keys, so skip validation for it\n if not api_key and provider != LLMProvider.OLLAMA:\n # Provide a more helpful error message based on the provider\n if provider == LLMProvider.ANTHROPIC and not (config_data.get(\"llm\") or {}).get(\"provider\"):\n # User didn't specify a provider, defaulted to Anthropic\n raise ValueError(\n f\"LLM API key required. To use Anthropic (default), set {api_key_env} environment variable. \"\n f\"To use a different provider, set 'llm.provider' to 'openai', 'google', or 'ollama' in ~/.kit/review-config.yaml\"\n )\n else:\n # User explicitly chose this provider\n raise ValueError(\n f\"LLM API key required for {provider.value}. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=str(api_key),\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n temperature=llm_data.get(\"temperature\", 0.1),\n api_base_url=llm_data.get(\"api_base_url\"),\n )\n\n # Set default base URLs for local providers\n if llm_config.provider == LLMProvider.OLLAMA and not llm_config.api_base_url:\n llm_config.api_base_url = \"http://localhost:11434\" # Default Ollama API endpoint\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n # Validate priority_filter if present in config\n priority_filter = review_data.get(\"priority_filter\", None)\n if priority_filter is not None:\n try:\n from .priority_utils import Priority\n\n priority_filter = Priority.validate_priorities(priority_filter)\n except (ValueError, TypeError) as e:\n raise ValueError(f\"Invalid priority_filter in config file: {e}. Valid priorities: high, medium, low\")\n\n # Load profile context if profile is specified\n profile_context = None\n if profile:\n try:\n from .profile_manager import ProfileManager\n\n profile_manager = ProfileManager()\n profile_obj = profile_manager.get_profile(profile)\n profile_context = profile_obj.context\n except Exception as e:\n raise ValueError(f\"Failed to load profile '{profile}': {e}\")\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n quiet=review_data.get(\"quiet\", False),\n priority_filter=priority_filter,\n max_review_size_mb=review_data.get(\"max_review_size_mb\", 5.0),\n profile=profile,\n profile_context=profile_context,\n repo_path=repo_path,\n save_reviews=review_data.get(\"save_reviews\", False),\n # Convenience accessors for LLM config\n llm_provider=llm_config.provider.value,\n llm_model=llm_config.model,\n llm_api_key=llm_config.api_key,\n llm_api_base_url=llm_config.api_base_url,\n llm_temperature=llm_config.temperature,\n llm_max_tokens=llm_config.max_tokens,\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\", \"google\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\", \"gemini-2.5-flash\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n \"temperature\": 0.1,\n # For custom OpenAI compatible providers (e.g., Together AI, OpenRouter, etc.)\n # \"api_base_url\": \"https://api.together.xyz/v1\", # Example: Together AI\n # \"api_base_url\": \"https://openrouter.ai/api/v1\", # Example: OpenRouter\n # \"api_base_url\": \"http://localhost:8000/v1\", # Example: Local OpenAI API server\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Priority filtering (can also be set via --priority CLI flag)\n # \"priority_filter\": [\"high\", \"medium\"], # Only show high and medium priority issues\n # \"priority_filter\": [\"high\"], # Only show high priority issues\n # Performance and safety limits\n \"max_review_size_mb\": 5.0, # Maximum review text size in MB (prevents DoS)\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n # Add a commented-out example for local Ollama usage\n local_ollama_example = \"\"\"\\\n# Example Ollama configuration (completely free local AI):\n# llm:\n# provider: ollama\n# model: \"qwen2.5-coder:latest\" # Or deepseek-r1:latest, gemma3:latest, etc.\n# api_base_url: \"http://localhost:11434\" # Default Ollama endpoint\n# api_key: \"ollama\" # Placeholder (Ollama doesn't use API keys)\n# max_tokens: 2000\n# temperature: 0.1\n\n# Example Google Gemini configuration:\n# llm:\n# provider: google\n# model: \"gemini-2.5-flash\" # Or \"gemini-2.5-pro\" for more complex reasoning, \"gemini-2.0-flash-lite\" for speed\n# api_key: \"AIzaSy...\" # Get from https://aistudio.google.com/apikey\n# max_tokens: 4000\n# temperature: 0.1\n\n# Example OpenAI compatible provider configurations:\n#\n# Together AI (https://together.ai/):\n# llm:\n# provider: openai\n# model: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"\n# api_key: \"your_together_api_key\"\n# api_base_url: \"https://api.together.xyz/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# OpenRouter (https://openrouter.ai/):\n# llm:\n# provider: openai\n# model: \"anthropic/claude-3.5-sonnet\"\n# api_key: \"your_openrouter_api_key\"\n# api_base_url: \"https://openrouter.ai/api/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Local OpenAI API server (e.g., text-generation-webui, vLLM):\n# llm:\n# provider: openai\n# model: \"local-model-name\"\n# api_key: \"not-used\" # Local servers often don't require API keys\n# api_base_url: \"http://localhost:8000/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Groq (https://groq.com/):\n# llm:\n# provider: openai\n# model: \"llama-3.3-70b-versatile\"\n# api_key: \"your_groq_api_key\"\n# api_base_url: \"https://api.groq.com/openai/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n\"\"\"\n\n with open(config_path, \"a\") as f:\n f.write(\"\\n\" + local_ollama_example)\n\n return config_path\n"}, "c6d46162c0ad8a82925dbaeaa20869d4b8b80341": {"t": 1785768280.5671947, "n": 16085, "h": "1c23686cafbc8a38", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, List, Optional\n\nimport yaml\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n OLLAMA = \"ollama\"\n GOOGLE = \"google\"\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\ndef _detect_provider_from_model(model_name: str) -> Optional[LLMProvider]:\n \"\"\"Detect LLM provider from model name.\"\"\"\n model_lower = model_name.lower()\n\n # Strip common prefixes first\n prefixes_to_strip = [\n \"vertex_ai/\",\n \"openrouter/\",\n \"together/\",\n \"groq/\",\n \"fireworks/\",\n \"perplexity/\",\n \"replicate/\",\n \"bedrock/\",\n \"azure/\",\n ]\n\n stripped_model = model_lower\n for prefix in prefixes_to_strip:\n if stripped_model.startswith(prefix):\n stripped_model = stripped_model[len(prefix) :]\n break\n\n # OpenAI model patterns\n openai_patterns = [\"gpt-\", \"o1-\", \"text-davinci\", \"text-curie\", \"text-babbage\", \"text-ada\"]\n if any(pattern in stripped_model for pattern in openai_patterns):\n return LLMProvider.OPENAI\n\n # Anthropic model patterns\n anthropic_patterns = [\"claude-\", \"haiku\", \"sonnet\", \"opus\"]\n if any(pattern in stripped_model for pattern in anthropic_patterns):\n return LLMProvider.ANTHROPIC\n\n # Google model patterns\n google_patterns = [\"gemini-\", \"gemini\", \"bison\", \"gecko\", \"palm\"]\n if any(pattern in stripped_model for pattern in google_patterns):\n return LLMProvider.GOOGLE\n\n # Ollama model patterns - popular models available in Ollama\n ollama_patterns = [\n \"llama\",\n \"mistral\",\n \"codellama\",\n \"deepseek\",\n \"qwen\",\n \"phi\",\n \"gemma\",\n \"wizardcoder\",\n \"starcoder\",\n \"codegemma\",\n \"solar\",\n \"nous-hermes\",\n \"openchat\",\n \"zephyr\",\n \"orca\",\n \"vicuna\",\n \"alpaca\",\n \"devstral\",\n ]\n if any(pattern in stripped_model for pattern in ollama_patterns):\n return LLMProvider.OLLAMA\n\n return None\n\n\ndef _is_placeholder_token(token: Optional[str]) -> bool:\n \"\"\"Check if a token is a placeholder that should be ignored.\"\"\"\n if not token:\n return True\n\n # Common placeholder patterns\n placeholder_patterns = [\n \"your_token_here\",\n \"your_api_key_here\",\n \"your_key_here\",\n \"replace_with_your_token\",\n \"sk-your_api_key_here\",\n \"ghp_your_token_here\",\n \"sk-ant-your_key\",\n ]\n\n token_lower = token.lower()\n return any(pattern in token_lower for pattern in placeholder_patterns)\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n temperature: float = 0.1\n api_base_url: Optional[str] = None # For local LLMs or custom OpenAI endpoints\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n # Output control\n quiet: bool = False # Suppress status output for plain mode\n # Priority filtering\n priority_filter: Optional[List[str]] = None # [\"high\", \"medium\", \"low\"] or subset\n max_review_size_mb: float = 5.0 # Default 5MB limit (was 1MB hardcoded)\n # Custom context profile\n profile: Optional[str] = None # Profile name to use\n profile_context: Optional[str] = None # Loaded profile context\n # Existing repository path (skips cloning when provided)\n repo_path: Optional[str] = None # Path to existing repository to use for analysis\n\n @classmethod\n def from_file(\n cls, config_path: Optional[str] = None, profile: Optional[str] = None, repo_path: Optional[str] = None\n ) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\n\n Args:\n config_path: Path to config file\n profile: Profile name to load custom context from\n repo_path: Path to existing repository to use for analysis\n \"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n config_github_token = config_data.get(\"github\", {}).get(\"token\")\n if _is_placeholder_token(config_github_token):\n config_github_token = None # Treat placeholder as missing\n\n github_token = config_github_token or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\")\n\n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {})\n provider_str = llm_data.get(\"provider\") or os.getenv(\"LLM_PROVIDER\", \"anthropic\")\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n elif provider == LLMProvider.GOOGLE:\n default_model = \"gemini-2.5-flash\" # Updated to latest model\n api_key_env = \"KIT_GOOGLE_API_KEY or GOOGLE_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_GOOGLE_API_KEY\") or os.getenv(\"GOOGLE_API_KEY\")\n elif provider == LLMProvider.OLLAMA:\n default_model = \"qwen2.5-coder:latest\" # Latest code-specialized model\n api_key_env = \"None (Ollama doesn't require API keys)\"\n # Ollama doesn't need an API key, but we'll use a placeholder for consistency\n api_key = llm_data.get(\"api_key\", \"ollama\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n # Ollama doesn't require API keys, so skip validation for it\n if not api_key and provider != LLMProvider.OLLAMA:\n raise ValueError(\n f\"LLM API key required. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=str(api_key),\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n temperature=llm_data.get(\"temperature\", 0.1),\n api_base_url=llm_data.get(\"api_base_url\"),\n )\n\n # Set default base URLs for local providers\n if llm_config.provider == LLMProvider.OLLAMA and not llm_config.api_base_url:\n llm_config.api_base_url = \"http://localhost:11434\" # Default Ollama API endpoint\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n # Validate priority_filter if present in config\n priority_filter = review_data.get(\"priority_filter\", None)\n if priority_filter is not None:\n try:\n from .priority_utils import Priority\n\n priority_filter = Priority.validate_priorities(priority_filter)\n except (ValueError, TypeError) as e:\n raise ValueError(f\"Invalid priority_filter in config file: {e}. Valid priorities: high, medium, low\")\n\n # Load profile context if profile is specified\n profile_context = None\n if profile:\n try:\n from .profile_manager import ProfileManager\n\n profile_manager = ProfileManager()\n profile_obj = profile_manager.get_profile(profile)\n profile_context = profile_obj.context\n except Exception as e:\n raise ValueError(f\"Failed to load profile '{profile}': {e}\")\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n quiet=review_data.get(\"quiet\", False),\n priority_filter=priority_filter,\n max_review_size_mb=review_data.get(\"max_review_size_mb\", 5.0),\n profile=profile,\n profile_context=profile_context,\n repo_path=repo_path,\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\", \"google\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\", \"gemini-2.5-flash\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n \"temperature\": 0.1,\n # For custom OpenAI compatible providers (e.g., Together AI, OpenRouter, etc.)\n # \"api_base_url\": \"https://api.together.xyz/v1\", # Example: Together AI\n # \"api_base_url\": \"https://openrouter.ai/api/v1\", # Example: OpenRouter\n # \"api_base_url\": \"http://localhost:8000/v1\", # Example: Local OpenAI API server\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Priority filtering (can also be set via --priority CLI flag)\n # \"priority_filter\": [\"high\", \"medium\"], # Only show high and medium priority issues\n # \"priority_filter\": [\"high\"], # Only show high priority issues\n # Performance and safety limits\n \"max_review_size_mb\": 5.0, # Maximum review text size in MB (prevents DoS)\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n # Add a commented-out example for local Ollama usage\n local_ollama_example = \"\"\"\\\n# Example Ollama configuration (completely free local AI):\n# llm:\n# provider: ollama\n# model: \"qwen2.5-coder:latest\" # Or deepseek-r1:latest, gemma3:latest, etc.\n# api_base_url: \"http://localhost:11434\" # Default Ollama endpoint\n# api_key: \"ollama\" # Placeholder (Ollama doesn't use API keys)\n# max_tokens: 2000\n# temperature: 0.1\n\n# Example Google Gemini configuration:\n# llm:\n# provider: google\n# model: \"gemini-2.5-flash\" # Or \"gemini-2.5-pro\" for more complex reasoning, \"gemini-2.0-flash-lite\" for speed\n# api_key: \"AIzaSy...\" # Get from https://aistudio.google.com/apikey\n# max_tokens: 4000\n# temperature: 0.1\n\n# Example OpenAI compatible provider configurations:\n#\n# Together AI (https://together.ai/):\n# llm:\n# provider: openai\n# model: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"\n# api_key: \"your_together_api_key\"\n# api_base_url: \"https://api.together.xyz/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# OpenRouter (https://openrouter.ai/):\n# llm:\n# provider: openai\n# model: \"anthropic/claude-3.5-sonnet\"\n# api_key: \"your_openrouter_api_key\"\n# api_base_url: \"https://openrouter.ai/api/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Local OpenAI API server (e.g., text-generation-webui, vLLM):\n# llm:\n# provider: openai\n# model: \"local-model-name\"\n# api_key: \"not-used\" # Local servers often don't require API keys\n# api_base_url: \"http://localhost:8000/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Groq (https://groq.com/):\n# llm:\n# provider: openai\n# model: \"llama-3.3-70b-versatile\"\n# api_key: \"your_groq_api_key\"\n# api_base_url: \"https://api.groq.com/openai/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n\"\"\"\n\n with open(config_path, \"a\") as f:\n f.write(\"\\n\" + local_ollama_example)\n\n return config_path\n"}, "83e1b5ab703a1851ab22a372049574e767c559de": {"t": 1785768283.0490263, "n": 16071, "h": "a6ae465d308941dc", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, List, Optional\n\nimport yaml\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n OLLAMA = \"ollama\"\n GOOGLE = \"google\"\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\ndef _detect_provider_from_model(model_name: str) -> Optional[LLMProvider]:\n \"\"\"Detect LLM provider from model name.\"\"\"\n model_lower = model_name.lower()\n\n # Strip common prefixes first\n prefixes_to_strip = [\n \"vertex_ai/\",\n \"openrouter/\",\n \"together/\",\n \"groq/\",\n \"fireworks/\",\n \"perplexity/\",\n \"replicate/\",\n \"bedrock/\",\n \"azure/\",\n ]\n\n stripped_model = model_lower\n for prefix in prefixes_to_strip:\n if stripped_model.startswith(prefix):\n stripped_model = stripped_model[len(prefix) :]\n break\n\n # OpenAI model patterns\n openai_patterns = [\"gpt-\", \"o1-\", \"text-davinci\", \"text-curie\", \"text-babbage\", \"text-ada\"]\n if any(pattern in stripped_model for pattern in openai_patterns):\n return LLMProvider.OPENAI\n\n # Anthropic model patterns\n anthropic_patterns = [\"claude-\", \"haiku\", \"sonnet\", \"opus\"]\n if any(pattern in stripped_model for pattern in anthropic_patterns):\n return LLMProvider.ANTHROPIC\n\n # Google model patterns\n google_patterns = [\"gemini-\", \"gemini\", \"bison\", \"gecko\", \"palm\"]\n if any(pattern in stripped_model for pattern in google_patterns):\n return LLMProvider.GOOGLE\n\n # Ollama model patterns - popular models available in Ollama\n ollama_patterns = [\n \"llama\",\n \"mistral\",\n \"codellama\",\n \"deepseek\",\n \"qwen\",\n \"phi\",\n \"gemma\",\n \"wizardcoder\",\n \"starcoder\",\n \"codegemma\",\n \"solar\",\n \"nous-hermes\",\n \"openchat\",\n \"zephyr\",\n \"orca\",\n \"vicuna\",\n \"alpaca\",\n \"devstral\",\n ]\n if any(pattern in stripped_model for pattern in ollama_patterns):\n return LLMProvider.OLLAMA\n\n return None\n\n\ndef _is_placeholder_token(token: Optional[str]) -> bool:\n \"\"\"Check if a token is a placeholder that should be ignored.\"\"\"\n if not token:\n return True\n\n # Common placeholder patterns\n placeholder_patterns = [\n \"your_token_here\",\n \"your_api_key_here\",\n \"your_key_here\",\n \"replace_with_your_token\",\n \"sk-your_api_key_here\",\n \"ghp_your_token_here\",\n \"sk-ant-your_key\",\n ]\n\n token_lower = token.lower()\n return any(pattern in token_lower for pattern in placeholder_patterns)\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n temperature: float = 0.1\n api_base_url: Optional[str] = None # For local LLMs or custom OpenAI endpoints\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n # Output control\n quiet: bool = False # Suppress status output for plain mode\n # Priority filtering\n priority_filter: Optional[List[str]] = None # [\"high\", \"medium\", \"low\"] or subset\n max_review_size_mb: float = 5.0 # Default 5MB limit (was 1MB hardcoded)\n # Custom context profile\n profile: Optional[str] = None # Profile name to use\n profile_context: Optional[str] = None # Loaded profile context\n # Existing repository path (skips cloning when provided)\n repo_path: Optional[str] = None # Path to existing repository to use for analysis\n\n @classmethod\n def from_file(cls, config_path: Optional[str] = None, profile: Optional[str] = None, repo_path: Optional[str] = None) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\n\n Args:\n config_path: Path to config file\n profile: Profile name to load custom context from\n repo_path: Path to existing repository to use for analysis\n \"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n config_github_token = config_data.get(\"github\", {}).get(\"token\")\n if _is_placeholder_token(config_github_token):\n config_github_token = None # Treat placeholder as missing\n\n github_token = config_github_token or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\")\n\n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {})\n provider_str = llm_data.get(\"provider\") or os.getenv(\"LLM_PROVIDER\", \"anthropic\")\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n elif provider == LLMProvider.GOOGLE:\n default_model = \"gemini-2.5-flash\" # Updated to latest model\n api_key_env = \"KIT_GOOGLE_API_KEY or GOOGLE_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_GOOGLE_API_KEY\") or os.getenv(\"GOOGLE_API_KEY\")\n elif provider == LLMProvider.OLLAMA:\n default_model = \"qwen2.5-coder:latest\" # Latest code-specialized model\n api_key_env = \"None (Ollama doesn't require API keys)\"\n # Ollama doesn't need an API key, but we'll use a placeholder for consistency\n api_key = llm_data.get(\"api_key\", \"ollama\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n # Ollama doesn't require API keys, so skip validation for it\n if not api_key and provider != LLMProvider.OLLAMA:\n raise ValueError(\n f\"LLM API key required. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=str(api_key),\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n temperature=llm_data.get(\"temperature\", 0.1),\n api_base_url=llm_data.get(\"api_base_url\"),\n )\n\n # Set default base URLs for local providers\n if llm_config.provider == LLMProvider.OLLAMA and not llm_config.api_base_url:\n llm_config.api_base_url = \"http://localhost:11434\" # Default Ollama API endpoint\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n # Validate priority_filter if present in config\n priority_filter = review_data.get(\"priority_filter\", None)\n if priority_filter is not None:\n try:\n from .priority_utils import Priority\n\n priority_filter = Priority.validate_priorities(priority_filter)\n except (ValueError, TypeError) as e:\n raise ValueError(f\"Invalid priority_filter in config file: {e}. Valid priorities: high, medium, low\")\n\n # Load profile context if profile is specified\n profile_context = None\n if profile:\n try:\n from .profile_manager import ProfileManager\n\n profile_manager = ProfileManager()\n profile_obj = profile_manager.get_profile(profile)\n profile_context = profile_obj.context\n except Exception as e:\n raise ValueError(f\"Failed to load profile '{profile}': {e}\")\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n quiet=review_data.get(\"quiet\", False),\n priority_filter=priority_filter,\n max_review_size_mb=review_data.get(\"max_review_size_mb\", 5.0),\n profile=profile,\n profile_context=profile_context,\n repo_path=repo_path,\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\", \"google\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\", \"gemini-2.5-flash\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n \"temperature\": 0.1,\n # For custom OpenAI compatible providers (e.g., Together AI, OpenRouter, etc.)\n # \"api_base_url\": \"https://api.together.xyz/v1\", # Example: Together AI\n # \"api_base_url\": \"https://openrouter.ai/api/v1\", # Example: OpenRouter\n # \"api_base_url\": \"http://localhost:8000/v1\", # Example: Local OpenAI API server\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Priority filtering (can also be set via --priority CLI flag)\n # \"priority_filter\": [\"high\", \"medium\"], # Only show high and medium priority issues\n # \"priority_filter\": [\"high\"], # Only show high priority issues\n # Performance and safety limits\n \"max_review_size_mb\": 5.0, # Maximum review text size in MB (prevents DoS)\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n # Add a commented-out example for local Ollama usage\n local_ollama_example = \"\"\"\\\n# Example Ollama configuration (completely free local AI):\n# llm:\n# provider: ollama\n# model: \"qwen2.5-coder:latest\" # Or deepseek-r1:latest, gemma3:latest, etc.\n# api_base_url: \"http://localhost:11434\" # Default Ollama endpoint\n# api_key: \"ollama\" # Placeholder (Ollama doesn't use API keys)\n# max_tokens: 2000\n# temperature: 0.1\n\n# Example Google Gemini configuration:\n# llm:\n# provider: google\n# model: \"gemini-2.5-flash\" # Or \"gemini-2.5-pro\" for more complex reasoning, \"gemini-2.0-flash-lite\" for speed\n# api_key: \"AIzaSy...\" # Get from https://aistudio.google.com/apikey\n# max_tokens: 4000\n# temperature: 0.1\n\n# Example OpenAI compatible provider configurations:\n#\n# Together AI (https://together.ai/):\n# llm:\n# provider: openai\n# model: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"\n# api_key: \"your_together_api_key\"\n# api_base_url: \"https://api.together.xyz/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# OpenRouter (https://openrouter.ai/):\n# llm:\n# provider: openai\n# model: \"anthropic/claude-3.5-sonnet\"\n# api_key: \"your_openrouter_api_key\"\n# api_base_url: \"https://openrouter.ai/api/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Local OpenAI API server (e.g., text-generation-webui, vLLM):\n# llm:\n# provider: openai\n# model: \"local-model-name\"\n# api_key: \"not-used\" # Local servers often don't require API keys\n# api_base_url: \"http://localhost:8000/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Groq (https://groq.com/):\n# llm:\n# provider: openai\n# model: \"llama-3.3-70b-versatile\"\n# api_key: \"your_groq_api_key\"\n# api_base_url: \"https://api.groq.com/openai/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n\"\"\"\n\n with open(config_path, \"a\") as f:\n f.write(\"\\n\" + local_ollama_example)\n\n return config_path\n"}, "231475ac8ebdaf2fce58714e3fc766eb4fe7db01": {"t": 1785768284.3061886, "n": 15786, "h": "4364b2a02b749f53", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, List, Optional\n\nimport yaml\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n OLLAMA = \"ollama\"\n GOOGLE = \"google\"\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\ndef _detect_provider_from_model(model_name: str) -> Optional[LLMProvider]:\n \"\"\"Detect LLM provider from model name.\"\"\"\n model_lower = model_name.lower()\n\n # Strip common prefixes first\n prefixes_to_strip = [\n \"vertex_ai/\",\n \"openrouter/\",\n \"together/\",\n \"groq/\",\n \"fireworks/\",\n \"perplexity/\",\n \"replicate/\",\n \"bedrock/\",\n \"azure/\",\n ]\n\n stripped_model = model_lower\n for prefix in prefixes_to_strip:\n if stripped_model.startswith(prefix):\n stripped_model = stripped_model[len(prefix) :]\n break\n\n # OpenAI model patterns\n openai_patterns = [\"gpt-\", \"o1-\", \"text-davinci\", \"text-curie\", \"text-babbage\", \"text-ada\"]\n if any(pattern in stripped_model for pattern in openai_patterns):\n return LLMProvider.OPENAI\n\n # Anthropic model patterns\n anthropic_patterns = [\"claude-\", \"haiku\", \"sonnet\", \"opus\"]\n if any(pattern in stripped_model for pattern in anthropic_patterns):\n return LLMProvider.ANTHROPIC\n\n # Google model patterns\n google_patterns = [\"gemini-\", \"gemini\", \"bison\", \"gecko\", \"palm\"]\n if any(pattern in stripped_model for pattern in google_patterns):\n return LLMProvider.GOOGLE\n\n # Ollama model patterns - popular models available in Ollama\n ollama_patterns = [\n \"llama\",\n \"mistral\",\n \"codellama\",\n \"deepseek\",\n \"qwen\",\n \"phi\",\n \"gemma\",\n \"wizardcoder\",\n \"starcoder\",\n \"codegemma\",\n \"solar\",\n \"nous-hermes\",\n \"openchat\",\n \"zephyr\",\n \"orca\",\n \"vicuna\",\n \"alpaca\",\n \"devstral\",\n ]\n if any(pattern in stripped_model for pattern in ollama_patterns):\n return LLMProvider.OLLAMA\n\n return None\n\n\ndef _is_placeholder_token(token: Optional[str]) -> bool:\n \"\"\"Check if a token is a placeholder that should be ignored.\"\"\"\n if not token:\n return True\n\n # Common placeholder patterns\n placeholder_patterns = [\n \"your_token_here\",\n \"your_api_key_here\",\n \"your_key_here\",\n \"replace_with_your_token\",\n \"sk-your_api_key_here\",\n \"ghp_your_token_here\",\n \"sk-ant-your_key\",\n ]\n\n token_lower = token.lower()\n return any(pattern in token_lower for pattern in placeholder_patterns)\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n temperature: float = 0.1\n api_base_url: Optional[str] = None # For local LLMs or custom OpenAI endpoints\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n # Output control\n quiet: bool = False # Suppress status output for plain mode\n # Priority filtering\n priority_filter: Optional[List[str]] = None # [\"high\", \"medium\", \"low\"] or subset\n max_review_size_mb: float = 5.0 # Default 5MB limit (was 1MB hardcoded)\n # Custom context profile\n profile: Optional[str] = None # Profile name to use\n profile_context: Optional[str] = None # Loaded profile context\n\n @classmethod\n def from_file(cls, config_path: Optional[str] = None, profile: Optional[str] = None) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\n\n Args:\n config_path: Path to config file\n profile: Profile name to load custom context from\n \"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n config_github_token = config_data.get(\"github\", {}).get(\"token\")\n if _is_placeholder_token(config_github_token):\n config_github_token = None # Treat placeholder as missing\n\n github_token = config_github_token or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\")\n\n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {})\n provider_str = llm_data.get(\"provider\") or os.getenv(\"LLM_PROVIDER\", \"anthropic\")\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n elif provider == LLMProvider.GOOGLE:\n default_model = \"gemini-2.5-flash\" # Updated to latest model\n api_key_env = \"KIT_GOOGLE_API_KEY or GOOGLE_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_GOOGLE_API_KEY\") or os.getenv(\"GOOGLE_API_KEY\")\n elif provider == LLMProvider.OLLAMA:\n default_model = \"qwen2.5-coder:latest\" # Latest code-specialized model\n api_key_env = \"None (Ollama doesn't require API keys)\"\n # Ollama doesn't need an API key, but we'll use a placeholder for consistency\n api_key = llm_data.get(\"api_key\", \"ollama\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n # Ollama doesn't require API keys, so skip validation for it\n if not api_key and provider != LLMProvider.OLLAMA:\n raise ValueError(\n f\"LLM API key required. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=str(api_key),\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n temperature=llm_data.get(\"temperature\", 0.1),\n api_base_url=llm_data.get(\"api_base_url\"),\n )\n\n # Set default base URLs for local providers\n if llm_config.provider == LLMProvider.OLLAMA and not llm_config.api_base_url:\n llm_config.api_base_url = \"http://localhost:11434\" # Default Ollama API endpoint\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n # Validate priority_filter if present in config\n priority_filter = review_data.get(\"priority_filter\", None)\n if priority_filter is not None:\n try:\n from .priority_utils import Priority\n\n priority_filter = Priority.validate_priorities(priority_filter)\n except (ValueError, TypeError) as e:\n raise ValueError(f\"Invalid priority_filter in config file: {e}. Valid priorities: high, medium, low\")\n\n # Load profile context if profile is specified\n profile_context = None\n if profile:\n try:\n from .profile_manager import ProfileManager\n\n profile_manager = ProfileManager()\n profile_obj = profile_manager.get_profile(profile)\n profile_context = profile_obj.context\n except Exception as e:\n raise ValueError(f\"Failed to load profile '{profile}': {e}\")\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n quiet=review_data.get(\"quiet\", False),\n priority_filter=priority_filter,\n max_review_size_mb=review_data.get(\"max_review_size_mb\", 5.0),\n profile=profile,\n profile_context=profile_context,\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\", \"google\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\", \"gemini-2.5-flash\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n \"temperature\": 0.1,\n # For custom OpenAI compatible providers (e.g., Together AI, OpenRouter, etc.)\n # \"api_base_url\": \"https://api.together.xyz/v1\", # Example: Together AI\n # \"api_base_url\": \"https://openrouter.ai/api/v1\", # Example: OpenRouter\n # \"api_base_url\": \"http://localhost:8000/v1\", # Example: Local OpenAI API server\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Priority filtering (can also be set via --priority CLI flag)\n # \"priority_filter\": [\"high\", \"medium\"], # Only show high and medium priority issues\n # \"priority_filter\": [\"high\"], # Only show high priority issues\n # Performance and safety limits\n \"max_review_size_mb\": 5.0, # Maximum review text size in MB (prevents DoS)\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n # Add a commented-out example for local Ollama usage\n local_ollama_example = \"\"\"\\\n# Example Ollama configuration (completely free local AI):\n# llm:\n# provider: ollama\n# model: \"qwen2.5-coder:latest\" # Or deepseek-r1:latest, gemma3:latest, etc.\n# api_base_url: \"http://localhost:11434\" # Default Ollama endpoint\n# api_key: \"ollama\" # Placeholder (Ollama doesn't use API keys)\n# max_tokens: 2000\n# temperature: 0.1\n\n# Example Google Gemini configuration:\n# llm:\n# provider: google\n# model: \"gemini-2.5-flash\" # Or \"gemini-2.5-pro\" for more complex reasoning, \"gemini-2.0-flash-lite\" for speed\n# api_key: \"AIzaSy...\" # Get from https://aistudio.google.com/apikey\n# max_tokens: 4000\n# temperature: 0.1\n\n# Example OpenAI compatible provider configurations:\n#\n# Together AI (https://together.ai/):\n# llm:\n# provider: openai\n# model: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"\n# api_key: \"your_together_api_key\"\n# api_base_url: \"https://api.together.xyz/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# OpenRouter (https://openrouter.ai/):\n# llm:\n# provider: openai\n# model: \"anthropic/claude-3.5-sonnet\"\n# api_key: \"your_openrouter_api_key\"\n# api_base_url: \"https://openrouter.ai/api/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Local OpenAI API server (e.g., text-generation-webui, vLLM):\n# llm:\n# provider: openai\n# model: \"local-model-name\"\n# api_key: \"not-used\" # Local servers often don't require API keys\n# api_base_url: \"http://localhost:8000/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Groq (https://groq.com/):\n# llm:\n# provider: openai\n# model: \"llama-3.3-70b-versatile\"\n# api_key: \"your_groq_api_key\"\n# api_base_url: \"https://api.groq.com/openai/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n\"\"\"\n\n with open(config_path, \"a\") as f:\n f.write(\"\\n\" + local_ollama_example)\n\n return config_path\n"}, "0021b002dfaaf806a58d0e6cf6b77d5f5625cc26": {"t": 1785768285.5672297, "n": 14927, "h": "340b9c8be802d143", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, List, Optional\n\nimport yaml\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n OLLAMA = \"ollama\"\n GOOGLE = \"google\"\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\ndef _detect_provider_from_model(model_name: str) -> Optional[LLMProvider]:\n \"\"\"Detect LLM provider from model name.\"\"\"\n model_lower = model_name.lower()\n\n # Strip common prefixes first\n prefixes_to_strip = [\n \"vertex_ai/\",\n \"openrouter/\",\n \"together/\",\n \"groq/\",\n \"fireworks/\",\n \"perplexity/\",\n \"replicate/\",\n \"bedrock/\",\n \"azure/\",\n ]\n\n stripped_model = model_lower\n for prefix in prefixes_to_strip:\n if stripped_model.startswith(prefix):\n stripped_model = stripped_model[len(prefix) :]\n break\n\n # OpenAI model patterns\n openai_patterns = [\"gpt-\", \"o1-\", \"text-davinci\", \"text-curie\", \"text-babbage\", \"text-ada\"]\n if any(pattern in stripped_model for pattern in openai_patterns):\n return LLMProvider.OPENAI\n\n # Anthropic model patterns\n anthropic_patterns = [\"claude-\", \"haiku\", \"sonnet\", \"opus\"]\n if any(pattern in stripped_model for pattern in anthropic_patterns):\n return LLMProvider.ANTHROPIC\n\n # Google model patterns\n google_patterns = [\"gemini-\", \"gemini\", \"bison\", \"gecko\", \"palm\"]\n if any(pattern in stripped_model for pattern in google_patterns):\n return LLMProvider.GOOGLE\n\n # Ollama model patterns - popular models available in Ollama\n ollama_patterns = [\n \"llama\",\n \"mistral\",\n \"codellama\",\n \"deepseek\",\n \"qwen\",\n \"phi\",\n \"gemma\",\n \"wizardcoder\",\n \"starcoder\",\n \"codegemma\",\n \"solar\",\n \"nous-hermes\",\n \"openchat\",\n \"zephyr\",\n \"orca\",\n \"vicuna\",\n \"alpaca\",\n \"devstral\",\n ]\n if any(pattern in stripped_model for pattern in ollama_patterns):\n return LLMProvider.OLLAMA\n\n return None\n\n\ndef _is_placeholder_token(token: Optional[str]) -> bool:\n \"\"\"Check if a token is a placeholder that should be ignored.\"\"\"\n if not token:\n return True\n\n # Common placeholder patterns\n placeholder_patterns = [\n \"your_token_here\",\n \"your_api_key_here\",\n \"your_key_here\",\n \"replace_with_your_token\",\n \"sk-your_api_key_here\",\n \"ghp_your_token_here\",\n \"sk-ant-your_key\",\n ]\n\n token_lower = token.lower()\n return any(pattern in token_lower for pattern in placeholder_patterns)\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n temperature: float = 0.1\n api_base_url: Optional[str] = None # For local LLMs or custom OpenAI endpoints\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n # Output control\n quiet: bool = False # Suppress status output for plain mode\n # Priority filtering\n priority_filter: Optional[List[str]] = None # [\"high\", \"medium\", \"low\"] or subset\n max_review_size_mb: float = 5.0 # Default 5MB limit (was 1MB hardcoded)\n\n @classmethod\n def from_file(cls, config_path: Optional[str] = None) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n config_github_token = config_data.get(\"github\", {}).get(\"token\")\n if _is_placeholder_token(config_github_token):\n config_github_token = None # Treat placeholder as missing\n\n github_token = config_github_token or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\")\n\n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {})\n provider_str = llm_data.get(\"provider\") or os.getenv(\"LLM_PROVIDER\", \"anthropic\")\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n elif provider == LLMProvider.GOOGLE:\n default_model = \"gemini-2.5-flash\" # Updated to latest model\n api_key_env = \"KIT_GOOGLE_API_KEY or GOOGLE_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_GOOGLE_API_KEY\") or os.getenv(\"GOOGLE_API_KEY\")\n elif provider == LLMProvider.OLLAMA:\n default_model = \"qwen2.5-coder:latest\" # Latest code-specialized model\n api_key_env = \"None (Ollama doesn't require API keys)\"\n # Ollama doesn't need an API key, but we'll use a placeholder for consistency\n api_key = llm_data.get(\"api_key\", \"ollama\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n # Ollama doesn't require API keys, so skip validation for it\n if not api_key and provider != LLMProvider.OLLAMA:\n raise ValueError(\n f\"LLM API key required. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=str(api_key),\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n temperature=llm_data.get(\"temperature\", 0.1),\n api_base_url=llm_data.get(\"api_base_url\"),\n )\n\n # Set default base URLs for local providers\n if llm_config.provider == LLMProvider.OLLAMA and not llm_config.api_base_url:\n llm_config.api_base_url = \"http://localhost:11434\" # Default Ollama API endpoint\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n # Validate priority_filter if present in config\n priority_filter = review_data.get(\"priority_filter\", None)\n if priority_filter is not None:\n try:\n from .priority_utils import Priority\n\n priority_filter = Priority.validate_priorities(priority_filter)\n except (ValueError, TypeError) as e:\n raise ValueError(f\"Invalid priority_filter in config file: {e}. Valid priorities: high, medium, low\")\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n quiet=review_data.get(\"quiet\", False),\n priority_filter=priority_filter,\n max_review_size_mb=review_data.get(\"max_review_size_mb\", 5.0),\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\", \"google\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\", \"gemini-2.5-flash\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n \"temperature\": 0.1,\n # For custom OpenAI compatible providers (e.g., Together AI, OpenRouter, etc.)\n # \"api_base_url\": \"https://api.together.xyz/v1\", # Example: Together AI\n # \"api_base_url\": \"https://openrouter.ai/api/v1\", # Example: OpenRouter\n # \"api_base_url\": \"http://localhost:8000/v1\", # Example: Local OpenAI API server\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Priority filtering (can also be set via --priority CLI flag)\n # \"priority_filter\": [\"high\", \"medium\"], # Only show high and medium priority issues\n # \"priority_filter\": [\"high\"], # Only show high priority issues\n # Performance and safety limits\n \"max_review_size_mb\": 5.0, # Maximum review text size in MB (prevents DoS)\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n # Add a commented-out example for local Ollama usage\n local_ollama_example = \"\"\"\\\n# Example Ollama configuration (completely free local AI):\n# llm:\n# provider: ollama\n# model: \"qwen2.5-coder:latest\" # Or deepseek-r1:latest, gemma3:latest, etc.\n# api_base_url: \"http://localhost:11434\" # Default Ollama endpoint\n# api_key: \"ollama\" # Placeholder (Ollama doesn't use API keys)\n# max_tokens: 2000\n# temperature: 0.1\n\n# Example Google Gemini configuration:\n# llm:\n# provider: google\n# model: \"gemini-2.5-flash\" # Or \"gemini-2.5-pro\" for more complex reasoning, \"gemini-2.0-flash-lite\" for speed\n# api_key: \"AIzaSy...\" # Get from https://aistudio.google.com/apikey\n# max_tokens: 4000\n# temperature: 0.1\n\n# Example OpenAI compatible provider configurations:\n#\n# Together AI (https://together.ai/):\n# llm:\n# provider: openai\n# model: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"\n# api_key: \"your_together_api_key\"\n# api_base_url: \"https://api.together.xyz/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# OpenRouter (https://openrouter.ai/):\n# llm:\n# provider: openai\n# model: \"anthropic/claude-3.5-sonnet\"\n# api_key: \"your_openrouter_api_key\"\n# api_base_url: \"https://openrouter.ai/api/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Local OpenAI API server (e.g., text-generation-webui, vLLM):\n# llm:\n# provider: openai\n# model: \"local-model-name\"\n# api_key: \"not-used\" # Local servers often don't require API keys\n# api_base_url: \"http://localhost:8000/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Groq (https://groq.com/):\n# llm:\n# provider: openai\n# model: \"llama-3.3-70b-versatile\"\n# api_key: \"your_groq_api_key\"\n# api_base_url: \"https://api.groq.com/openai/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n\"\"\"\n\n with open(config_path, \"a\") as f:\n f.write(\"\\n\" + local_ollama_example)\n\n return config_path\n"}, "2e03af8feacac66ea36ada46b9efb28e0fb4a2c8": {"t": 1785768287.0670042, "n": 13718, "h": "459877072caec087", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, Optional\n\nimport yaml\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n OLLAMA = \"ollama\"\n GOOGLE = \"google\"\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\ndef _detect_provider_from_model(model_name: str) -> Optional[LLMProvider]:\n \"\"\"Detect LLM provider from model name.\"\"\"\n model_lower = model_name.lower()\n\n # Strip common prefixes first\n prefixes_to_strip = [\n \"vertex_ai/\",\n \"openrouter/\",\n \"together/\",\n \"groq/\",\n \"fireworks/\",\n \"perplexity/\",\n \"replicate/\",\n \"bedrock/\",\n \"azure/\",\n ]\n\n stripped_model = model_lower\n for prefix in prefixes_to_strip:\n if stripped_model.startswith(prefix):\n stripped_model = stripped_model[len(prefix) :]\n break\n\n # OpenAI model patterns\n openai_patterns = [\"gpt-\", \"o1-\", \"text-davinci\", \"text-curie\", \"text-babbage\", \"text-ada\"]\n if any(pattern in stripped_model for pattern in openai_patterns):\n return LLMProvider.OPENAI\n\n # Anthropic model patterns\n anthropic_patterns = [\"claude-\", \"haiku\", \"sonnet\", \"opus\"]\n if any(pattern in stripped_model for pattern in anthropic_patterns):\n return LLMProvider.ANTHROPIC\n\n # Google model patterns\n google_patterns = [\"gemini-\", \"gemini\", \"bison\", \"gecko\", \"palm\"]\n if any(pattern in stripped_model for pattern in google_patterns):\n return LLMProvider.GOOGLE\n\n # Ollama model patterns - popular models available in Ollama\n ollama_patterns = [\n \"llama\",\n \"mistral\",\n \"codellama\",\n \"deepseek\",\n \"qwen\",\n \"phi\",\n \"gemma\",\n \"wizardcoder\",\n \"starcoder\",\n \"codegemma\",\n \"solar\",\n \"nous-hermes\",\n \"openchat\",\n \"zephyr\",\n \"orca\",\n \"vicuna\",\n \"alpaca\",\n \"devstral\",\n ]\n if any(pattern in stripped_model for pattern in ollama_patterns):\n return LLMProvider.OLLAMA\n\n return None\n\n\ndef _is_placeholder_token(token: Optional[str]) -> bool:\n \"\"\"Check if a token is a placeholder that should be ignored.\"\"\"\n if not token:\n return True\n\n # Common placeholder patterns\n placeholder_patterns = [\n \"your_token_here\",\n \"your_api_key_here\",\n \"your_key_here\",\n \"replace_with_your_token\",\n \"sk-your_api_key_here\",\n \"ghp_your_token_here\",\n \"sk-ant-your_key\",\n ]\n\n token_lower = token.lower()\n return any(pattern in token_lower for pattern in placeholder_patterns)\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n temperature: float = 0.1\n api_base_url: Optional[str] = None # For local LLMs or custom OpenAI endpoints\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n # Output control\n quiet: bool = False # Suppress status output for plain mode\n\n @classmethod\n def from_file(cls, config_path: Optional[str] = None) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n config_github_token = config_data.get(\"github\", {}).get(\"token\")\n if _is_placeholder_token(config_github_token):\n config_github_token = None # Treat placeholder as missing\n\n github_token = config_github_token or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\")\n\n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {})\n provider_str = llm_data.get(\"provider\") or os.getenv(\"LLM_PROVIDER\", \"anthropic\")\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n elif provider == LLMProvider.GOOGLE:\n default_model = \"gemini-2.5-flash\" # Updated to latest model\n api_key_env = \"KIT_GOOGLE_API_KEY or GOOGLE_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_GOOGLE_API_KEY\") or os.getenv(\"GOOGLE_API_KEY\")\n elif provider == LLMProvider.OLLAMA:\n default_model = \"qwen2.5-coder:latest\" # Latest code-specialized model\n api_key_env = \"None (Ollama doesn't require API keys)\"\n # Ollama doesn't need an API key, but we'll use a placeholder for consistency\n api_key = llm_data.get(\"api_key\", \"ollama\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n # Ollama doesn't require API keys, so skip validation for it\n if not api_key and provider != LLMProvider.OLLAMA:\n raise ValueError(\n f\"LLM API key required. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=str(api_key),\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n temperature=llm_data.get(\"temperature\", 0.1),\n api_base_url=llm_data.get(\"api_base_url\"),\n )\n\n # Set default base URLs for local providers\n if llm_config.provider == LLMProvider.OLLAMA and not llm_config.api_base_url:\n llm_config.api_base_url = \"http://localhost:11434\" # Default Ollama API endpoint\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n quiet=review_data.get(\"quiet\", False),\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\", \"google\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\", \"gemini-2.5-flash\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n \"temperature\": 0.1,\n # For custom OpenAI compatible providers (e.g., Together AI, OpenRouter, etc.)\n # \"api_base_url\": \"https://api.together.xyz/v1\", # Example: Together AI\n # \"api_base_url\": \"https://openrouter.ai/api/v1\", # Example: OpenRouter\n # \"api_base_url\": \"http://localhost:8000/v1\", # Example: Local OpenAI API server\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n # Add a commented-out example for local Ollama usage\n local_ollama_example = \"\"\"\\\n# Example Ollama configuration (completely free local AI):\n# llm:\n# provider: ollama\n# model: \"qwen2.5-coder:latest\" # Or deepseek-r1:latest, gemma3:latest, etc.\n# api_base_url: \"http://localhost:11434\" # Default Ollama endpoint\n# api_key: \"ollama\" # Placeholder (Ollama doesn't use API keys)\n# max_tokens: 2000\n# temperature: 0.1\n\n# Example Google Gemini configuration:\n# llm:\n# provider: google\n# model: \"gemini-2.5-flash\" # Or \"gemini-2.5-pro\" for more complex reasoning, \"gemini-2.0-flash-lite\" for speed\n# api_key: \"AIzaSy...\" # Get from https://aistudio.google.com/apikey\n# max_tokens: 4000\n# temperature: 0.1\n\n# Example OpenAI compatible provider configurations:\n#\n# Together AI (https://together.ai/):\n# llm:\n# provider: openai\n# model: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"\n# api_key: \"your_together_api_key\"\n# api_base_url: \"https://api.together.xyz/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# OpenRouter (https://openrouter.ai/):\n# llm:\n# provider: openai\n# model: \"anthropic/claude-3.5-sonnet\"\n# api_key: \"your_openrouter_api_key\"\n# api_base_url: \"https://openrouter.ai/api/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Local OpenAI API server (e.g., text-generation-webui, vLLM):\n# llm:\n# provider: openai\n# model: \"local-model-name\"\n# api_key: \"not-used\" # Local servers often don't require API keys\n# api_base_url: \"http://localhost:8000/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Groq (https://groq.com/):\n# llm:\n# provider: openai\n# model: \"llama-3.3-70b-versatile\"\n# api_key: \"your_groq_api_key\"\n# api_base_url: \"https://api.groq.com/openai/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n\"\"\"\n\n with open(config_path, \"a\") as f:\n f.write(\"\\n\" + local_ollama_example)\n\n return config_path\n"}, "5c37c910a615dbe7ca3fdd233a824c2a18e1d1ab": {"t": 1785768288.3369408, "n": 12744, "h": "d1ac57316d9075b3", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, Optional\n\nimport yaml\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n OLLAMA = \"ollama\" # Dedicated Ollama provider\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\ndef _detect_provider_from_model(model_name: str) -> Optional[LLMProvider]:\n \"\"\"Detect LLM provider from model name.\"\"\"\n model_lower = model_name.lower()\n\n # Strip common prefixes first\n prefixes_to_strip = [\n \"vertex_ai/\",\n \"openrouter/\",\n \"together/\",\n \"groq/\",\n \"fireworks/\",\n \"perplexity/\",\n \"replicate/\",\n \"bedrock/\",\n \"azure/\",\n ]\n\n stripped_model = model_lower\n for prefix in prefixes_to_strip:\n if stripped_model.startswith(prefix):\n stripped_model = stripped_model[len(prefix):]\n break\n\n # OpenAI model patterns\n openai_patterns = [\n \"gpt-\", \"o1-\", \"text-davinci\", \"text-curie\", \"text-babbage\", \"text-ada\"\n ]\n if any(pattern in stripped_model for pattern in openai_patterns):\n return LLMProvider.OPENAI\n\n # Anthropic model patterns\n anthropic_patterns = [\"claude-\", \"haiku\", \"sonnet\", \"opus\"]\n if any(pattern in stripped_model for pattern in anthropic_patterns):\n return LLMProvider.ANTHROPIC\n\n # Ollama model patterns - popular models available in Ollama\n ollama_patterns = [\n \"llama\",\n \"mistral\",\n \"codellama\",\n \"deepseek\",\n \"qwen\",\n \"phi\",\n \"gemma\",\n \"wizardcoder\",\n \"starcoder\",\n \"codegemma\",\n \"solar\",\n \"nous-hermes\",\n \"openchat\",\n \"zephyr\",\n \"orca\",\n \"vicuna\",\n \"alpaca\",\n \"devstral\",\n ]\n if any(pattern in stripped_model for pattern in ollama_patterns):\n return LLMProvider.OLLAMA\n\n return None\n\n\ndef _is_placeholder_token(token: Optional[str]) -> bool:\n \"\"\"Check if a token is a placeholder that should be ignored.\"\"\"\n if not token:\n return True\n\n # Common placeholder patterns\n placeholder_patterns = [\n \"your_token_here\",\n \"your_api_key_here\",\n \"your_key_here\",\n \"replace_with_your_token\",\n \"sk-your_api_key_here\",\n \"ghp_your_token_here\",\n \"sk-ant-your_key\",\n ]\n\n token_lower = token.lower()\n return any(pattern in token_lower for pattern in placeholder_patterns)\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n temperature: float = 0.1\n api_base_url: Optional[str] = None # For local LLMs or custom OpenAI endpoints\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n # Output control\n quiet: bool = False # Suppress status output for plain mode\n\n @classmethod\n def from_file(cls, config_path: Optional[str] = None) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n config_github_token = config_data.get(\"github\", {}).get(\"token\")\n if _is_placeholder_token(config_github_token):\n config_github_token = None # Treat placeholder as missing\n\n github_token = config_github_token or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\")\n\n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {})\n provider_str = llm_data.get(\"provider\") or os.getenv(\"LLM_PROVIDER\", \"anthropic\")\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n elif provider == LLMProvider.OLLAMA:\n default_model = \"qwen2.5-coder:latest\" # Latest code-specialized model\n api_key_env = \"None (Ollama doesn't require API keys)\"\n # Ollama doesn't need an API key, but we'll use a placeholder for consistency\n api_key = llm_data.get(\"api_key\", \"ollama\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n # Ollama doesn't require API keys, so skip validation for it\n if not api_key and provider != LLMProvider.OLLAMA:\n raise ValueError(\n f\"LLM API key required. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=str(api_key),\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n temperature=llm_data.get(\"temperature\", 0.1),\n api_base_url=llm_data.get(\"api_base_url\"),\n )\n\n # Set default base URLs for local providers\n if llm_config.provider == LLMProvider.OLLAMA and not llm_config.api_base_url:\n llm_config.api_base_url = \"http://localhost:11434\" # Default Ollama API endpoint\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n quiet=review_data.get(\"quiet\", False),\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n \"temperature\": 0.1,\n # For custom OpenAI compatible providers (e.g., Together AI, OpenRouter, etc.)\n # \"api_base_url\": \"https://api.together.xyz/v1\", # Example: Together AI\n # \"api_base_url\": \"https://openrouter.ai/api/v1\", # Example: OpenRouter\n # \"api_base_url\": \"http://localhost:8000/v1\", # Example: Local OpenAI API server\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n # Add a commented-out example for local Ollama usage\n local_ollama_example = \"\"\"\\\n# Example Ollama configuration (completely free local AI):\n# llm:\n# provider: ollama\n# model: \"qwen2.5-coder:latest\" # Or deepseek-r1:latest, gemma3:latest, etc.\n# api_base_url: \"http://localhost:11434\" # Default Ollama endpoint\n# api_key: \"ollama\" # Placeholder (Ollama doesn't use API keys)\n# max_tokens: 2000\n# temperature: 0.1\n\n# Example OpenAI compatible provider configurations:\n#\n# Together AI (https://together.ai/):\n# llm:\n# provider: openai\n# model: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"\n# api_key: \"your_together_api_key\"\n# api_base_url: \"https://api.together.xyz/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# OpenRouter (https://openrouter.ai/):\n# llm:\n# provider: openai\n# model: \"anthropic/claude-3.5-sonnet\"\n# api_key: \"your_openrouter_api_key\"\n# api_base_url: \"https://openrouter.ai/api/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Local OpenAI API server (e.g., text-generation-webui, vLLM):\n# llm:\n# provider: openai\n# model: \"local-model-name\"\n# api_key: \"not-used\" # Local servers often don't require API keys\n# api_base_url: \"http://localhost:8000/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Groq (https://groq.com/):\n# llm:\n# provider: openai\n# model: \"llama-3.3-70b-versatile\"\n# api_key: \"your_groq_api_key\"\n# api_base_url: \"https://api.groq.com/openai/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n\"\"\"\n\n with open(config_path, \"a\") as f:\n f.write(\"\\n\" + local_ollama_example)\n\n return config_path\n"}, "b54f8c803c6eeee11b463e13f4ceeff5fffb174d": {"t": 1785768289.8995366, "n": 12754, "h": "1392c0bb9be1dc50", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, Optional\n\nimport yaml\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n OLLAMA = \"ollama\" # Dedicated Ollama provider\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\ndef _detect_provider_from_model(model_name: str) -> Optional[LLMProvider]:\n \"\"\"Detect LLM provider from model name.\"\"\"\n model_lower = model_name.lower()\n \n # Strip common prefixes first\n prefixes_to_strip = [\n \"vertex_ai/\",\n \"openrouter/\", \n \"together/\",\n \"groq/\",\n \"fireworks/\",\n \"perplexity/\",\n \"replicate/\",\n \"bedrock/\",\n \"azure/\",\n ]\n \n stripped_model = model_lower\n for prefix in prefixes_to_strip:\n if stripped_model.startswith(prefix):\n stripped_model = stripped_model[len(prefix):]\n break\n\n # OpenAI model patterns\n openai_patterns = [\n \"gpt-\", \"o1-\", \"text-davinci\", \"text-curie\", \"text-babbage\", \"text-ada\"\n ]\n if any(pattern in stripped_model for pattern in openai_patterns):\n return LLMProvider.OPENAI\n\n # Anthropic model patterns\n anthropic_patterns = [\"claude-\", \"haiku\", \"sonnet\", \"opus\"]\n if any(pattern in stripped_model for pattern in anthropic_patterns):\n return LLMProvider.ANTHROPIC\n\n # Ollama model patterns - popular models available in Ollama\n ollama_patterns = [\n \"llama\",\n \"mistral\",\n \"codellama\",\n \"deepseek\",\n \"qwen\",\n \"phi\",\n \"gemma\",\n \"wizardcoder\",\n \"starcoder\",\n \"codegemma\",\n \"solar\",\n \"nous-hermes\",\n \"openchat\",\n \"zephyr\",\n \"orca\",\n \"vicuna\",\n \"alpaca\",\n \"devstral\",\n ]\n if any(pattern in stripped_model for pattern in ollama_patterns):\n return LLMProvider.OLLAMA\n\n return None\n\n\ndef _is_placeholder_token(token: Optional[str]) -> bool:\n \"\"\"Check if a token is a placeholder that should be ignored.\"\"\"\n if not token:\n return True\n\n # Common placeholder patterns\n placeholder_patterns = [\n \"your_token_here\",\n \"your_api_key_here\",\n \"your_key_here\",\n \"replace_with_your_token\",\n \"sk-your_api_key_here\",\n \"ghp_your_token_here\",\n \"sk-ant-your_key\",\n ]\n\n token_lower = token.lower()\n return any(pattern in token_lower for pattern in placeholder_patterns)\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n temperature: float = 0.1\n api_base_url: Optional[str] = None # For local LLMs or custom OpenAI endpoints\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n # Output control\n quiet: bool = False # Suppress status output for plain mode\n\n @classmethod\n def from_file(cls, config_path: Optional[str] = None) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n config_github_token = config_data.get(\"github\", {}).get(\"token\")\n if _is_placeholder_token(config_github_token):\n config_github_token = None # Treat placeholder as missing\n\n github_token = config_github_token or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\")\n\n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {})\n provider_str = llm_data.get(\"provider\") or os.getenv(\"LLM_PROVIDER\", \"anthropic\")\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n elif provider == LLMProvider.OLLAMA:\n default_model = \"qwen2.5-coder:latest\" # Latest code-specialized model\n api_key_env = \"None (Ollama doesn't require API keys)\"\n # Ollama doesn't need an API key, but we'll use a placeholder for consistency\n api_key = llm_data.get(\"api_key\", \"ollama\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n # Ollama doesn't require API keys, so skip validation for it\n if not api_key and provider != LLMProvider.OLLAMA:\n raise ValueError(\n f\"LLM API key required. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=str(api_key),\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n temperature=llm_data.get(\"temperature\", 0.1),\n api_base_url=llm_data.get(\"api_base_url\"),\n )\n\n # Set default base URLs for local providers\n if llm_config.provider == LLMProvider.OLLAMA and not llm_config.api_base_url:\n llm_config.api_base_url = \"http://localhost:11434\" # Default Ollama API endpoint\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n quiet=review_data.get(\"quiet\", False),\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n \"temperature\": 0.1,\n # For custom OpenAI compatible providers (e.g., Together AI, OpenRouter, etc.)\n # \"api_base_url\": \"https://api.together.xyz/v1\", # Example: Together AI\n # \"api_base_url\": \"https://openrouter.ai/api/v1\", # Example: OpenRouter\n # \"api_base_url\": \"http://localhost:8000/v1\", # Example: Local OpenAI API server\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n # Add a commented-out example for local Ollama usage\n local_ollama_example = \"\"\"\\\n# Example Ollama configuration (completely free local AI):\n# llm:\n# provider: ollama\n# model: \"qwen2.5-coder:latest\" # Or deepseek-r1:latest, gemma3:latest, etc.\n# api_base_url: \"http://localhost:11434\" # Default Ollama endpoint\n# api_key: \"ollama\" # Placeholder (Ollama doesn't use API keys)\n# max_tokens: 2000\n# temperature: 0.1\n\n# Example OpenAI compatible provider configurations:\n# \n# Together AI (https://together.ai/):\n# llm:\n# provider: openai\n# model: \"meta-llama/Llama-3.3-70B-Instruct-Turbo\"\n# api_key: \"your_together_api_key\"\n# api_base_url: \"https://api.together.xyz/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# OpenRouter (https://openrouter.ai/):\n# llm:\n# provider: openai\n# model: \"anthropic/claude-3.5-sonnet\"\n# api_key: \"your_openrouter_api_key\"\n# api_base_url: \"https://openrouter.ai/api/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Local OpenAI API server (e.g., text-generation-webui, vLLM):\n# llm:\n# provider: openai\n# model: \"local-model-name\"\n# api_key: \"not-used\" # Local servers often don't require API keys\n# api_base_url: \"http://localhost:8000/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n#\n# Groq (https://groq.com/):\n# llm:\n# provider: openai\n# model: \"llama-3.3-70b-versatile\"\n# api_key: \"your_groq_api_key\"\n# api_base_url: \"https://api.groq.com/openai/v1\"\n# max_tokens: 4000\n# temperature: 0.1\n\"\"\"\n\n with open(config_path, \"a\") as f:\n f.write(\"\\n\" + local_ollama_example)\n\n return config_path\n"}, "b5a296aeb48a0aa415d04610e4ce95bd25c2b93e": {"t": 1785768291.3452334, "n": 10855, "h": "ad6367bdcc23ac1b", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, Optional\n\nimport yaml\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n OLLAMA = \"ollama\" # Dedicated Ollama provider\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\ndef _detect_provider_from_model(model_name: str) -> Optional[LLMProvider]:\n \"\"\"Detect LLM provider from model name.\"\"\"\n model_lower = model_name.lower()\n\n # OpenAI model patterns\n openai_patterns = [\"gpt-\", \"o1-\", \"text-davinci\", \"text-curie\", \"text-babbage\", \"text-ada\"]\n if any(pattern in model_lower for pattern in openai_patterns):\n return LLMProvider.OPENAI\n\n # Anthropic model patterns\n anthropic_patterns = [\"claude-\", \"haiku\", \"sonnet\", \"opus\"]\n if any(pattern in model_lower for pattern in anthropic_patterns):\n return LLMProvider.ANTHROPIC\n\n # Ollama model patterns - popular models available in Ollama\n ollama_patterns = [\n \"llama\",\n \"mistral\",\n \"codellama\",\n \"deepseek\",\n \"qwen\",\n \"phi\",\n \"gemma\",\n \"wizardcoder\",\n \"starcoder\",\n \"codegemma\",\n \"solar\",\n \"nous-hermes\",\n \"openchat\",\n \"zephyr\",\n \"orca\",\n \"vicuna\",\n \"alpaca\",\n \"devstral\",\n ]\n if any(pattern in model_lower for pattern in ollama_patterns):\n return LLMProvider.OLLAMA\n\n return None\n\n\ndef _is_placeholder_token(token: Optional[str]) -> bool:\n \"\"\"Check if a token is a placeholder that should be ignored.\"\"\"\n if not token:\n return True\n\n # Common placeholder patterns\n placeholder_patterns = [\n \"your_token_here\",\n \"your_api_key_here\",\n \"your_key_here\",\n \"replace_with_your_token\",\n \"sk-your_api_key_here\",\n \"ghp_your_token_here\",\n \"sk-ant-your_key\",\n ]\n\n token_lower = token.lower()\n return any(pattern in token_lower for pattern in placeholder_patterns)\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n temperature: float = 0.1\n api_base_url: Optional[str] = None # For local LLMs or custom OpenAI endpoints\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n # Output control\n quiet: bool = False # Suppress status output for plain mode\n\n @classmethod\n def from_file(cls, config_path: Optional[str] = None) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n config_github_token = config_data.get(\"github\", {}).get(\"token\")\n if _is_placeholder_token(config_github_token):\n config_github_token = None # Treat placeholder as missing\n\n github_token = config_github_token or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\")\n\n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {})\n provider_str = llm_data.get(\"provider\") or os.getenv(\"LLM_PROVIDER\", \"anthropic\")\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n elif provider == LLMProvider.OLLAMA:\n default_model = \"qwen2.5-coder:latest\" # Latest code-specialized model\n api_key_env = \"None (Ollama doesn't require API keys)\"\n # Ollama doesn't need an API key, but we'll use a placeholder for consistency\n api_key = llm_data.get(\"api_key\", \"ollama\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n # Ollama doesn't require API keys, so skip validation for it\n if not api_key and provider != LLMProvider.OLLAMA:\n raise ValueError(\n f\"LLM API key required. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=str(api_key),\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n temperature=llm_data.get(\"temperature\", 0.1),\n api_base_url=llm_data.get(\"api_base_url\"),\n )\n\n # Set default base URLs for local providers\n if llm_config.provider == LLMProvider.OLLAMA and not llm_config.api_base_url:\n llm_config.api_base_url = \"http://localhost:11434\" # Default Ollama API endpoint\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n quiet=review_data.get(\"quiet\", False),\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n \"temperature\": 0.1,\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n # Add a commented-out example for local Ollama usage\n local_ollama_example = \"\"\"\\\n# Example Ollama configuration (completely free local AI):\n# llm:\n# provider: ollama\n# model: \"qwen2.5-coder:latest\" # Or deepseek-r1:latest, gemma3:latest, etc.\n# api_base_url: \"http://localhost:11434\" # Default Ollama endpoint\n# api_key: \"ollama\" # Placeholder (Ollama doesn't use API keys)\n# max_tokens: 2000\n# temperature: 0.1\n\"\"\"\n\n with open(config_path, \"a\") as f:\n f.write(\"\\n\" + local_ollama_example)\n\n return config_path\n"}, "9414b09d335f09375875ef987a19e990b396eb04": {"t": 1785768292.5445333, "n": 10718, "h": "d99a8dcf5acce919", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, Optional\n\nimport yaml\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n OLLAMA = \"ollama\" # Dedicated Ollama provider\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\ndef _detect_provider_from_model(model_name: str) -> Optional[LLMProvider]:\n \"\"\"Detect LLM provider from model name.\"\"\"\n model_lower = model_name.lower()\n\n # OpenAI model patterns\n openai_patterns = [\"gpt-\", \"o1-\", \"text-davinci\", \"text-curie\", \"text-babbage\", \"text-ada\"]\n if any(pattern in model_lower for pattern in openai_patterns):\n return LLMProvider.OPENAI\n\n # Anthropic model patterns\n anthropic_patterns = [\"claude-\", \"haiku\", \"sonnet\", \"opus\"]\n if any(pattern in model_lower for pattern in anthropic_patterns):\n return LLMProvider.ANTHROPIC\n\n # Ollama model patterns - popular models available in Ollama\n ollama_patterns = [\n \"llama\",\n \"mistral\",\n \"codellama\",\n \"deepseek\",\n \"qwen\",\n \"phi\",\n \"gemma\",\n \"wizardcoder\",\n \"starcoder\",\n \"codegemma\",\n \"solar\",\n \"nous-hermes\",\n \"openchat\",\n \"zephyr\",\n \"orca\",\n \"vicuna\",\n \"alpaca\",\n \"devstral\",\n ]\n if any(pattern in model_lower for pattern in ollama_patterns):\n return LLMProvider.OLLAMA\n\n return None\n\n\ndef _is_placeholder_token(token: Optional[str]) -> bool:\n \"\"\"Check if a token is a placeholder that should be ignored.\"\"\"\n if not token:\n return True\n\n # Common placeholder patterns\n placeholder_patterns = [\n \"your_token_here\",\n \"your_api_key_here\",\n \"your_key_here\",\n \"replace_with_your_token\",\n \"sk-your_api_key_here\",\n \"ghp_your_token_here\",\n \"sk-ant-your_key\",\n ]\n\n token_lower = token.lower()\n return any(pattern in token_lower for pattern in placeholder_patterns)\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n temperature: float = 0.1\n api_base_url: Optional[str] = None # For local LLMs or custom OpenAI endpoints\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n\n @classmethod\n def from_file(cls, config_path: Optional[str] = None) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n config_github_token = config_data.get(\"github\", {}).get(\"token\")\n if _is_placeholder_token(config_github_token):\n config_github_token = None # Treat placeholder as missing\n\n github_token = config_github_token or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\")\n\n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {})\n provider_str = llm_data.get(\"provider\") or os.getenv(\"LLM_PROVIDER\", \"anthropic\")\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n elif provider == LLMProvider.OLLAMA:\n default_model = \"qwen2.5-coder:latest\" # Latest code-specialized model\n api_key_env = \"None (Ollama doesn't require API keys)\"\n # Ollama doesn't need an API key, but we'll use a placeholder for consistency\n api_key = llm_data.get(\"api_key\", \"ollama\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n # Ollama doesn't require API keys, so skip validation for it\n if not api_key and provider != LLMProvider.OLLAMA:\n raise ValueError(\n f\"LLM API key required. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=str(api_key),\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n temperature=llm_data.get(\"temperature\", 0.1),\n api_base_url=llm_data.get(\"api_base_url\"),\n )\n\n # Set default base URLs for local providers\n if llm_config.provider == LLMProvider.OLLAMA and not llm_config.api_base_url:\n llm_config.api_base_url = \"http://localhost:11434\" # Default Ollama API endpoint\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n \"temperature\": 0.1,\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n # Add a commented-out example for local Ollama usage\n local_ollama_example = \"\"\"\\\n# Example Ollama configuration (completely free local AI):\n# llm:\n# provider: ollama\n# model: \"qwen2.5-coder:latest\" # Or deepseek-r1:latest, gemma3:latest, etc.\n# api_base_url: \"http://localhost:11434\" # Default Ollama endpoint\n# api_key: \"ollama\" # Placeholder (Ollama doesn't use API keys)\n# max_tokens: 2000\n# temperature: 0.1\n\"\"\"\n\n with open(config_path, \"a\") as f:\n f.write(\"\\n\" + local_ollama_example)\n\n return config_path\n"}, "aad99e052f330587a10fec8d5d43a206aa6d1429": {"t": 1785768293.9909265, "n": 8767, "h": "02e281c2f84b095e", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, Optional\n\nimport yaml\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\ndef _detect_provider_from_model(model_name: str) -> Optional[LLMProvider]:\n \"\"\"Detect LLM provider from model name.\"\"\"\n model_lower = model_name.lower()\n\n # OpenAI model patterns\n openai_patterns = [\"gpt-\", \"o1-\", \"text-davinci\", \"text-curie\", \"text-babbage\", \"text-ada\"]\n if any(pattern in model_lower for pattern in openai_patterns):\n return LLMProvider.OPENAI\n\n # Anthropic model patterns\n anthropic_patterns = [\"claude-\", \"haiku\", \"sonnet\", \"opus\"]\n if any(pattern in model_lower for pattern in anthropic_patterns):\n return LLMProvider.ANTHROPIC\n\n return None\n\n\ndef _is_placeholder_token(token: Optional[str]) -> bool:\n \"\"\"Check if a token is a placeholder that should be ignored.\"\"\"\n if not token:\n return True\n\n # Common placeholder patterns\n placeholder_patterns = [\n \"your_token_here\",\n \"your_api_key_here\",\n \"your_key_here\",\n \"replace_with_your_token\",\n \"sk-your_api_key_here\",\n \"ghp_your_token_here\",\n \"sk-ant-your_key\",\n ]\n\n token_lower = token.lower()\n return any(pattern in token_lower for pattern in placeholder_patterns)\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n temperature: float = 0.1\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n\n @classmethod\n def from_file(cls, config_path: Optional[str] = None) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n config_github_token = config_data.get(\"github\", {}).get(\"token\")\n if _is_placeholder_token(config_github_token):\n config_github_token = None # Treat placeholder as missing\n\n github_token = config_github_token or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\")\n\n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {})\n provider_str = llm_data.get(\"provider\") or os.getenv(\"LLM_PROVIDER\", \"anthropic\")\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n config_api_key = llm_data.get(\"api_key\")\n if _is_placeholder_token(config_api_key):\n config_api_key = None # Treat placeholder as missing\n api_key = config_api_key or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n if not api_key:\n raise ValueError(\n f\"LLM API key required. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=api_key,\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n temperature=llm_data.get(\"temperature\", 0.1),\n )\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n \"temperature\": 0.1,\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n return config_path\n"}, "6e523becaabd22cbf84d05a695feedf32a3e4515": {"t": 1785768295.166003, "n": 7122, "h": "6622ed80379651c0", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, Optional\n\nimport yaml\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n temperature: float = 0.1\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n\n @classmethod\n def from_file(cls, config_path: Optional[str] = None) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n github_token = (\n config_data.get(\"github\", {}).get(\"token\") or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\")\n )\n\n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {})\n provider_str = llm_data.get(\"provider\") or os.getenv(\"LLM_PROVIDER\", \"anthropic\")\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n api_key = llm_data.get(\"api_key\") or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n api_key = llm_data.get(\"api_key\") or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n if not api_key:\n raise ValueError(\n f\"LLM API key required. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=api_key,\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n temperature=llm_data.get(\"temperature\", 0.1),\n )\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n \"temperature\": 0.1,\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n return config_path\n"}, "edbd43e4343e726bd54f2dd4e10c64821fd09cbd": {"t": 1785768296.6361146, "n": 7154, "h": "95f564a46c64f143", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, Optional\n\nimport yaml\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n temperature: float = 0.1\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n\n @classmethod\n def from_file(cls, config_path: Optional[str] = None) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data: Dict = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n github_token = (\n config_data.get(\"github\", {}).get(\"token\")\n or os.getenv(\"KIT_GITHUB_TOKEN\")\n or os.getenv(\"GITHUB_TOKEN\")\n )\n \n if not github_token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n github_config = GitHubConfig(\n token=github_token,\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {})\n provider_str = llm_data.get(\"provider\") or os.getenv(\"LLM_PROVIDER\", \"anthropic\")\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n api_key = llm_data.get(\"api_key\") or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n api_key = llm_data.get(\"api_key\") or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n if not api_key:\n raise ValueError(\n f\"LLM API key required. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=api_key,\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n temperature=llm_data.get(\"temperature\", 0.1),\n )\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n \"temperature\": 0.1,\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n return config_path\n"}, "2b0927750c2bc27b3de1638c52f2b823ab0a73da": {"t": 1785768298.1189876, "n": 7097, "h": "deb9455b546f9402", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, Optional\n\nimport yaml\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n temperature: float = 0.1\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n\n @classmethod\n def from_file(cls, config_path: Optional[str] = None) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, \"r\") as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n github_config = GitHubConfig(\n token=config_data.get(\"github\", {}).get(\"token\")\n or os.getenv(\"KIT_GITHUB_TOKEN\")\n or os.getenv(\"GITHUB_TOKEN\"),\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\"),\n )\n\n if not github_config.token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {})\n provider_str = llm_data.get(\"provider\") or os.getenv(\"LLM_PROVIDER\", \"anthropic\")\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n api_key = llm_data.get(\"api_key\") or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n api_key = llm_data.get(\"api_key\") or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=api_key,\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n temperature=llm_data.get(\"temperature\", 0.1),\n )\n\n if not llm_config.api_key:\n raise ValueError(\n f\"LLM API key required. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15),\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\"token\": \"ghp_your_token_here\", \"base_url\": \"https://api.github.com\"},\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n \"temperature\": 0.1,\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n },\n }\n\n with open(config_path, \"w\") as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n return config_path\n"}, "f9a13c9747eb30f3e793b585d1896ba2ee08b729": {"t": 1785768299.4542902, "n": 7109, "h": "c8bdf3d977901185", "c": "\"\"\"Configuration management for PR review functionality.\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Dict, Optional\n\nimport yaml\n\n\nclass LLMProvider(Enum):\n \"\"\"Supported LLM providers.\"\"\"\n ANTHROPIC = \"anthropic\"\n OPENAI = \"openai\"\n\n\nclass ReviewDepth(Enum):\n \"\"\"Review analysis depth levels.\"\"\"\n QUICK = \"quick\"\n STANDARD = \"standard\"\n THOROUGH = \"thorough\"\n\n\n@dataclass\nclass GitHubConfig:\n \"\"\"GitHub configuration.\"\"\"\n token: str\n base_url: str = \"https://api.github.com\"\n\n\n@dataclass\nclass LLMConfig:\n \"\"\"LLM configuration.\"\"\"\n provider: LLMProvider\n model: str\n api_key: str\n max_tokens: int = 4000\n temperature: float = 0.1\n\n\n@dataclass\nclass ReviewConfig:\n \"\"\"Complete review configuration.\"\"\"\n github: GitHubConfig\n llm: LLMConfig\n max_files: int = 50\n include_recent_prs: bool = True\n analysis_depth: ReviewDepth = ReviewDepth.STANDARD\n post_as_comment: bool = True\n clone_for_analysis: bool = True\n cache_repos: bool = True\n cache_directory: str = \"~/.kit/repo-cache\"\n cache_ttl_hours: int = 24\n custom_pricing: Optional[Dict] = None\n # Agentic reviewer settings\n agentic_max_turns: int = 20\n agentic_finalize_threshold: int = 15 # Start encouraging finalization at this turn\n\n @classmethod\n def from_file(cls, config_path: Optional[str] = None) -> \"ReviewConfig\":\n \"\"\"Load configuration from file or environment variables.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_data = {}\n\n # Try to load from file\n config_file = Path(config_path)\n if config_file.exists():\n with open(config_file, 'r') as f:\n config_data = yaml.safe_load(f) or {}\n\n # Override with environment variables\n github_config = GitHubConfig(\n token=config_data.get(\"github\", {}).get(\"token\") or os.getenv(\"KIT_GITHUB_TOKEN\") or os.getenv(\"GITHUB_TOKEN\"),\n base_url=config_data.get(\"github\", {}).get(\"base_url\", \"https://api.github.com\")\n )\n\n if not github_config.token:\n raise ValueError(\n \"GitHub token required. Set KIT_GITHUB_TOKEN environment variable or \"\n \"add 'github.token' to ~/.kit/review-config.yaml\"\n )\n\n # LLM configuration\n llm_data = config_data.get(\"llm\", {})\n provider_str = llm_data.get(\"provider\") or os.getenv(\"LLM_PROVIDER\", \"anthropic\")\n\n try:\n provider = LLMProvider(provider_str)\n except ValueError:\n raise ValueError(f\"Unsupported LLM provider: {provider_str}. Use: {[p.value for p in LLMProvider]}\")\n\n # Default models and API key environment variables\n if provider == LLMProvider.ANTHROPIC:\n default_model = \"claude-sonnet-4-20250514\"\n api_key_env = \"KIT_ANTHROPIC_TOKEN or ANTHROPIC_API_KEY\"\n api_key = llm_data.get(\"api_key\") or os.getenv(\"KIT_ANTHROPIC_TOKEN\") or os.getenv(\"ANTHROPIC_API_KEY\")\n else: # OpenAI\n default_model = \"gpt-4.1-2025-04-14\"\n api_key_env = \"KIT_OPENAI_TOKEN or OPENAI_API_KEY\"\n api_key = llm_data.get(\"api_key\") or os.getenv(\"KIT_OPENAI_TOKEN\") or os.getenv(\"OPENAI_API_KEY\")\n\n llm_config = LLMConfig(\n provider=provider,\n model=llm_data.get(\"model\", default_model),\n api_key=api_key,\n max_tokens=llm_data.get(\"max_tokens\", 4000),\n temperature=llm_data.get(\"temperature\", 0.1)\n )\n\n if not llm_config.api_key:\n raise ValueError(\n f\"LLM API key required. Set {api_key_env} environment variable or \"\n f\"add 'llm.api_key' to ~/.kit/review-config.yaml\"\n )\n\n # Review settings\n review_data = config_data.get(\"review\", {})\n try:\n depth = ReviewDepth(review_data.get(\"analysis_depth\", \"standard\"))\n except ValueError:\n depth = ReviewDepth.STANDARD\n\n return cls(\n github=github_config,\n llm=llm_config,\n max_files=review_data.get(\"max_files\", 50),\n include_recent_prs=review_data.get(\"include_recent_prs\", True),\n analysis_depth=depth,\n post_as_comment=review_data.get(\"post_as_comment\", True),\n clone_for_analysis=review_data.get(\"clone_for_analysis\", True),\n cache_repos=review_data.get(\"cache_repos\", True),\n cache_directory=review_data.get(\"cache_directory\", \"~/.kit/repo-cache\"),\n cache_ttl_hours=review_data.get(\"cache_ttl_hours\", 24),\n custom_pricing=review_data.get(\"custom_pricing\", None),\n agentic_max_turns=review_data.get(\"agentic_max_turns\", 20),\n agentic_finalize_threshold=review_data.get(\"agentic_finalize_threshold\", 15)\n )\n\n def create_default_config_file(self, config_path: Optional[str] = None) -> str:\n \"\"\"Create a default configuration file.\"\"\"\n if config_path is None:\n config_path = os.path.expanduser(\"~/.kit/review-config.yaml\")\n\n config_dir = Path(config_path).parent\n config_dir.mkdir(parents=True, exist_ok=True)\n\n default_config = {\n \"github\": {\n \"token\": \"ghp_your_token_here\",\n \"base_url\": \"https://api.github.com\"\n },\n \"llm\": {\n \"provider\": \"anthropic\", # or \"openai\"\n \"model\": \"claude-sonnet-4-20250514\", # or \"gpt-4o\"\n \"api_key\": \"sk-your_api_key_here\",\n \"max_tokens\": 4000,\n \"temperature\": 0.1\n },\n \"review\": {\n \"max_files\": 50,\n \"include_recent_prs\": True,\n \"analysis_depth\": \"standard\", # quick, standard, thorough\n \"post_as_comment\": True,\n \"clone_for_analysis\": True,\n \"cache_repos\": True,\n \"cache_directory\": \"~/.kit/repo-cache\",\n \"cache_ttl_hours\": 24,\n # Agentic reviewer settings (for multi-turn analysis)\n \"agentic_max_turns\": 20, # Maximum number of analysis turns\n \"agentic_finalize_threshold\": 15, # Start encouraging finalization at this turn\n # \"custom_pricing\": {\n # \"anthropic\": {\n # \"claude-3-7-sonnet-latest\": {\n # \"input_per_million\": 3.00,\n # \"output_per_million\": 15.00\n # }\n # },\n # \"openai\": {\n # \"gpt-4o\": {\n # \"input_per_million\": 2.50,\n # \"output_per_million\": 10.00\n # }\n # }\n # }\n }\n }\n\n with open(config_path, 'w') as f:\n yaml.dump(default_config, f, default_flow_style=False, indent=2)\n\n return config_path\n"}}}} |