Skip to content

Commit 3cfc603

Browse files
committed
✨ new ai provider & @ paths autocomplete
1 parent c28d104 commit 3cfc603

File tree

13 files changed

+750
-61
lines changed

13 files changed

+750
-61
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ $ pip install -r requirements.txt
5454

5555
Then, replace the `.env.example` file to `.env` and fill in the tokens you need.
5656
```bash
57-
# For production (not available yet).
57+
# For production.
5858
$ dymo-code
5959
# For development (replace dymo-code with python run.py).
6060
$ python run.py

src/agent.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,11 @@ def _find_fallback_model(self, current_provider: str) -> Optional[str]:
348348

349349
# Find the best model from fallback providers
350350
for provider in fallback_providers:
351+
# Check if the client for this provider is actually available (package installed)
352+
client = self.client_manager.get_client_for_provider(provider)
353+
if client and not client.is_available():
354+
continue # Skip if client package not installed or not configured
355+
351356
# First try the default model for this provider
352357
default_model = get_default_model(provider)
353358
if default_model and default_model in AVAILABLE_MODELS:

src/api_key_manager.py

Lines changed: 98 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class KeyStatus(Enum):
2727
class APIKeyInfo:
2828
"""Information about an API key"""
2929
key: str
30+
name: Optional[str] = None # Optional friendly name for the key
3031
status: KeyStatus = KeyStatus.ACTIVE
3132
last_used: Optional[datetime] = None
3233
last_error: Optional[str] = None
@@ -41,6 +42,11 @@ def masked_key(self) -> str:
4142
return f"{self.key[:4]}...{self.key[-4:]}"
4243
return "****"
4344

45+
@property
46+
def display_name(self) -> str:
47+
"""Return display name (custom name or masked key)"""
48+
return self.name if self.name else self.masked_key
49+
4450
def is_available(self) -> bool:
4551
"""Check if key is currently available for use"""
4652
if self.status == KeyStatus.INVALID:
@@ -99,26 +105,34 @@ def __init__(self, provider: str):
99105
self._cooldown_duration = timedelta(seconds=60) # 1 minute cooldown
100106
self._rate_limit_cooldown = timedelta(minutes=5) # 5 minutes for rate limits
101107

102-
def add_key(self, key: str) -> bool:
103-
"""Add a new API key to the pool"""
108+
def add_key(self, key: str, name: Optional[str] = None) -> bool:
109+
"""Add a new API key to the pool with optional name"""
104110
with self._lock:
105111
# Check if key already exists
106112
for existing in self.keys:
107-
if existing.key == key:
108-
return False
113+
if existing.key == key: return False
109114

110-
self.keys.append(APIKeyInfo(key=key))
111-
log_debug(f"Added new API key to {self.provider} pool (total: {len(self.keys)})")
115+
self.keys.append(APIKeyInfo(key=key, name=name))
116+
display = name if name else f"{key[:4]}...{key[-4:]}" if len(key) > 12 else "****"
117+
log_debug(f"Added new API key '{display}' to {self.provider} pool (total: {len(self.keys)})")
112118
return True
113119

120+
def update_key_name(self, key: str, name: Optional[str]) -> bool:
121+
"""Update the name of an existing API key"""
122+
with self._lock:
123+
for key_info in self.keys:
124+
if key_info.key == key or key_info.masked_key == key:
125+
key_info.name = name
126+
return True
127+
return False
128+
114129
def remove_key(self, key: str) -> bool:
115130
"""Remove an API key from the pool"""
116131
with self._lock:
117132
for i, key_info in enumerate(self.keys):
118133
if key_info.key == key or key_info.masked_key == key:
119134
self.keys.pop(i)
120-
if self._current_index >= len(self.keys):
121-
self._current_index = 0
135+
if self._current_index >= len(self.keys): self._current_index = 0
122136
log_debug(f"Removed API key from {self.provider} pool (remaining: {len(self.keys)})")
123137
return True
124138
return False
@@ -221,6 +235,8 @@ def get_all_keys_info(self) -> List[Dict]:
221235
return [
222236
{
223237
"masked_key": k.masked_key,
238+
"name": k.name,
239+
"display_name": k.display_name,
224240
"status": k.status.value,
225241
"requests": k.requests_count,
226242
"errors": k.error_count,
@@ -269,6 +285,7 @@ def __init__(self):
269285
"anthropic": "ANTHROPIC_API_KEY",
270286
"openai": "OPENAI_API_KEY",
271287
"google": "GOOGLE_API_KEY",
288+
"cerebras": "CEREBRAS_API_KEY",
272289
}
273290
self._provider_lock = threading.Lock()
274291
self._initialized = True
@@ -277,28 +294,33 @@ def __init__(self):
277294
self._load_from_storage()
278295

279296
def _load_from_storage(self):
280-
"""Load API keys from storage"""
297+
"""Load API keys from storage (filters out placeholders)"""
281298
try:
282299
from .storage import user_config
283300

284301
for provider in self._env_key_map.keys():
285-
pool = self._get_or_create_pool(provider)
286-
287302
# Load multi-keys if available
288303
multi_keys = user_config.get_api_keys_list(provider)
289304
if multi_keys:
290-
for key in multi_keys:
291-
pool.add_key(key)
305+
for key_data in multi_keys:
306+
# Support both old format (string) and new format (dict with key/name)
307+
if isinstance(key_data, dict):
308+
key = key_data.get("key", "")
309+
name = key_data.get("name")
310+
self.add_key(provider, key, name)
311+
else:
312+
# Legacy format: just a string
313+
self.add_key(provider, key_data)
292314
else:
293315
# Fallback to single key (backward compatibility)
294316
single_key = user_config.get_api_key(provider)
295317
if single_key:
296-
pool.add_key(single_key)
318+
self.add_key(provider, single_key)
297319

298320
# Also check environment variable
299321
env_key = os.environ.get(self._env_key_map[provider])
300322
if env_key:
301-
pool.add_key(env_key)
323+
self.add_key(provider, env_key)
302324

303325
except Exception as e:
304326
log_error("Failed to load API keys from storage", e)
@@ -311,11 +333,16 @@ def _get_or_create_pool(self, provider: str) -> ProviderKeyPool:
311333
self._pools[provider] = ProviderKeyPool(provider)
312334
return self._pools[provider]
313335

314-
def add_key(self, provider: str, key: str) -> bool:
315-
"""Add an API key for a provider"""
336+
def add_key(self, provider: str, key: str, name: Optional[str] = None) -> bool:
337+
"""Add an API key for a provider with optional name (rejects placeholders)"""
338+
# Reject placeholder keys
339+
if self._is_placeholder_key(key):
340+
log_debug(f"Rejected placeholder key for {provider}")
341+
return False
342+
316343
provider = provider.lower()
317344
pool = self._get_or_create_pool(provider)
318-
success = pool.add_key(key)
345+
success = pool.add_key(key, name)
319346

320347
if success:
321348
# Also update environment for immediate use
@@ -328,6 +355,18 @@ def add_key(self, provider: str, key: str) -> bool:
328355

329356
return success
330357

358+
def update_key_name(self, provider: str, key: str, name: Optional[str]) -> bool:
359+
"""Update the name of an existing API key"""
360+
provider = provider.lower()
361+
pool = self._pools.get(provider)
362+
if not pool:
363+
return False
364+
365+
success = pool.update_key_name(key, name)
366+
if success:
367+
self._save_to_storage(provider)
368+
return success
369+
331370
def remove_key(self, provider: str, key: str) -> bool:
332371
"""Remove an API key for a provider"""
333372
provider = provider.lower()
@@ -341,26 +380,35 @@ def remove_key(self, provider: str, key: str) -> bool:
341380
return success
342381

343382
def _save_to_storage(self, provider: str):
344-
"""Save keys to storage"""
383+
"""Save keys to storage with optional names"""
345384
try:
346385
from .storage import user_config
347386

348387
pool = self._pools.get(provider)
349388
if pool:
350-
keys = [k.key for k in pool.keys]
351-
user_config.set_api_keys_list(provider, keys)
389+
# Save as list of objects with key and optional name
390+
keys_data = []
391+
for k in pool.keys:
392+
if k.name:
393+
keys_data.append({"key": k.key, "name": k.name})
394+
else:
395+
keys_data.append(k.key) # Simple string for backward compatibility
396+
user_config.set_api_keys_list(provider, keys_data)
352397
except Exception as e:
353398
log_error("Failed to save API keys to storage", e)
354399

355400
def get_key(self, provider: str) -> Optional[str]:
356-
"""Get the current active API key for a provider"""
401+
"""Get the current active API key for a provider (excludes placeholders)"""
357402
provider = provider.lower()
358403
pool = self._pools.get(provider)
359404
if not pool:
360405
# Try to get from environment as fallback
361406
env_var = self._env_key_map.get(provider)
362407
if env_var:
363-
return os.environ.get(env_var)
408+
key = os.environ.get(env_var)
409+
# Don't return placeholder keys
410+
if key and not self._is_placeholder_key(key):
411+
return key
364412
return None
365413

366414
key = pool.get_current_key()
@@ -441,15 +489,40 @@ def get_all_providers_info(self) -> List[Dict]:
441489
info.append(self.get_provider_info(provider))
442490
return info
443491

492+
def _is_placeholder_key(self, key: str) -> bool:
493+
"""Check if a key appears to be a placeholder/example value"""
494+
if not key:
495+
return True
496+
key_lower = key.lower()
497+
placeholder_patterns = [
498+
"your_",
499+
"_here",
500+
"example",
501+
"placeholder",
502+
"xxx",
503+
"insert",
504+
"paste",
505+
"api_key",
506+
"apikey",
507+
"enter_",
508+
"put_",
509+
"add_",
510+
"<",
511+
">",
512+
]
513+
return any(pattern in key_lower for pattern in placeholder_patterns)
514+
444515
def has_available_key(self, provider: str) -> bool:
445-
"""Check if provider has any available keys"""
516+
"""Check if provider has any available keys (excluding placeholders)"""
446517
provider = provider.lower()
447518
pool = self._pools.get(provider)
448519
if not pool:
449520
# Check environment as fallback
450521
env_var = self._env_key_map.get(provider)
451522
if env_var:
452-
return bool(os.environ.get(env_var))
523+
key = os.environ.get(env_var, "")
524+
# Make sure it's not a placeholder value
525+
return bool(key) and not self._is_placeholder_key(key)
453526
return False
454527
return pool.has_available_keys()
455528

src/async_input.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from .config import COLORS
2020
from .commands import get_command_suggestions, COMMANDS, Command
21+
from .terminal_ui import get_path_suggestions
2122

2223
# ═══════════════════════════════════════════════════════════════════════════════
2324
# Windows-specific imports
@@ -212,6 +213,7 @@ def _windows_input_loop(self):
212213
# Tab - autocomplete
213214
elif char == '\t':
214215
if buffer.startswith("/"):
216+
# Command autocomplete
215217
suggestions = get_command_suggestions(buffer[1:])[:6]
216218
if suggestions:
217219
cmd = suggestions[0]
@@ -222,6 +224,18 @@ def _windows_input_loop(self):
222224
self._show_prompt()
223225
sys.stdout.write(buffer)
224226
sys.stdout.flush()
227+
elif "@" in buffer:
228+
# Path autocomplete
229+
path_suggestions = get_path_suggestions(buffer)
230+
if path_suggestions:
231+
# Find @ position and replace path part
232+
at_index = buffer.rfind("@")
233+
new_path = path_suggestions[0]["path"]
234+
buffer = buffer[:at_index + 1] + new_path
235+
self._clear_line()
236+
self._show_prompt()
237+
sys.stdout.write(buffer)
238+
sys.stdout.flush()
225239
continue
226240

227241
# Special keys (arrows)

0 commit comments

Comments
 (0)