aiecs 1.2.0__py3-none-any.whl → 1.2.2__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.

Potentially problematic release.


This version of aiecs might be problematic. Click here for more details.

Files changed (31) hide show
  1. aiecs/__init__.py +1 -1
  2. aiecs/aiecs_client.py +1 -1
  3. aiecs/config/config.py +38 -1
  4. aiecs/infrastructure/monitoring/__init__.py +22 -0
  5. aiecs/infrastructure/monitoring/global_metrics_manager.py +207 -0
  6. aiecs/infrastructure/persistence/file_storage.py +41 -28
  7. aiecs/llm/__init__.py +44 -7
  8. aiecs/llm/callbacks/__init__.py +12 -0
  9. aiecs/llm/{custom_callbacks.py → callbacks/custom_callbacks.py} +1 -1
  10. aiecs/llm/client_factory.py +23 -6
  11. aiecs/llm/clients/__init__.py +35 -0
  12. aiecs/llm/{base_client.py → clients/base_client.py} +73 -1
  13. aiecs/llm/{googleai_client.py → clients/googleai_client.py} +19 -15
  14. aiecs/llm/{openai_client.py → clients/openai_client.py} +9 -14
  15. aiecs/llm/{vertex_client.py → clients/vertex_client.py} +15 -15
  16. aiecs/llm/{xai_client.py → clients/xai_client.py} +36 -50
  17. aiecs/llm/config/__init__.py +54 -0
  18. aiecs/llm/config/config_loader.py +275 -0
  19. aiecs/llm/config/config_validator.py +237 -0
  20. aiecs/llm/config/model_config.py +132 -0
  21. aiecs/llm/utils/__init__.py +11 -0
  22. aiecs/llm/utils/validate_config.py +91 -0
  23. aiecs/main.py +32 -2
  24. aiecs/tools/tool_executor/__init__.py +2 -2
  25. aiecs/tools/tool_executor/tool_executor.py +3 -3
  26. {aiecs-1.2.0.dist-info → aiecs-1.2.2.dist-info}/METADATA +1 -1
  27. {aiecs-1.2.0.dist-info → aiecs-1.2.2.dist-info}/RECORD +31 -22
  28. {aiecs-1.2.0.dist-info → aiecs-1.2.2.dist-info}/WHEEL +0 -0
  29. {aiecs-1.2.0.dist-info → aiecs-1.2.2.dist-info}/entry_points.txt +0 -0
  30. {aiecs-1.2.0.dist-info → aiecs-1.2.2.dist-info}/licenses/LICENSE +0 -0
  31. {aiecs-1.2.0.dist-info → aiecs-1.2.2.dist-info}/top_level.txt +0 -0
@@ -6,6 +6,12 @@ import logging
6
6
 
7
7
  logger = logging.getLogger(__name__)
8
8
 
9
+ # Lazy import to avoid circular dependency
10
+ def _get_config_loader():
11
+ """Lazy import of config loader to avoid circular dependency"""
12
+ from aiecs.llm.config import get_llm_config_loader
13
+ return get_llm_config_loader()
14
+
9
15
  @dataclass
10
16
  class LLMMessage:
11
17
  role: str # "system", "user", "assistant"
@@ -93,8 +99,74 @@ class BaseLLMClient(ABC):
93
99
  return len(text) // 4
94
100
 
95
101
  def _estimate_cost(self, model: str, input_tokens: int, output_tokens: int, token_costs: Dict) -> float:
96
- """Estimate the cost of the API call"""
102
+ """
103
+ Estimate the cost of the API call.
104
+
105
+ DEPRECATED: Use _estimate_cost_from_config instead for config-based cost estimation.
106
+ This method is kept for backward compatibility.
107
+ """
97
108
  if model in token_costs:
98
109
  costs = token_costs[model]
99
110
  return (input_tokens * costs["input"] + output_tokens * costs["output"]) / 1000
100
111
  return 0.0
112
+
113
+ def _estimate_cost_from_config(self, model_name: str, input_tokens: int, output_tokens: int) -> float:
114
+ """
115
+ Estimate the cost using configuration-based pricing.
116
+
117
+ Args:
118
+ model_name: Name of the model
119
+ input_tokens: Number of input tokens
120
+ output_tokens: Number of output tokens
121
+
122
+ Returns:
123
+ Estimated cost in USD
124
+ """
125
+ try:
126
+ loader = _get_config_loader()
127
+ model_config = loader.get_model_config(self.provider_name, model_name)
128
+
129
+ if model_config and model_config.costs:
130
+ input_cost = (input_tokens * model_config.costs.input) / 1000
131
+ output_cost = (output_tokens * model_config.costs.output) / 1000
132
+ return input_cost + output_cost
133
+ else:
134
+ self.logger.warning(
135
+ f"No cost configuration found for model {model_name} "
136
+ f"in provider {self.provider_name}"
137
+ )
138
+ return 0.0
139
+ except Exception as e:
140
+ self.logger.warning(f"Failed to estimate cost from config: {e}")
141
+ return 0.0
142
+
143
+ def _get_model_config(self, model_name: str):
144
+ """
145
+ Get model configuration from the config loader.
146
+
147
+ Args:
148
+ model_name: Name of the model
149
+
150
+ Returns:
151
+ ModelConfig if found, None otherwise
152
+ """
153
+ try:
154
+ loader = _get_config_loader()
155
+ return loader.get_model_config(self.provider_name, model_name)
156
+ except Exception as e:
157
+ self.logger.warning(f"Failed to get model config: {e}")
158
+ return None
159
+
160
+ def _get_default_model(self) -> Optional[str]:
161
+ """
162
+ Get the default model for this provider from configuration.
163
+
164
+ Returns:
165
+ Default model name if configured, None otherwise
166
+ """
167
+ try:
168
+ loader = _get_config_loader()
169
+ return loader.get_default_model(self.provider_name)
170
+ except Exception as e:
171
+ self.logger.warning(f"Failed to get default model: {e}")
172
+ return None
@@ -6,7 +6,7 @@ from typing import Dict, Any, Optional, List, AsyncGenerator
6
6
  import google.generativeai as genai
7
7
  from google.generativeai.types import GenerationConfig, HarmCategory, HarmBlockThreshold
8
8
 
9
- from aiecs.llm.base_client import BaseLLMClient, LLMMessage, LLMResponse, ProviderNotAvailableError, RateLimitError
9
+ from aiecs.llm.clients.base_client import BaseLLMClient, LLMMessage, LLMResponse, ProviderNotAvailableError, RateLimitError
10
10
  from aiecs.config.config import get_settings
11
11
 
12
12
  logger = logging.getLogger(__name__)
@@ -20,12 +20,6 @@ class GoogleAIClient(BaseLLMClient):
20
20
  self._initialized = False
21
21
  self.client = None
22
22
 
23
- # Token cost estimates for Gemini 2.5 series
24
- self.token_costs = {
25
- "gemini-2.5-pro": {"input": 0.00125, "output": 0.00375},
26
- "gemini-2.5-flash": {"input": 0.000075, "output": 0.0003},
27
- }
28
-
29
23
  def _init_google_ai(self):
30
24
  """Lazy initialization of Google AI SDK"""
31
25
  if not self._initialized:
@@ -50,7 +44,14 @@ class GoogleAIClient(BaseLLMClient):
50
44
  ) -> LLMResponse:
51
45
  """Generate text using Google AI"""
52
46
  self._init_google_ai()
53
- model_name = model or "gemini-2.5-pro"
47
+
48
+ # Get model name from config if not provided
49
+ model_name = model or self._get_default_model() or "gemini-2.5-pro"
50
+
51
+ # Get model config for default parameters
52
+ model_config = self._get_model_config(model_name)
53
+ if model_config and max_tokens is None:
54
+ max_tokens = model_config.default_params.max_tokens
54
55
 
55
56
  try:
56
57
  model_instance = genai.GenerativeModel(model_name)
@@ -88,12 +89,8 @@ class GoogleAIClient(BaseLLMClient):
88
89
  completion_tokens = response.usage_metadata.candidates_token_count
89
90
  total_tokens = response.usage_metadata.total_token_count
90
91
 
91
- cost = self._estimate_cost(
92
- model_name,
93
- prompt_tokens,
94
- completion_tokens,
95
- self.token_costs
96
- )
92
+ # Use config-based cost estimation
93
+ cost = self._estimate_cost_from_config(model_name, prompt_tokens, completion_tokens)
97
94
 
98
95
  return LLMResponse(
99
96
  content=content,
@@ -121,7 +118,14 @@ class GoogleAIClient(BaseLLMClient):
121
118
  ) -> AsyncGenerator[str, None]:
122
119
  """Stream text generation using Google AI"""
123
120
  self._init_google_ai()
124
- model_name = model or "gemini-2.5-pro"
121
+
122
+ # Get model name from config if not provided
123
+ model_name = model or self._get_default_model() or "gemini-2.5-pro"
124
+
125
+ # Get model config for default parameters
126
+ model_config = self._get_model_config(model_name)
127
+ if model_config and max_tokens is None:
128
+ max_tokens = model_config.default_params.max_tokens
125
129
 
126
130
  try:
127
131
  model_instance = genai.GenerativeModel(model_name)
@@ -5,7 +5,7 @@ from openai import AsyncOpenAI
5
5
  from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
6
6
  import httpx
7
7
 
8
- from aiecs.llm.base_client import BaseLLMClient, LLMMessage, LLMResponse, ProviderNotAvailableError, RateLimitError
8
+ from aiecs.llm.clients.base_client import BaseLLMClient, LLMMessage, LLMResponse, ProviderNotAvailableError, RateLimitError
9
9
  from aiecs.config.config import get_settings
10
10
 
11
11
  logger = logging.getLogger(__name__)
@@ -18,15 +18,6 @@ class OpenAIClient(BaseLLMClient):
18
18
  self.settings = get_settings()
19
19
  self._client: Optional[AsyncOpenAI] = None
20
20
 
21
- # Token cost estimates (USD per 1K tokens)
22
- self.token_costs = {
23
- "gpt-4": {"input": 0.03, "output": 0.06},
24
- "gpt-4-turbo": {"input": 0.01, "output": 0.03},
25
- "gpt-3.5-turbo": {"input": 0.0015, "output": 0.002},
26
- "gpt-4o": {"input": 0.005, "output": 0.015},
27
- "gpt-4o-mini": {"input": 0.00015, "output": 0.0006},
28
- }
29
-
30
21
  def _get_client(self) -> AsyncOpenAI:
31
22
  """Lazy initialization of OpenAI client"""
32
23
  if not self._client:
@@ -50,7 +41,9 @@ class OpenAIClient(BaseLLMClient):
50
41
  ) -> LLMResponse:
51
42
  """Generate text using OpenAI API"""
52
43
  client = self._get_client()
53
- model = model or "gpt-4-turbo"
44
+
45
+ # Get model name from config if not provided
46
+ model = model or self._get_default_model() or "gpt-4-turbo"
54
47
 
55
48
  # Convert to OpenAI message format
56
49
  openai_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
@@ -67,10 +60,10 @@ class OpenAIClient(BaseLLMClient):
67
60
  content = response.choices[0].message.content
68
61
  tokens_used = response.usage.total_tokens if response.usage else None
69
62
 
70
- # Estimate cost
63
+ # Estimate cost using config
71
64
  input_tokens = response.usage.prompt_tokens if response.usage else 0
72
65
  output_tokens = response.usage.completion_tokens if response.usage else 0
73
- cost = self._estimate_cost(model, input_tokens, output_tokens, self.token_costs)
66
+ cost = self._estimate_cost_from_config(model, input_tokens, output_tokens)
74
67
 
75
68
  return LLMResponse(
76
69
  content=content,
@@ -95,7 +88,9 @@ class OpenAIClient(BaseLLMClient):
95
88
  ) -> AsyncGenerator[str, None]:
96
89
  """Stream text using OpenAI API"""
97
90
  client = self._get_client()
98
- model = model or "gpt-4-turbo"
91
+
92
+ # Get model name from config if not provided
93
+ model = model or self._get_default_model() or "gpt-4-turbo"
99
94
 
100
95
  openai_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
101
96
 
@@ -6,7 +6,7 @@ import vertexai
6
6
  from vertexai.generative_models import GenerativeModel, HarmCategory, HarmBlockThreshold, GenerationConfig, SafetySetting
7
7
  from google.oauth2 import service_account
8
8
 
9
- from aiecs.llm.base_client import BaseLLMClient, LLMMessage, LLMResponse, ProviderNotAvailableError, RateLimitError
9
+ from aiecs.llm.clients.base_client import BaseLLMClient, LLMMessage, LLMResponse, ProviderNotAvailableError, RateLimitError
10
10
  from aiecs.config.config import get_settings
11
11
 
12
12
  logger = logging.getLogger(__name__)
@@ -19,12 +19,6 @@ class VertexAIClient(BaseLLMClient):
19
19
  self.settings = get_settings()
20
20
  self._initialized = False
21
21
 
22
- # Token cost estimates (USD per 1K tokens)
23
- self.token_costs = {
24
- "gemini-2.5-pro": {"input": 0.00125, "output": 0.00375},
25
- "gemini-2.5-flash": {"input": 0.000075, "output": 0.0003},
26
- }
27
-
28
22
  def _init_vertex_ai(self):
29
23
  """Lazy initialization of Vertex AI with proper authentication"""
30
24
  if not self._initialized:
@@ -71,7 +65,14 @@ class VertexAIClient(BaseLLMClient):
71
65
  ) -> LLMResponse:
72
66
  """Generate text using Vertex AI"""
73
67
  self._init_vertex_ai()
74
- model_name = model or "gemini-2.5-pro"
68
+
69
+ # Get model name from config if not provided
70
+ model_name = model or self._get_default_model() or "gemini-2.5-pro"
71
+
72
+ # Get model config for default parameters
73
+ model_config = self._get_model_config(model_name)
74
+ if model_config and max_tokens is None:
75
+ max_tokens = model_config.default_params.max_tokens
75
76
 
76
77
  try:
77
78
  # Use the stable Vertex AI API
@@ -163,13 +164,12 @@ class VertexAIClient(BaseLLMClient):
163
164
  content = "[Response error: Cannot get the response text. Multiple content parts are not supported.]"
164
165
 
165
166
  # Vertex AI doesn't provide detailed token usage in the response
166
- tokens_used = self._count_tokens_estimate(prompt + content)
167
- cost = self._estimate_cost(
168
- model_name,
169
- self._count_tokens_estimate(prompt),
170
- self._count_tokens_estimate(content),
171
- self.token_costs
172
- )
167
+ input_tokens = self._count_tokens_estimate(prompt)
168
+ output_tokens = self._count_tokens_estimate(content)
169
+ tokens_used = input_tokens + output_tokens
170
+
171
+ # Use config-based cost estimation
172
+ cost = self._estimate_cost_from_config(model_name, input_tokens, output_tokens)
173
173
 
174
174
  return LLMResponse(
175
175
  content=content,
@@ -2,10 +2,17 @@ import json
2
2
  import asyncio
3
3
  import logging
4
4
  from typing import Dict, Any, Optional, List, AsyncGenerator
5
+
6
+ # Lazy import to avoid circular dependency
7
+ def _get_config_loader():
8
+ """Lazy import of config loader to avoid circular dependency"""
9
+ from aiecs.llm.config import get_llm_config_loader
10
+ return get_llm_config_loader()
11
+
5
12
  from openai import AsyncOpenAI
6
13
  from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
7
14
 
8
- from aiecs.llm.base_client import BaseLLMClient, LLMMessage, LLMResponse, ProviderNotAvailableError, RateLimitError
15
+ from aiecs.llm.clients.base_client import BaseLLMClient, LLMMessage, LLMResponse, ProviderNotAvailableError, RateLimitError
9
16
  from aiecs.config.config import get_settings
10
17
 
11
18
  logger = logging.getLogger(__name__)
@@ -17,51 +24,7 @@ class XAIClient(BaseLLMClient):
17
24
  super().__init__("xAI")
18
25
  self.settings = get_settings()
19
26
  self._openai_client: Optional[AsyncOpenAI] = None
20
-
21
- # Enhanced model mapping for all Grok models
22
- self.model_map = {
23
- # Legacy Grok models
24
- "grok-beta": "grok-beta",
25
- "grok": "grok-beta",
26
-
27
- # Current Grok models
28
- "Grok 2": "grok-2",
29
- "grok-2": "grok-2",
30
- "Grok 2 Vision": "grok-2-vision",
31
- "grok-2-vision": "grok-2-vision",
32
-
33
- # Grok 3 models
34
- "Grok 3 Normal": "grok-3",
35
- "grok-3": "grok-3",
36
- "Grok 3 Fast": "grok-3-fast",
37
- "grok-3-fast": "grok-3-fast",
38
-
39
- # Grok 3 Mini models
40
- "Grok 3 Mini Normal": "grok-3-mini",
41
- "grok-3-mini": "grok-3-mini",
42
- "Grok 3 Mini Fast": "grok-3-mini-fast",
43
- "grok-3-mini-fast": "grok-3-mini-fast",
44
-
45
- # Grok 3 Reasoning models
46
- "Grok 3 Reasoning Normal": "grok-3-reasoning",
47
- "grok-3-reasoning": "grok-3-reasoning",
48
- "Grok 3 Reasoning Fast": "grok-3-reasoning-fast",
49
- "grok-3-reasoning-fast": "grok-3-reasoning-fast",
50
-
51
- # Grok 3 Mini Reasoning models
52
- "Grok 3 Mini Reasoning Normal": "grok-3-mini-reasoning",
53
- "grok-3-mini-reasoning": "grok-3-mini-reasoning",
54
- "Grok 3 Mini Reasoning Fast": "grok-3-mini-reasoning-fast",
55
- "grok-3-mini-reasoning-fast": "grok-3-mini-reasoning-fast",
56
-
57
- # Grok 4 models
58
- "Grok 4 Normal": "grok-4",
59
- "grok-4": "grok-4",
60
- "Grok 4 Fast": "grok-4-fast",
61
- "grok-4-fast": "grok-4-fast",
62
- "Grok 4 0709": "grok-4-0709",
63
- "grok-4-0709": "grok-4-0709",
64
- }
27
+ self._model_map: Optional[Dict[str, str]] = None
65
28
 
66
29
  def _get_openai_client(self) -> AsyncOpenAI:
67
30
  """Lazy initialization of OpenAI client for XAI"""
@@ -81,6 +44,21 @@ class XAIClient(BaseLLMClient):
81
44
  if not api_key:
82
45
  raise ProviderNotAvailableError("xAI API key not configured")
83
46
  return api_key
47
+
48
+ def _get_model_map(self) -> Dict[str, str]:
49
+ """Get model mappings from configuration"""
50
+ if self._model_map is None:
51
+ try:
52
+ loader = _get_config_loader()
53
+ provider_config = loader.get_provider_config("xAI")
54
+ if provider_config and provider_config.model_mappings:
55
+ self._model_map = provider_config.model_mappings
56
+ else:
57
+ self._model_map = {}
58
+ except Exception as e:
59
+ self.logger.warning(f"Failed to load model mappings from config: {e}")
60
+ self._model_map = {}
61
+ return self._model_map
84
62
 
85
63
  @retry(
86
64
  stop=stop_after_attempt(3),
@@ -103,8 +81,12 @@ class XAIClient(BaseLLMClient):
103
81
 
104
82
  client = self._get_openai_client()
105
83
 
106
- selected_model = model or "grok-4" # Default to grok-4 as in the example
107
- api_model = self.model_map.get(selected_model, selected_model)
84
+ # Get model name from config if not provided
85
+ selected_model = model or self._get_default_model() or "grok-4"
86
+
87
+ # Get model mappings from config
88
+ model_map = self._get_model_map()
89
+ api_model = model_map.get(selected_model, selected_model)
108
90
 
109
91
  # Convert to OpenAI format
110
92
  openai_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
@@ -151,8 +133,12 @@ class XAIClient(BaseLLMClient):
151
133
 
152
134
  client = self._get_openai_client()
153
135
 
154
- selected_model = model or "grok-4" # Default to grok-4
155
- api_model = self.model_map.get(selected_model, selected_model)
136
+ # Get model name from config if not provided
137
+ selected_model = model or self._get_default_model() or "grok-4"
138
+
139
+ # Get model mappings from config
140
+ model_map = self._get_model_map()
141
+ api_model = model_map.get(selected_model, selected_model)
156
142
 
157
143
  # Convert to OpenAI format
158
144
  openai_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
@@ -0,0 +1,54 @@
1
+ """
2
+ LLM Configuration management.
3
+
4
+ This package handles configuration loading, validation, and management
5
+ for all LLM providers and models.
6
+ """
7
+
8
+ from .model_config import (
9
+ ModelCostConfig,
10
+ ModelCapabilities,
11
+ ModelDefaultParams,
12
+ ModelConfig,
13
+ ProviderConfig,
14
+ LLMModelsConfig
15
+ )
16
+ from .config_loader import (
17
+ LLMConfigLoader,
18
+ get_llm_config_loader,
19
+ get_llm_config,
20
+ reload_llm_config
21
+ )
22
+ from .config_validator import (
23
+ ConfigValidationError,
24
+ validate_cost_config,
25
+ validate_model_config,
26
+ validate_provider_config,
27
+ validate_llm_config,
28
+ validate_config_file
29
+ )
30
+
31
+ __all__ = [
32
+ # Configuration models
33
+ "ModelCostConfig",
34
+ "ModelCapabilities",
35
+ "ModelDefaultParams",
36
+ "ModelConfig",
37
+ "ProviderConfig",
38
+ "LLMModelsConfig",
39
+
40
+ # Config loader
41
+ "LLMConfigLoader",
42
+ "get_llm_config_loader",
43
+ "get_llm_config",
44
+ "reload_llm_config",
45
+
46
+ # Validation
47
+ "ConfigValidationError",
48
+ "validate_cost_config",
49
+ "validate_model_config",
50
+ "validate_provider_config",
51
+ "validate_llm_config",
52
+ "validate_config_file",
53
+ ]
54
+