sigma-terminal 3.3.2__py3-none-any.whl → 3.4.0__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.0."""
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.0"
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.0[/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.0 - 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][ok] 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.0."""
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.0"
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,38 @@ class LLMProvider(str, Enum):
24
73
  OLLAMA = "ollama"
25
74
 
26
75
 
27
- # Available models per provider
76
+ # Available models per provider (Feb 2026 - LATEST MODELS ONLY)
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", # Latest fast model
80
+ "gemini-3-pro", # Latest capable model
81
+ ],
82
+ "openai": [
83
+ "gpt-5", # Latest flagship
84
+ "gpt-5-mini", # Latest efficient
85
+ "o3", # Latest reasoning
86
+ "o3-mini", # Fast reasoning
87
+ ],
88
+ "anthropic": [
89
+ "claude-sonnet-4-20250514", # Latest Sonnet
90
+ "claude-opus-4-20250514", # Latest Opus
91
+ ],
92
+ "groq": [
93
+ "llama-3.3-70b-versatile", # Most capable free
94
+ "llama-3.3-8b-instant", # Fast free
95
+ "mixtral-8x7b-32768", # Good balance free
96
+ ],
97
+ "xai": [
98
+ "grok-3", # Latest full capability
99
+ "grok-3-mini", # Latest fast variant
100
+ ],
101
+ "ollama": [
102
+ "llama3.3", # Latest local LLM
103
+ "llama3.2",
104
+ "mistral",
105
+ "phi4", # Latest Phi
106
+ "qwen2.5", # Latest Qwen
107
+ ],
35
108
  }
36
109
 
37
110
  # Config directory
@@ -41,8 +114,40 @@ FIRST_RUN_MARKER = CONFIG_DIR / ".first_run_complete"
41
114
 
42
115
 
43
116
  def is_first_run() -> bool:
44
- """Check if this is the first run of the application."""
45
- return not FIRST_RUN_MARKER.exists()
117
+ """
118
+ Check if this is the first run of the application.
119
+
120
+ Returns False (not first run) if:
121
+ - The first run marker exists, OR
122
+ - A config file exists with at least one API key configured
123
+
124
+ This ensures users who upgrade from older versions or manually
125
+ configure their ~/.sigma/config.env don't see the setup wizard.
126
+ """
127
+ # Check for explicit marker
128
+ if FIRST_RUN_MARKER.exists():
129
+ return False
130
+
131
+ # Check if config file exists and has API keys
132
+ if CONFIG_FILE.exists():
133
+ try:
134
+ content = CONFIG_FILE.read_text()
135
+ # Check if any API key is set (not empty)
136
+ api_keys = ["GOOGLE_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY",
137
+ "GROQ_API_KEY", "XAI_API_KEY"]
138
+ for key in api_keys:
139
+ # Look for KEY=value where value is not empty
140
+ for line in content.splitlines():
141
+ if line.startswith(f"{key}="):
142
+ value = line.split("=", 1)[1].strip()
143
+ if value and value not in ('""', "''", ""):
144
+ # Found a configured API key - not first run
145
+ mark_first_run_complete() # Create marker for future
146
+ return False
147
+ except Exception:
148
+ pass
149
+
150
+ return True
46
151
 
47
152
 
48
153
  def mark_first_run_complete() -> None:
@@ -205,22 +310,22 @@ class Settings(BaseSettings):
205
310
 
206
311
  # Provider settings
207
312
  default_provider: LLMProvider = LLMProvider.GOOGLE
208
- default_model: str = Field(default="gemini-2.0-flash", alias="DEFAULT_MODEL")
313
+ default_model: str = Field(default="gemini-3-flash", alias="DEFAULT_MODEL")
209
314
 
210
- # API Keys
315
+ # LLM API Keys
211
316
  google_api_key: Optional[str] = Field(default=None, alias="GOOGLE_API_KEY")
212
317
  openai_api_key: Optional[str] = Field(default=None, alias="OPENAI_API_KEY")
213
318
  anthropic_api_key: Optional[str] = Field(default=None, alias="ANTHROPIC_API_KEY")
214
319
  groq_api_key: Optional[str] = Field(default=None, alias="GROQ_API_KEY")
215
320
  xai_api_key: Optional[str] = Field(default=None, alias="XAI_API_KEY")
216
321
 
217
- # Model settings
218
- google_model: str = "gemini-2.0-flash"
219
- openai_model: str = "gpt-4o"
322
+ # Model settings - LATEST MODELS ONLY (Feb 2026)
323
+ google_model: str = "gemini-3-flash"
324
+ openai_model: str = "gpt-5"
220
325
  anthropic_model: str = "claude-sonnet-4-20250514"
221
326
  groq_model: str = "llama-3.3-70b-versatile"
222
- xai_model: str = "grok-2"
223
- ollama_model: str = "llama3.2"
327
+ xai_model: str = "grok-3"
328
+ ollama_model: str = "llama3.3"
224
329
 
225
330
  # Ollama settings
226
331
  ollama_host: str = "http://localhost:11434"
@@ -232,6 +337,7 @@ class Settings(BaseSettings):
232
337
 
233
338
  # Data API keys
234
339
  alpha_vantage_api_key: str = Field(default="6ER128DD3NQUPTVC", alias="ALPHA_VANTAGE_API_KEY")
340
+ polygon_api_key: Optional[str] = Field(default=None, alias="POLYGON_API_KEY")
235
341
  exa_api_key: Optional[str] = Field(default=None, alias="EXA_API_KEY")
236
342
 
237
343
  class Config:
@@ -287,38 +393,70 @@ def get_settings() -> Settings:
287
393
  return Settings()
288
394
 
289
395
 
290
- def save_api_key(provider: str, key: str) -> None:
291
- """Save an API key to the config file."""
396
+ def save_api_key(provider: str, key: str) -> bool:
397
+ """Save an API key to the config file. Returns True on success."""
292
398
  CONFIG_DIR.mkdir(parents=True, exist_ok=True)
293
399
 
294
400
  # Read existing config
295
401
  config = {}
296
402
  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
403
+ try:
404
+ with open(CONFIG_FILE) as f:
405
+ for line in f:
406
+ line = line.strip()
407
+ if "=" in line and not line.startswith("#"):
408
+ k, v = line.split("=", 1)
409
+ config[k] = v
410
+ except IOError:
411
+ pass
303
412
 
304
- # Update key
413
+ # Map provider names to config keys (LLM + Data providers)
305
414
  key_map = {
415
+ # LLM providers
306
416
  "google": "GOOGLE_API_KEY",
307
417
  "openai": "OPENAI_API_KEY",
308
418
  "anthropic": "ANTHROPIC_API_KEY",
309
419
  "groq": "GROQ_API_KEY",
310
420
  "xai": "XAI_API_KEY",
421
+ # Data providers
422
+ "polygon": "POLYGON_API_KEY",
423
+ "alphavantage": "ALPHA_VANTAGE_API_KEY",
424
+ "exa": "EXA_API_KEY",
311
425
  }
312
426
 
313
427
  env_key = key_map.get(provider.lower())
314
- if env_key:
315
- config[env_key] = key
428
+ if not env_key:
429
+ return False
430
+
431
+ config[env_key] = key
316
432
 
317
433
  # 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")
434
+ try:
435
+ with open(CONFIG_FILE, "w") as f:
436
+ f.write("# Sigma Configuration\n")
437
+ f.write(f"# Updated: {__import__('datetime').datetime.now().isoformat()}\n\n")
438
+
439
+ # Group by type for readability
440
+ llm_keys = ["GOOGLE_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GROQ_API_KEY", "XAI_API_KEY"]
441
+ data_keys = ["POLYGON_API_KEY", "ALPHA_VANTAGE_API_KEY", "EXA_API_KEY"]
442
+
443
+ f.write("# LLM Provider Keys\n")
444
+ for k in llm_keys:
445
+ if k in config:
446
+ f.write(f"{k}={config[k]}\n")
447
+
448
+ f.write("\n# Data Provider Keys\n")
449
+ for k in data_keys:
450
+ if k in config:
451
+ f.write(f"{k}={config[k]}\n")
452
+
453
+ f.write("\n# Other Settings\n")
454
+ for k, v in sorted(config.items()):
455
+ if k not in llm_keys and k not in data_keys:
456
+ f.write(f"{k}={v}\n")
457
+ return True
458
+ except IOError:
459
+ return False
322
460
 
323
461
 
324
462
  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
@@ -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
+ )