sigma-terminal 3.3.2__py3-none-any.whl → 3.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sigma/charts.py CHANGED
@@ -355,44 +355,62 @@ def _calculate_macd(prices: pd.Series, fast: int = 12, slow: int = 26, signal: i
355
355
 
356
356
 
357
357
  def _apply_layout(fig: go.Figure, title: str, has_volume: bool):
358
- """Apply Sigma theme to chart."""
358
+ """Apply Sigma theme to chart with publication-grade styling."""
359
359
  fig.update_layout(
360
360
  title=dict(
361
- text=title,
362
- font=dict(size=18, color=SIGMA_THEME["text_color"]),
361
+ text=f"<b>{title}</b>",
362
+ font=dict(size=20, color=SIGMA_THEME["text_color"], family="SF Pro Display, -apple-system, sans-serif"),
363
363
  x=0.5,
364
+ xanchor="center",
364
365
  ),
365
366
  paper_bgcolor=SIGMA_THEME["paper_color"],
366
367
  plot_bgcolor=SIGMA_THEME["bg_color"],
367
- font=dict(color=SIGMA_THEME["text_color"], family="SF Mono, Menlo, monospace"),
368
+ font=dict(color=SIGMA_THEME["text_color"], family="SF Pro Display, -apple-system, sans-serif", size=12),
368
369
  xaxis=dict(
369
370
  gridcolor=SIGMA_THEME["grid_color"],
370
371
  showgrid=True,
372
+ gridwidth=1,
371
373
  zeroline=False,
374
+ showline=True,
375
+ linewidth=1,
376
+ linecolor=SIGMA_THEME["grid_color"],
377
+ tickfont=dict(size=11),
372
378
  ),
373
379
  yaxis=dict(
374
380
  gridcolor=SIGMA_THEME["grid_color"],
375
381
  showgrid=True,
382
+ gridwidth=1,
376
383
  zeroline=False,
384
+ showline=True,
385
+ linewidth=1,
386
+ linecolor=SIGMA_THEME["grid_color"],
377
387
  title_text="Price ($)",
388
+ tickfont=dict(size=11),
389
+ tickformat="$,.2f",
378
390
  ),
379
391
  legend=dict(
380
392
  bgcolor="rgba(0,0,0,0)",
381
- font=dict(color=SIGMA_THEME["text_color"]),
393
+ font=dict(color=SIGMA_THEME["text_color"], size=11),
382
394
  orientation="h",
383
395
  yanchor="bottom",
384
396
  y=1.02,
385
397
  xanchor="right",
386
398
  x=1,
387
399
  ),
388
- margin=dict(l=60, r=40, t=80, b=40),
400
+ margin=dict(l=70, r=50, t=90, b=50),
389
401
  xaxis_rangeslider_visible=False,
390
402
  hovermode="x unified",
403
+ hoverlabel=dict(
404
+ bgcolor=SIGMA_THEME["bg_color"],
405
+ font_size=12,
406
+ font_family="SF Mono, Menlo, monospace",
407
+ bordercolor=SIGMA_THEME["accent"],
408
+ ),
391
409
  )
392
410
 
393
411
 
394
412
  def _save_chart(fig: go.Figure, name: str) -> str:
395
- """Save chart to file and return path."""
413
+ """Save chart to file with maximum quality and return path."""
396
414
 
397
415
  # Create charts directory
398
416
  charts_dir = os.path.expanduser("~/.sigma/charts")
@@ -402,6 +420,13 @@ def _save_chart(fig: go.Figure, name: str) -> str:
402
420
  filename = f"{name}_{timestamp}.png"
403
421
  filepath = os.path.join(charts_dir, filename)
404
422
 
405
- fig.write_image(filepath, width=1200, height=800, scale=2)
423
+ # High quality export settings
424
+ fig.write_image(
425
+ filepath,
426
+ width=1600, # Higher resolution
427
+ height=1000, # Better aspect ratio
428
+ scale=3, # 3x scaling for retina/high-DPI
429
+ engine="kaleido"
430
+ )
406
431
 
407
432
  return filepath
sigma/cli.py CHANGED
@@ -1,4 +1,4 @@
1
- """CLI entry point for Sigma v3.3.1."""
1
+ """CLI entry point for Sigma v3.4.1."""
2
2
 
3
3
  import argparse
4
4
  import json
@@ -17,7 +17,7 @@ from .config import (
17
17
  )
18
18
 
19
19
 
20
- __version__ = "3.3.1"
20
+ __version__ = "3.4.1"
21
21
 
22
22
  console = Console()
23
23
 
@@ -25,14 +25,14 @@ console = Console()
25
25
  def show_banner():
26
26
  """Show the Sigma banner."""
27
27
  banner = """
28
- [bold white]███████╗██╗ ██████╗ ███╗ ███╗ █████╗ [/bold white]
29
- [bold white]██╔════╝██║██╔════╝ ████╗ ████║██╔══██╗[/bold white]
30
- [bold white]███████╗██║██║ ███╗██╔████╔██║███████║[/bold white]
31
- [bold white]╚════██║██║██║ ██║██║╚██╔╝██║██╔══██║[/bold white]
32
- [bold white]███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║[/bold white]
33
- [bold white]╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold white]
28
+ [bold #3b82f6]███████╗██╗ ██████╗ ███╗ ███╗ █████╗ [/bold #3b82f6]
29
+ [bold #60a5fa]██╔════╝██║██╔════╝ ████╗ ████║██╔══██╗[/bold #60a5fa]
30
+ [bold #93c5fd]███████╗██║██║ ███╗██╔████╔██║███████║[/bold #93c5fd]
31
+ [bold #60a5fa]╚════██║██║██║ ██║██║╚██╔╝██║██╔══██║[/bold #60a5fa]
32
+ [bold #3b82f6]███████║██║╚██████╔╝██║ ╚═╝ ██║██║ ██║[/bold #3b82f6]
33
+ [bold #1d4ed8]╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝[/bold #1d4ed8]
34
34
 
35
- [dim]v3.3.1[/dim] [bold cyan]σ[/bold cyan] [bold]Finance Research Agent[/bold]
35
+ [dim]v3.4.1[/dim] [bold cyan]σ[/bold cyan] [bold]Finance Research Agent[/bold]
36
36
  """
37
37
  console.print(banner)
38
38
 
@@ -41,7 +41,7 @@ def main():
41
41
  """Main CLI entry point."""
42
42
  parser = argparse.ArgumentParser(
43
43
  prog="sigma",
44
- description="Sigma v3.3.1 - Finance Research Agent",
44
+ description="Sigma v3.4.1 - Finance Research Agent",
45
45
  formatter_class=argparse.RawDescriptionHelpFormatter,
46
46
  epilog="""
47
47
  Examples:
@@ -148,7 +148,7 @@ Examples:
148
148
  mark_first_run_complete() # Always mark complete to not ask again
149
149
 
150
150
  if result:
151
- console.print("\n[bold green]Setup complete![/bold green]")
151
+ console.print("\n[bold green]Setup complete![/bold green]")
152
152
  console.print("[dim]Launching Sigma...[/dim]\n")
153
153
  import time
154
154
  time.sleep(1) # Brief pause for user to see message
sigma/config.py CHANGED
@@ -1,9 +1,9 @@
1
- """Configuration management for Sigma v3.3.2."""
1
+ """Configuration management for Sigma v3.4.1."""
2
2
 
3
3
  import os
4
4
  import shutil
5
5
  import subprocess
6
- from enum import Enum
6
+ from enum import Enum, IntEnum
7
7
  from pathlib import Path
8
8
  from typing import Optional, Tuple
9
9
 
@@ -11,7 +11,56 @@ from pydantic import Field
11
11
  from pydantic_settings import BaseSettings
12
12
 
13
13
 
14
- __version__ = "3.3.2"
14
+ __version__ = "3.4.1"
15
+
16
+
17
+ class ErrorCode(IntEnum):
18
+ """Error codes for consistent error handling."""
19
+ # General errors (1000-1099)
20
+ UNKNOWN_ERROR = 1000
21
+ INVALID_INPUT = 1001
22
+ TIMEOUT = 1002
23
+
24
+ # API Key errors (1100-1199)
25
+ API_KEY_MISSING = 1100
26
+ API_KEY_INVALID = 1101
27
+ API_KEY_EXPIRED = 1102
28
+ API_KEY_RATE_LIMITED = 1103
29
+
30
+ # Provider errors (1200-1299)
31
+ PROVIDER_UNAVAILABLE = 1200
32
+ PROVIDER_ERROR = 1201
33
+ MODEL_NOT_FOUND = 1202
34
+ MODEL_DEPRECATED = 1203
35
+
36
+ # Data errors (1300-1399)
37
+ SYMBOL_NOT_FOUND = 1300
38
+ DATA_UNAVAILABLE = 1301
39
+ MARKET_CLOSED = 1302
40
+ RATE_LIMIT_EXCEEDED = 1303
41
+
42
+ # Network errors (1400-1499)
43
+ CONNECTION_ERROR = 1400
44
+ REQUEST_FAILED = 1401
45
+ RESPONSE_INVALID = 1402
46
+
47
+
48
+ class SigmaError(Exception):
49
+ """Custom exception with error codes."""
50
+
51
+ def __init__(self, code: ErrorCode, message: str, details: Optional[dict] = None):
52
+ self.code = code
53
+ self.message = message
54
+ self.details = details or {}
55
+ super().__init__(f"[E{code}] {message}")
56
+
57
+ def to_dict(self) -> dict:
58
+ return {
59
+ "error_code": int(self.code),
60
+ "error_name": self.code.name,
61
+ "message": self.message,
62
+ "details": self.details
63
+ }
15
64
 
16
65
 
17
66
  class LLMProvider(str, Enum):
@@ -24,14 +73,41 @@ class LLMProvider(str, Enum):
24
73
  OLLAMA = "ollama"
25
74
 
26
75
 
27
- # Available models per provider
76
+ # Available models per provider (Feb 2026 - REAL API NAMES)
28
77
  AVAILABLE_MODELS = {
29
- "google": ["gemini-3-flash-preview", "gemini-2.5-pro", "gemini-2.5-flash"],
30
- "openai": ["gpt-4o", "gpt-4o-mini", "o1", "o1-mini", "o3-mini"],
31
- "anthropic": ["claude-sonnet-4-20250514", "claude-3-5-sonnet-20241022", "claude-3-opus-20240229"],
32
- "groq": ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768"],
33
- "xai": ["grok-2", "grok-2-mini"],
34
- "ollama": ["llama3.2", "llama3.1", "mistral", "codellama", "phi3"],
78
+ "google": [
79
+ "gemini-3-flash-preview", # Fast multimodal, Pro-level at Flash speed
80
+ "gemini-3-pro-preview", # Multimodal reasoning (1M tokens)
81
+ "gemini-3-pro-image-preview", # Image+text focus (65K tokens)
82
+ ],
83
+ "openai": [
84
+ "gpt-5", # Flagship general/agentic, multimodal (256K)
85
+ "gpt-5-mini", # Cost-efficient variant (256K)
86
+ "gpt-5.2", # Enterprise knowledge work, advanced reasoning
87
+ "gpt-5-nano", # Ultra-cheap/lightweight
88
+ "o3", # Advanced reasoning
89
+ "o3-mini", # Fast reasoning
90
+ ],
91
+ "anthropic": [
92
+ "claude-sonnet-4-20250514", # Latest Sonnet
93
+ "claude-opus-4-20250514", # Latest Opus
94
+ ],
95
+ "groq": [
96
+ "llama-3.3-70b-versatile", # Most capable free
97
+ "llama-3.3-8b-instant", # Fast free
98
+ "mixtral-8x7b-32768", # Good balance free
99
+ ],
100
+ "xai": [
101
+ "grok-3", # Latest full capability
102
+ "grok-3-mini", # Latest fast variant
103
+ ],
104
+ "ollama": [
105
+ "llama3.3", # Latest local LLM
106
+ "llama3.2",
107
+ "mistral",
108
+ "phi4", # Latest Phi
109
+ "qwen2.5", # Latest Qwen
110
+ ],
35
111
  }
36
112
 
37
113
  # Config directory
@@ -41,8 +117,40 @@ FIRST_RUN_MARKER = CONFIG_DIR / ".first_run_complete"
41
117
 
42
118
 
43
119
  def is_first_run() -> bool:
44
- """Check if this is the first run of the application."""
45
- return not FIRST_RUN_MARKER.exists()
120
+ """
121
+ Check if this is the first run of the application.
122
+
123
+ Returns False (not first run) if:
124
+ - The first run marker exists, OR
125
+ - A config file exists with at least one API key configured
126
+
127
+ This ensures users who upgrade from older versions or manually
128
+ configure their ~/.sigma/config.env don't see the setup wizard.
129
+ """
130
+ # Check for explicit marker
131
+ if FIRST_RUN_MARKER.exists():
132
+ return False
133
+
134
+ # Check if config file exists and has API keys
135
+ if CONFIG_FILE.exists():
136
+ try:
137
+ content = CONFIG_FILE.read_text()
138
+ # Check if any API key is set (not empty)
139
+ api_keys = ["GOOGLE_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY",
140
+ "GROQ_API_KEY", "XAI_API_KEY"]
141
+ for key in api_keys:
142
+ # Look for KEY=value where value is not empty
143
+ for line in content.splitlines():
144
+ if line.startswith(f"{key}="):
145
+ value = line.split("=", 1)[1].strip()
146
+ if value and value not in ('""', "''", ""):
147
+ # Found a configured API key - not first run
148
+ mark_first_run_complete() # Create marker for future
149
+ return False
150
+ except Exception:
151
+ pass
152
+
153
+ return True
46
154
 
47
155
 
48
156
  def mark_first_run_complete() -> None:
@@ -205,22 +313,22 @@ class Settings(BaseSettings):
205
313
 
206
314
  # Provider settings
207
315
  default_provider: LLMProvider = LLMProvider.GOOGLE
208
- default_model: str = Field(default="gemini-2.0-flash", alias="DEFAULT_MODEL")
316
+ default_model: str = Field(default="gemini-3-flash-preview", alias="DEFAULT_MODEL")
209
317
 
210
- # API Keys
318
+ # LLM API Keys
211
319
  google_api_key: Optional[str] = Field(default=None, alias="GOOGLE_API_KEY")
212
320
  openai_api_key: Optional[str] = Field(default=None, alias="OPENAI_API_KEY")
213
321
  anthropic_api_key: Optional[str] = Field(default=None, alias="ANTHROPIC_API_KEY")
214
322
  groq_api_key: Optional[str] = Field(default=None, alias="GROQ_API_KEY")
215
323
  xai_api_key: Optional[str] = Field(default=None, alias="XAI_API_KEY")
216
324
 
217
- # Model settings
218
- google_model: str = "gemini-2.0-flash"
219
- openai_model: str = "gpt-4o"
325
+ # Model settings - REAL API NAMES (Feb 2026)
326
+ google_model: str = "gemini-3-flash-preview"
327
+ openai_model: str = "gpt-5"
220
328
  anthropic_model: str = "claude-sonnet-4-20250514"
221
329
  groq_model: str = "llama-3.3-70b-versatile"
222
- xai_model: str = "grok-2"
223
- ollama_model: str = "llama3.2"
330
+ xai_model: str = "grok-3"
331
+ ollama_model: str = "llama3.3"
224
332
 
225
333
  # Ollama settings
226
334
  ollama_host: str = "http://localhost:11434"
@@ -232,6 +340,7 @@ class Settings(BaseSettings):
232
340
 
233
341
  # Data API keys
234
342
  alpha_vantage_api_key: str = Field(default="6ER128DD3NQUPTVC", alias="ALPHA_VANTAGE_API_KEY")
343
+ polygon_api_key: Optional[str] = Field(default=None, alias="POLYGON_API_KEY")
235
344
  exa_api_key: Optional[str] = Field(default=None, alias="EXA_API_KEY")
236
345
 
237
346
  class Config:
@@ -287,38 +396,70 @@ def get_settings() -> Settings:
287
396
  return Settings()
288
397
 
289
398
 
290
- def save_api_key(provider: str, key: str) -> None:
291
- """Save an API key to the config file."""
399
+ def save_api_key(provider: str, key: str) -> bool:
400
+ """Save an API key to the config file. Returns True on success."""
292
401
  CONFIG_DIR.mkdir(parents=True, exist_ok=True)
293
402
 
294
403
  # Read existing config
295
404
  config = {}
296
405
  if CONFIG_FILE.exists():
297
- with open(CONFIG_FILE) as f:
298
- for line in f:
299
- line = line.strip()
300
- if "=" in line and not line.startswith("#"):
301
- k, v = line.split("=", 1)
302
- config[k] = v
406
+ try:
407
+ with open(CONFIG_FILE) as f:
408
+ for line in f:
409
+ line = line.strip()
410
+ if "=" in line and not line.startswith("#"):
411
+ k, v = line.split("=", 1)
412
+ config[k] = v
413
+ except IOError:
414
+ pass
303
415
 
304
- # Update key
416
+ # Map provider names to config keys (LLM + Data providers)
305
417
  key_map = {
418
+ # LLM providers
306
419
  "google": "GOOGLE_API_KEY",
307
420
  "openai": "OPENAI_API_KEY",
308
421
  "anthropic": "ANTHROPIC_API_KEY",
309
422
  "groq": "GROQ_API_KEY",
310
423
  "xai": "XAI_API_KEY",
424
+ # Data providers
425
+ "polygon": "POLYGON_API_KEY",
426
+ "alphavantage": "ALPHA_VANTAGE_API_KEY",
427
+ "exa": "EXA_API_KEY",
311
428
  }
312
429
 
313
430
  env_key = key_map.get(provider.lower())
314
- if env_key:
315
- config[env_key] = key
431
+ if not env_key:
432
+ return False
433
+
434
+ config[env_key] = key
316
435
 
317
436
  # Write back
318
- with open(CONFIG_FILE, "w") as f:
319
- f.write("# Sigma Configuration\n\n")
320
- for k, v in sorted(config.items()):
321
- f.write(f"{k}={v}\n")
437
+ try:
438
+ with open(CONFIG_FILE, "w") as f:
439
+ f.write("# Sigma Configuration\n")
440
+ f.write(f"# Updated: {__import__('datetime').datetime.now().isoformat()}\n\n")
441
+
442
+ # Group by type for readability
443
+ llm_keys = ["GOOGLE_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GROQ_API_KEY", "XAI_API_KEY"]
444
+ data_keys = ["POLYGON_API_KEY", "ALPHA_VANTAGE_API_KEY", "EXA_API_KEY"]
445
+
446
+ f.write("# LLM Provider Keys\n")
447
+ for k in llm_keys:
448
+ if k in config:
449
+ f.write(f"{k}={config[k]}\n")
450
+
451
+ f.write("\n# Data Provider Keys\n")
452
+ for k in data_keys:
453
+ if k in config:
454
+ f.write(f"{k}={config[k]}\n")
455
+
456
+ f.write("\n# Other Settings\n")
457
+ for k, v in sorted(config.items()):
458
+ if k not in llm_keys and k not in data_keys:
459
+ f.write(f"{k}={v}\n")
460
+ return True
461
+ except IOError:
462
+ return False
322
463
 
323
464
 
324
465
  def get_api_key(provider: str) -> Optional[str]:
sigma/llm.py CHANGED
@@ -7,7 +7,7 @@ import time
7
7
  from abc import ABC, abstractmethod
8
8
  from typing import Any, AsyncIterator, Callable, Optional
9
9
 
10
- from sigma.config import LLMProvider, get_settings
10
+ from sigma.config import LLMProvider, get_settings, ErrorCode, SigmaError
11
11
 
12
12
 
13
13
  # Rate limiting configuration
@@ -47,14 +47,14 @@ class RateLimiter:
47
47
  self.request_count += 1
48
48
 
49
49
 
50
- # Global rate limiters per provider
50
+ # Global rate limiters per provider (generous limits to avoid rate limiting)
51
51
  _rate_limiters = {
52
- "google": RateLimiter(requests_per_minute=15, min_interval=0.5),
53
- "openai": RateLimiter(requests_per_minute=20, min_interval=0.3),
54
- "anthropic": RateLimiter(requests_per_minute=15, min_interval=0.5),
55
- "groq": RateLimiter(requests_per_minute=30, min_interval=0.2),
56
- "xai": RateLimiter(requests_per_minute=10, min_interval=1.0),
57
- "ollama": RateLimiter(requests_per_minute=60, min_interval=0.1), # Local, can be faster
52
+ "google": RateLimiter(requests_per_minute=60, min_interval=0.2), # Gemini free tier is generous
53
+ "openai": RateLimiter(requests_per_minute=60, min_interval=0.2), # GPT-5 tier 1+
54
+ "anthropic": RateLimiter(requests_per_minute=40, min_interval=0.3), # Claude standard
55
+ "groq": RateLimiter(requests_per_minute=30, min_interval=0.2), # Groq free tier
56
+ "xai": RateLimiter(requests_per_minute=30, min_interval=0.3), # Grok standard
57
+ "ollama": RateLimiter(requests_per_minute=120, min_interval=0.05), # Local, no limits
58
58
  }
59
59
 
60
60
 
@@ -601,6 +601,123 @@ class OllamaLLM(BaseLLM):
601
601
  return "\n".join(lines)
602
602
 
603
603
 
604
+ class XaiLLM(BaseLLM):
605
+ """xAI Grok client (uses OpenAI-compatible API)."""
606
+
607
+ provider_name = "xai"
608
+
609
+ def __init__(self, api_key: str, model: str):
610
+ from openai import AsyncOpenAI
611
+ self.client = AsyncOpenAI(
612
+ api_key=api_key,
613
+ base_url="https://api.x.ai/v1"
614
+ )
615
+ self.model = model
616
+
617
+ async def generate(
618
+ self,
619
+ messages: list[dict],
620
+ tools: Optional[list[dict]] = None,
621
+ on_tool_call: Optional[Callable] = None,
622
+ ) -> str:
623
+ await self._rate_limit()
624
+
625
+ try:
626
+ kwargs = {
627
+ "model": self.model,
628
+ "messages": messages,
629
+ }
630
+
631
+ if tools:
632
+ kwargs["tools"] = tools
633
+ kwargs["tool_choice"] = "auto"
634
+
635
+ response = await self.client.chat.completions.create(**kwargs)
636
+ message = response.choices[0].message
637
+
638
+ # Handle tool calls
639
+ if message.tool_calls and on_tool_call:
640
+ tool_results = []
641
+ for tc in message.tool_calls:
642
+ args = json.loads(tc.function.arguments)
643
+ result = await on_tool_call(tc.function.name, args)
644
+ tool_results.append({
645
+ "tool_call_id": tc.id,
646
+ "role": "tool",
647
+ "content": json.dumps(result)
648
+ })
649
+
650
+ # Continue with tool results
651
+ messages = messages + [message.model_dump()] + tool_results
652
+ return await self.generate(messages, tools, on_tool_call)
653
+
654
+ return message.content or ""
655
+
656
+ except Exception as e:
657
+ error_str = str(e)
658
+ if "401" in error_str or "invalid_api_key" in error_str:
659
+ raise SigmaError(
660
+ ErrorCode.API_KEY_INVALID,
661
+ "xAI API key is invalid",
662
+ {"provider": "xai"}
663
+ )
664
+ elif "429" in error_str or "rate_limit" in error_str:
665
+ raise SigmaError(
666
+ ErrorCode.API_KEY_RATE_LIMITED,
667
+ "xAI rate limit exceeded. Please wait.",
668
+ {"provider": "xai"}
669
+ )
670
+ raise
671
+
672
+
673
+ def _parse_api_error(error: Exception, provider: str) -> SigmaError:
674
+ """Parse API errors into SigmaError with proper codes."""
675
+ error_str = str(error).lower()
676
+
677
+ if "401" in error_str or "invalid_api_key" in error_str or "api key not valid" in error_str:
678
+ return SigmaError(
679
+ ErrorCode.API_KEY_INVALID,
680
+ f"{provider.title()} API key is invalid. Check your key at /keys",
681
+ {"provider": provider, "original_error": str(error)[:200]}
682
+ )
683
+ elif "403" in error_str or "forbidden" in error_str:
684
+ return SigmaError(
685
+ ErrorCode.API_KEY_INVALID,
686
+ f"{provider.title()} API key doesn't have access to this model",
687
+ {"provider": provider}
688
+ )
689
+ elif "429" in error_str or "rate_limit" in error_str or "quota" in error_str:
690
+ return SigmaError(
691
+ ErrorCode.API_KEY_RATE_LIMITED,
692
+ f"{provider.title()} rate limit exceeded. Wait a moment.",
693
+ {"provider": provider}
694
+ )
695
+ elif "404" in error_str or "not found" in error_str or "does not exist" in error_str:
696
+ return SigmaError(
697
+ ErrorCode.MODEL_NOT_FOUND,
698
+ f"Model not found. Try /models to see available options.",
699
+ {"provider": provider}
700
+ )
701
+ elif "timeout" in error_str:
702
+ return SigmaError(
703
+ ErrorCode.TIMEOUT,
704
+ "Request timed out. Try a simpler query.",
705
+ {"provider": provider}
706
+ )
707
+ elif "connection" in error_str:
708
+ return SigmaError(
709
+ ErrorCode.CONNECTION_ERROR,
710
+ f"Cannot connect to {provider.title()}. Check internet.",
711
+ {"provider": provider}
712
+ )
713
+ else:
714
+ return SigmaError(
715
+ ErrorCode.PROVIDER_ERROR,
716
+ f"{provider.title()} error: {str(error)[:150]}",
717
+ {"provider": provider, "original_error": str(error)[:300]}
718
+ )
719
+
720
+
604
721
  def get_llm(provider: LLMProvider, model: Optional[str] = None) -> BaseLLM:
605
722
  """Get LLM client for a provider."""
606
723
  settings = get_settings()
@@ -611,29 +728,59 @@ def get_llm(provider: LLMProvider, model: Optional[str] = None) -> BaseLLM:
611
728
  if provider == LLMProvider.GOOGLE:
612
729
  api_key = settings.google_api_key
613
730
  if not api_key:
614
- raise ValueError("Google API key not configured")
731
+ raise SigmaError(
732
+ ErrorCode.API_KEY_MISSING,
733
+ "Google API key not configured. Use /keys to set up.",
734
+ {"provider": "google", "hint": "Get key at: https://aistudio.google.com/apikey"}
735
+ )
615
736
  return GoogleLLM(api_key, model)
616
737
 
617
738
  elif provider == LLMProvider.OPENAI:
618
739
  api_key = settings.openai_api_key
619
740
  if not api_key:
620
- raise ValueError("OpenAI API key not configured")
741
+ raise SigmaError(
742
+ ErrorCode.API_KEY_MISSING,
743
+ "OpenAI API key not configured. Use /keys to set up.",
744
+ {"provider": "openai", "hint": "Get key at: https://platform.openai.com/api-keys"}
745
+ )
621
746
  return OpenAILLM(api_key, model)
622
747
 
623
748
  elif provider == LLMProvider.ANTHROPIC:
624
749
  api_key = settings.anthropic_api_key
625
750
  if not api_key:
626
- raise ValueError("Anthropic API key not configured")
751
+ raise SigmaError(
752
+ ErrorCode.API_KEY_MISSING,
753
+ "Anthropic API key not configured. Use /keys to set up.",
754
+ {"provider": "anthropic", "hint": "Get key at: https://console.anthropic.com/settings/keys"}
755
+ )
627
756
  return AnthropicLLM(api_key, model)
628
757
 
629
758
  elif provider == LLMProvider.GROQ:
630
759
  api_key = settings.groq_api_key
631
760
  if not api_key:
632
- raise ValueError("Groq API key not configured")
761
+ raise SigmaError(
762
+ ErrorCode.API_KEY_MISSING,
763
+ "Groq API key not configured. Use /keys to set up.",
764
+ {"provider": "groq", "hint": "Get key at: https://console.groq.com/keys"}
765
+ )
633
766
  return GroqLLM(api_key, model)
634
767
 
768
+ elif provider == LLMProvider.XAI:
769
+ api_key = settings.xai_api_key
770
+ if not api_key:
771
+ raise SigmaError(
772
+ ErrorCode.API_KEY_MISSING,
773
+ "xAI API key not configured. Use /keys to set up.",
774
+ {"provider": "xai", "hint": "Get key at: https://console.x.ai"}
775
+ )
776
+ return XaiLLM(api_key, model)
777
+
635
778
  elif provider == LLMProvider.OLLAMA:
636
779
  return OllamaLLM(settings.ollama_host, model)
637
780
 
638
781
  else:
639
- raise ValueError(f"Unsupported provider: {provider}")
782
+ raise SigmaError(
783
+ ErrorCode.PROVIDER_UNAVAILABLE,
784
+ f"Unsupported provider: {provider}",
785
+ {"provider": str(provider)}
786
+ )