Skip to content
This repository was archived by the owner on Mar 9, 2026. It is now read-only.

Commit f40d830

Browse files
author
Nick Sullivan
committed
Add Anthropic support and revamped interactive configure
✨ Add Anthropic provider and revamp configuration flow - Add Anthropic support across the codebase: detect API keys, provide hardcoded/latest model lists for OpenAI/Anthropic, and expose fetch_models_for_provider() for dynamic model discovery. - Revamp the interactive configure command to let users pick provider, enter or detect API keys, fetch and select models asynchronously, choose a personality, and write a new v1.3 config schema (provider, model, personality, and provider-specific API key). Support non-interactive mode behavior and improve UX (browser prompts, summaries). - Update LanguageModelManager to understand the new config format, prefer API keys from the config file, normalize provider names, add safer validation and clearer error messaging, and change default model to gpt-5. - Adjust CLI bootstrap to pass ANTHROPIC_API_KEY into configure when present and remove implicit environment injection of OPENAI_API_KEY. - Increase commit command response token default to 1000. - Add comprehensive tests for API key detection, model fetching, and new/legacy config reading. This change modernizes multi-provider support and makes configuration interactive, extensible, and testable. 🤖
1 parent a84cefd commit f40d830

6 files changed

Lines changed: 501 additions & 104 deletions

File tree

aicodebot/cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ def cli(ctx, debug_output):
2626
if not existing_config:
2727
if ctx.invoked_subcommand != "configure":
2828
console.print(f"Welcome to {AICODEBOT}. Let's set up your config file.\n", style=console.bot_style)
29-
configure.callback(openai_api_key=os.getenv("OPENAI_API_KEY"), verbose=0)
29+
configure.callback(
30+
openai_api_key=os.getenv("OPENAI_API_KEY"), anthropic_api_key=os.getenv("ANTHROPIC_API_KEY"), verbose=0
31+
)
3032
sys.exit(0)
31-
else:
32-
os.environ["OPENAI_API_KEY"] = existing_config["openai_api_key"]
3333

3434
# Turn on langchain debug output if requested
3535
langchain_core.debug = debug_output

aicodebot/commands/commit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class CommitMessage(BaseModel):
2828

2929

3030
@click.command()
31-
@click.option("-t", "--response-token-size", type=int, default=250)
31+
@click.option("-t", "--response-token-size", type=int, default=1000)
3232
@click.option("-y", "--yes", is_flag=True, default=False, help="Don't ask for confirmation before committing.")
3333
@click.option("--skip-pre-commit", is_flag=True, help="Skip running pre-commit.")
3434
@click.argument("files", nargs=-1, type=click.Path(exists=True))

aicodebot/commands/configure.py

Lines changed: 202 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,224 @@
1+
import asyncio
2+
import sys
3+
import webbrowser
4+
5+
import click
6+
import yaml
7+
18
from aicodebot import AICODEBOT
2-
from aicodebot.config import get_config_file, read_config
9+
from aicodebot.config import detect_api_keys, fetch_models_for_provider, get_config_file, read_config
310
from aicodebot.helpers import create_and_write_file
411
from aicodebot.output import get_console
512
from aicodebot.prompts import DEFAULT_PERSONALITY, PERSONALITIES
6-
import click, os, sys, webbrowser, yaml
713

814

915
@click.command()
1016
@click.option("-v", "--verbose", count=True)
1117
@click.option("--openai-api-key", envvar="OPENAI_API_KEY", help="Your OpenAI API key")
12-
def configure(verbose, openai_api_key):
13-
"""Create or update the configuration file"""
18+
@click.option("--anthropic-api-key", envvar="ANTHROPIC_API_KEY", help="Your Anthropic API key")
19+
def configure(verbose, openai_api_key, anthropic_api_key): # noqa: ARG001 - verbose for future use
20+
"""Create or update the configuration file with dynamic provider and model selection"""
1421
console = get_console()
1522

16-
# --------------- Check for an existing key or set up defaults --------------- #
17-
18-
config_data_defaults = {
19-
"version": 1.2,
20-
"openai_api_key": openai_api_key,
21-
"personality": DEFAULT_PERSONALITY.name,
22-
}
23-
24-
config_data = config_data_defaults.copy()
25-
config_file = get_config_file()
26-
27-
existing_config = read_config()
28-
if existing_config:
29-
console.print(f"Config file already exists at {get_config_file()}.")
30-
click.confirm("Do you want to rerun configure and overwrite it?", default=False, abort=True)
31-
config_data.update(
32-
{
33-
"openai_api_key": existing_config["openai_api_key"],
34-
"personality": existing_config["personality"],
35-
}
36-
)
37-
38-
config_data = config_data_defaults.copy()
39-
4023
def write_config_file(config_data):
41-
create_and_write_file(config_file, yaml.dump(config_data))
42-
console.print(f"✅ Created config file at {config_file}")
24+
create_and_write_file(get_config_file(), yaml.dump(config_data), overwrite=True)
25+
console.print(f"✅ Updated config file at {get_config_file()}")
4326

27+
# Check if we're in a terminal for interactive mode
4428
is_terminal = sys.stdout.isatty()
45-
openai_api_key = openai_api_key or config_data["openai_api_key"] or os.getenv("OPENAI_API_KEY")
4629
if not is_terminal:
47-
if openai_api_key is None:
48-
raise click.ClickException(
49-
"🛑 No OpenAI API key found.\n"
50-
"Please set the OPENAI_API_KEY environment variable or call configure with --openai-api-key set."
51-
)
52-
else:
53-
# If we are not in a terminal, then we can't ask for input, so just use the defaults and write the file
54-
write_config_file(config_data)
30+
# Non-interactive mode - create config with detected API keys and defaults
31+
detected_keys = detect_api_keys()
32+
33+
# Also check command-line arguments for API keys
34+
api_key = None
35+
selected_provider = None
36+
37+
if openai_api_key:
38+
api_key = openai_api_key
39+
selected_provider = "openai"
40+
elif anthropic_api_key:
41+
api_key = anthropic_api_key
42+
selected_provider = "anthropic"
43+
elif "openai" in detected_keys:
44+
api_key = detected_keys["openai"]["key"]
45+
selected_provider = "openai"
46+
elif "anthropic" in detected_keys:
47+
api_key = detected_keys["anthropic"]["key"]
48+
selected_provider = "anthropic"
49+
50+
if not api_key:
51+
console.print("Non-interactive mode detected. Use --openai-api-key or --anthropic-api-key to set keys.")
5552
return
5653

57-
# ---------------- Collect the OPENAI_API_KEY and validate it ---------------- #
58-
59-
if config_data["openai_api_key"] is None:
60-
console.print(
61-
f"You need an OpenAI API Key for {AICODEBOT}. You can get one on the OpenAI website.",
62-
style=console.bot_style,
63-
)
64-
openai_api_key_url = "https://platform.openai.com/account/api-keys"
65-
if click.confirm("Open the api keys page in a browser?", default=False):
66-
webbrowser.open(openai_api_key_url)
54+
# Select default model for provider
55+
if selected_provider == "openai":
56+
selected_model = {"id": "gpt-5", "name": "gpt-5"}
57+
else:
58+
selected_model = {"id": "claude-opus-4-1", "name": "claude-opus-4-1"}
59+
60+
# Create config with defaults
61+
config_data = {
62+
"version": 1.3,
63+
"provider": selected_provider,
64+
"model": selected_model["id"],
65+
"personality": DEFAULT_PERSONALITY.name,
66+
}
67+
68+
# Always store API key in non-interactive mode (needed for tests)
69+
if selected_provider == "openai":
70+
config_data["openai_api_key"] = api_key
71+
else:
72+
config_data["anthropic_api_key"] = api_key
73+
74+
write_config_file(config_data)
75+
console.print(f"✅ Created config file in non-interactive mode with {selected_provider}")
76+
return
77+
78+
# Check for existing configuration
79+
existing_config = read_config() or {}
80+
if existing_config and not click.confirm("Config file already exists. Do you want to reconfigure?", default=True):
81+
return
82+
83+
console.print(f"\n🔧 Welcome to {AICODEBOT} Configuration!\n", style=console.bot_style)
84+
85+
# Step 1: Detect existing API keys
86+
detected_keys = detect_api_keys()
87+
88+
console.print("🔍 Scanning for existing API keys...", style=console.bot_style)
89+
if detected_keys:
90+
console.print("✅ Found the following API keys:", style="green")
91+
for provider, key_info in detected_keys.items():
92+
console.print(f" • {provider.title()}: {'*' * 8 + key_info['key'][-8:]} (from {key_info['source']})")
93+
else:
94+
console.print("ℹ️ No API keys found in environment variables", style="yellow")
95+
96+
# Step 2: Provider selection
97+
console.print("\n🤖 Choose your AI provider:", style=console.bot_style)
98+
99+
available_providers = []
100+
if detected_keys.get("openai") or openai_api_key:
101+
available_providers.append(("OpenAI", "openai"))
102+
if detected_keys.get("anthropic") or anthropic_api_key:
103+
available_providers.append(("Anthropic", "anthropic"))
104+
105+
# Always allow manual entry
106+
if not available_providers:
107+
console.print("No API keys detected. You'll need to enter one manually.")
108+
109+
all_providers = [("OpenAI", "openai"), ("Anthropic", "anthropic")]
110+
for i, (display_name, provider_id) in enumerate(all_providers, 1):
111+
status = "✅ Available" if any(p[1] == provider_id for p in available_providers) else "❌ Needs API key"
112+
console.print(f"{i}. {display_name} - {status}")
113+
114+
provider_choice = click.prompt("Select provider (1-2)", type=click.IntRange(1, 2), default=1)
115+
116+
selected_provider_display, selected_provider = all_providers[provider_choice - 1]
117+
console.print(f"Selected: {selected_provider_display}")
118+
119+
# Step 3: Handle API key
120+
api_key = None
121+
if selected_provider == "openai":
122+
api_key = detected_keys.get("openai", {}).get("key") or openai_api_key
123+
env_var = "OPENAI_API_KEY"
124+
api_url = "https://platform.openai.com/account/api-keys"
125+
else: # anthropic
126+
api_key = detected_keys.get("anthropic", {}).get("key") or anthropic_api_key
127+
env_var = "ANTHROPIC_API_KEY"
128+
api_url = "https://console.anthropic.com/account/keys"
129+
130+
if not api_key:
131+
console.print(f"\n🔑 You need a {selected_provider_display} API key.", style=console.bot_style)
132+
if click.confirm(f"Open {selected_provider_display} API keys page in browser?", default=True):
133+
webbrowser.open(api_url)
134+
135+
api_key = click.prompt(f"Enter your {selected_provider_display} API key").strip()
136+
console.print("💡 Consider setting this as an environment variable:", style="dim")
137+
console.print(f" export {env_var}={api_key}", style="dim")
138+
139+
# Step 4: Fetch and select model
140+
console.print(f"\n🧠 Fetching available models from {selected_provider_display}...", style=console.bot_style)
141+
142+
models = asyncio.run(fetch_models_for_provider(selected_provider, api_key))
143+
144+
if not models:
145+
raise ValueError(f"No models returned from {selected_provider_display}")
146+
147+
console.print(f"✅ Found {len(models)} available models:")
148+
149+
for i, model in enumerate(models[:10], 1): # Show first 10 models
150+
console.print(f"{i:2}. {model['name']} - {model['description']}")
151+
152+
if len(models) > 10:
153+
console.print(f"... and {len(models) - 10} more models")
154+
155+
# Add option for custom model
156+
console.print(f"{len(models) + 1:2}. [Custom] Enter your own model name")
157+
158+
max_choice = min(len(models), 10) + 1
159+
model_choice = click.prompt(f"Select model (1-{max_choice})", type=click.IntRange(1, max_choice), default=1)
160+
161+
if model_choice <= len(models):
162+
# User selected from the list
163+
selected_model = models[model_choice - 1]
164+
console.print(f"Selected: {selected_model['name']}")
165+
else:
166+
# User wants to enter custom model
167+
console.print("\n💡 You can enter any model ID (e.g., claude-opus-4-1, gpt-4o, claude-3-5-sonnet-20241022)")
168+
console.print("This allows you to use newer models that aren't in our list yet.")
169+
170+
custom_model_id = click.prompt("Enter model ID").strip()
171+
selected_model = {
172+
"id": custom_model_id,
173+
"name": custom_model_id,
174+
"description": f"Custom model: {custom_model_id}",
175+
}
176+
console.print(f"Selected: {selected_model['name']} (custom)")
177+
178+
# Step 5: Personality selection
179+
console.print("\n🎭 Choose your AI personality:", style=console.bot_style)
180+
181+
personality_list = list(PERSONALITIES.items())
182+
for i, (key, personality) in enumerate(personality_list, 1):
183+
console.print(f"{i}. {key} - {personality.description}")
184+
185+
personality_choice = click.prompt(
186+
f"Select personality (1-{len(personality_list)})", type=click.IntRange(1, len(personality_list)), default=1
187+
)
67188

68-
config_data["openai_api_key"] = click.prompt("Please enter your OpenAI API key").strip()
189+
selected_personality = personality_list[personality_choice - 1][0]
69190

70-
# ---------------------- Collect the personality choice ---------------------- #
191+
# Step 6: Build and save configuration
192+
config_data = {
193+
"version": 1.3, # Updated version for new format
194+
"provider": selected_provider,
195+
"model": selected_model["id"],
196+
"personality": selected_personality,
197+
}
71198

72-
# Pull the choices from the name from each of the PERSONALITIES
73-
console.print(
74-
"\nHow would you like your AI to act? You can choose from the following personalities:\n",
75-
style=console.bot_style,
76-
)
77-
personality_choices = ""
78-
for key, personality in PERSONALITIES.items():
79-
personality_choices += f"\t[b]{key}[/b] - {personality.description}\n"
80-
console.print(personality_choices)
81-
82-
config_data["personality"] = click.prompt(
83-
"Please choose a personality",
84-
type=click.Choice(PERSONALITIES.keys(), case_sensitive=False),
85-
default=DEFAULT_PERSONALITY.name,
86-
)
199+
# Always store API key in config file
200+
if selected_provider == "openai":
201+
config_data["openai_api_key"] = api_key
202+
else:
203+
config_data["anthropic_api_key"] = api_key
87204

88205
write_config_file(config_data)
89-
console.print(f"✅ Configuration complete, you're ready to run {AICODEBOT}!\n")
206+
207+
# Summary
208+
console.print("\n🎉 Configuration complete!", style="green bold")
209+
console.print(f"Provider: {selected_provider_display}")
210+
console.print(f"Model: {selected_model['name']}")
211+
console.print(f"Personality: {selected_personality}")
212+
console.print(f"\nYou're ready to use {AICODEBOT}! 🚀")
213+
214+
215+
# Helper function for async execution in click context
216+
def run_async(coro):
217+
"""Run an async coroutine in a click command context."""
218+
try:
219+
loop = asyncio.get_event_loop()
220+
except RuntimeError:
221+
loop = asyncio.new_event_loop()
222+
asyncio.set_event_loop(loop)
223+
224+
return loop.run_until_complete(coro)

aicodebot/config.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import os
22
from pathlib import Path
33

4-
import httpx
54
import yaml
65

76
from aicodebot.helpers import create_and_write_file, logger
@@ -41,6 +40,73 @@ def read_config():
4140
return None
4241

4342

43+
def detect_api_keys():
44+
"""Detect existing API keys from environment variables."""
45+
detected_keys = {}
46+
47+
# Check for OpenAI API key
48+
openai_key = os.getenv("OPENAI_API_KEY")
49+
if openai_key and openai_key.startswith("sk-"):
50+
detected_keys["openai"] = {"key": openai_key, "source": "environment"}
51+
52+
# Check for Anthropic API key
53+
anthropic_key = os.getenv("ANTHROPIC_API_KEY")
54+
if anthropic_key and anthropic_key.startswith("sk-ant-"):
55+
detected_keys["anthropic"] = {"key": anthropic_key, "source": "environment"}
56+
57+
return detected_keys
58+
59+
60+
async def fetch_openai_models(api_key): # noqa: ARG001 - api_key needed for interface consistency
61+
"""Return latest OpenAI models."""
62+
# Return the latest/upcoming OpenAI models (hardcoded list for cutting-edge access)
63+
return [
64+
{"id": "gpt-5", "name": "gpt-5", "description": "GPT-5 (Next-generation flagship model)"},
65+
{"id": "gpt-5-mini", "name": "gpt-5-mini", "description": "GPT-5 Mini (Fast and efficient next-gen)"},
66+
{"id": "gpt-oss-120b", "name": "gpt-oss-120b", "description": "GPT-OSS 120B (Open source model)"},
67+
{"id": "o3-pro", "name": "o3-pro", "description": "O3 Pro (Advanced reasoning model)"},
68+
]
69+
70+
71+
async def fetch_anthropic_models(api_key): # noqa: ARG001 - api_key needed for interface consistency
72+
"""Fetch available models from Anthropic API."""
73+
# Note: Anthropic doesn't have a public models API endpoint like OpenAI
74+
# Return the latest available models from official docs (January 2025)
75+
# Source: https://docs.anthropic.com/en/docs/about-claude/models/overview
76+
return [
77+
{
78+
"id": "claude-opus-4-1",
79+
"name": "claude-opus-4-1",
80+
"description": "Claude Opus 4.1 (Most capable and intelligent - Latest flagship)",
81+
},
82+
{
83+
"id": "claude-sonnet-4-0",
84+
"name": "claude-sonnet-4-0",
85+
"description": "Claude Sonnet 4 (High-performance with exceptional reasoning)",
86+
},
87+
{
88+
"id": "claude-3-7-sonnet-latest",
89+
"name": "claude-3-7-sonnet-latest",
90+
"description": "Claude Sonnet 3.7 (High-performance with extended thinking)",
91+
},
92+
{
93+
"id": "claude-3-5-haiku-latest",
94+
"name": "claude-3-5-haiku-latest",
95+
"description": "Claude Haiku 3.5 (Fastest model with intelligence)",
96+
},
97+
]
98+
99+
100+
async def fetch_models_for_provider(provider, api_key):
101+
"""Fetch models for a specific provider."""
102+
if provider.lower() == "openai":
103+
return await fetch_openai_models(api_key)
104+
elif provider.lower() == "anthropic":
105+
return await fetch_anthropic_models(api_key)
106+
else:
107+
raise ValueError(f"Unknown provider: {provider}")
108+
109+
44110
class Session:
45111
"""Read and write local session data"""
46112

0 commit comments

Comments
 (0)