kader 0.1.1__tar.gz → 0.1.3__tar.gz

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.
Files changed (78) hide show
  1. {kader-0.1.1 → kader-0.1.3}/PKG-INFO +2 -1
  2. {kader-0.1.1 → kader-0.1.3}/cli/app.py +124 -2
  3. {kader-0.1.1 → kader-0.1.3}/cli/app.tcss +17 -0
  4. {kader-0.1.1 → kader-0.1.3}/cli/utils.py +1 -0
  5. {kader-0.1.1 → kader-0.1.3}/kader/agent/base.py +8 -6
  6. {kader-0.1.1 → kader-0.1.3}/kader/agent/logger.py +1 -19
  7. {kader-0.1.1 → kader-0.1.3}/pyproject.toml +2 -1
  8. {kader-0.1.1 → kader-0.1.3}/tests/test_agent_logger.py +2 -99
  9. {kader-0.1.1 → kader-0.1.3}/uv.lock +132 -1
  10. {kader-0.1.1 → kader-0.1.3}/.github/workflows/ci.yml +0 -0
  11. {kader-0.1.1 → kader-0.1.3}/.github/workflows/release.yml +0 -0
  12. {kader-0.1.1 → kader-0.1.3}/.gitignore +0 -0
  13. {kader-0.1.1 → kader-0.1.3}/.python-version +0 -0
  14. {kader-0.1.1 → kader-0.1.3}/.qwen/QWEN.md +0 -0
  15. {kader-0.1.1 → kader-0.1.3}/.qwen/agents/technical-writer.md +0 -0
  16. {kader-0.1.1 → kader-0.1.3}/.qwen/agents/test-automation-specialist.md +0 -0
  17. {kader-0.1.1 → kader-0.1.3}/README.md +0 -0
  18. {kader-0.1.1 → kader-0.1.3}/cli/README.md +0 -0
  19. {kader-0.1.1 → kader-0.1.3}/cli/__init__.py +0 -0
  20. {kader-0.1.1 → kader-0.1.3}/cli/__main__.py +0 -0
  21. {kader-0.1.1 → kader-0.1.3}/cli/widgets/__init__.py +0 -0
  22. {kader-0.1.1 → kader-0.1.3}/cli/widgets/confirmation.py +0 -0
  23. {kader-0.1.1 → kader-0.1.3}/cli/widgets/conversation.py +0 -0
  24. {kader-0.1.1 → kader-0.1.3}/cli/widgets/loading.py +0 -0
  25. {kader-0.1.1 → kader-0.1.3}/examples/.gitignore +0 -0
  26. {kader-0.1.1 → kader-0.1.3}/examples/README.md +0 -0
  27. {kader-0.1.1 → kader-0.1.3}/examples/memory_example.py +0 -0
  28. {kader-0.1.1 → kader-0.1.3}/examples/ollama_example.py +0 -0
  29. {kader-0.1.1 → kader-0.1.3}/examples/planning_agent_example.py +0 -0
  30. {kader-0.1.1 → kader-0.1.3}/examples/python_developer/main.py +0 -0
  31. {kader-0.1.1 → kader-0.1.3}/examples/python_developer/template.yaml +0 -0
  32. {kader-0.1.1 → kader-0.1.3}/examples/react_agent_example.py +0 -0
  33. {kader-0.1.1 → kader-0.1.3}/examples/simple_agent.py +0 -0
  34. {kader-0.1.1 → kader-0.1.3}/examples/todo_agent/main.py +0 -0
  35. {kader-0.1.1 → kader-0.1.3}/examples/tools_example.py +0 -0
  36. {kader-0.1.1 → kader-0.1.3}/kader/__init__.py +0 -0
  37. {kader-0.1.1 → kader-0.1.3}/kader/agent/__init__.py +0 -0
  38. {kader-0.1.1 → kader-0.1.3}/kader/agent/agents.py +0 -0
  39. {kader-0.1.1 → kader-0.1.3}/kader/config.py +0 -0
  40. {kader-0.1.1 → kader-0.1.3}/kader/memory/__init__.py +0 -0
  41. {kader-0.1.1 → kader-0.1.3}/kader/memory/conversation.py +0 -0
  42. {kader-0.1.1 → kader-0.1.3}/kader/memory/session.py +0 -0
  43. {kader-0.1.1 → kader-0.1.3}/kader/memory/state.py +0 -0
  44. {kader-0.1.1 → kader-0.1.3}/kader/memory/types.py +0 -0
  45. {kader-0.1.1 → kader-0.1.3}/kader/prompts/__init__.py +0 -0
  46. {kader-0.1.1 → kader-0.1.3}/kader/prompts/agent_prompts.py +0 -0
  47. {kader-0.1.1 → kader-0.1.3}/kader/prompts/base.py +0 -0
  48. {kader-0.1.1 → kader-0.1.3}/kader/prompts/templates/planning_agent.j2 +0 -0
  49. {kader-0.1.1 → kader-0.1.3}/kader/prompts/templates/react_agent.j2 +0 -0
  50. {kader-0.1.1 → kader-0.1.3}/kader/providers/__init__.py +0 -0
  51. {kader-0.1.1 → kader-0.1.3}/kader/providers/base.py +0 -0
  52. {kader-0.1.1 → kader-0.1.3}/kader/providers/mock.py +0 -0
  53. {kader-0.1.1 → kader-0.1.3}/kader/providers/ollama.py +0 -0
  54. {kader-0.1.1 → kader-0.1.3}/kader/tools/README.md +0 -0
  55. {kader-0.1.1 → kader-0.1.3}/kader/tools/__init__.py +0 -0
  56. {kader-0.1.1 → kader-0.1.3}/kader/tools/base.py +0 -0
  57. {kader-0.1.1 → kader-0.1.3}/kader/tools/exec_commands.py +0 -0
  58. {kader-0.1.1 → kader-0.1.3}/kader/tools/filesys.py +0 -0
  59. {kader-0.1.1 → kader-0.1.3}/kader/tools/filesystem.py +0 -0
  60. {kader-0.1.1 → kader-0.1.3}/kader/tools/protocol.py +0 -0
  61. {kader-0.1.1 → kader-0.1.3}/kader/tools/rag.py +0 -0
  62. {kader-0.1.1 → kader-0.1.3}/kader/tools/todo.py +0 -0
  63. {kader-0.1.1 → kader-0.1.3}/kader/tools/utils.py +0 -0
  64. {kader-0.1.1 → kader-0.1.3}/kader/tools/web.py +0 -0
  65. {kader-0.1.1 → kader-0.1.3}/tests/conftest.py +0 -0
  66. {kader-0.1.1 → kader-0.1.3}/tests/providers/test_mock.py +0 -0
  67. {kader-0.1.1 → kader-0.1.3}/tests/providers/test_ollama.py +0 -0
  68. {kader-0.1.1 → kader-0.1.3}/tests/providers/test_providers_base.py +0 -0
  69. {kader-0.1.1 → kader-0.1.3}/tests/test_agent_logger_integration.py +0 -0
  70. {kader-0.1.1 → kader-0.1.3}/tests/test_base_agent.py +0 -0
  71. {kader-0.1.1 → kader-0.1.3}/tests/test_file_memory.py +0 -0
  72. {kader-0.1.1 → kader-0.1.3}/tests/test_todo_tool.py +0 -0
  73. {kader-0.1.1 → kader-0.1.3}/tests/tools/test_exec_commands.py +0 -0
  74. {kader-0.1.1 → kader-0.1.3}/tests/tools/test_filesys_tools.py +0 -0
  75. {kader-0.1.1 → kader-0.1.3}/tests/tools/test_filesystem_tools.py +0 -0
  76. {kader-0.1.1 → kader-0.1.3}/tests/tools/test_rag.py +0 -0
  77. {kader-0.1.1 → kader-0.1.3}/tests/tools/test_tools_base.py +0 -0
  78. {kader-0.1.1 → kader-0.1.3}/tests/tools/test_web.py +0 -0
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kader
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: kader coding agent
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: faiss-cpu>=1.9.0
7
7
  Requires-Dist: jinja2>=3.1.6
8
8
  Requires-Dist: loguru>=0.7.3
9
9
  Requires-Dist: ollama>=0.6.1
10
+ Requires-Dist: outdated>=0.2.2
10
11
  Requires-Dist: pyyaml>=6.0.3
11
12
  Requires-Dist: tenacity>=9.1.2
12
13
  Requires-Dist: textual[syntax]>=6.8.0
@@ -2,6 +2,7 @@
2
2
 
3
3
  import asyncio
4
4
  import threading
5
+ from importlib.metadata import version as get_version
5
6
  from pathlib import Path
6
7
  from typing import Optional
7
8
 
@@ -32,9 +33,19 @@ from .utils import (
32
33
  )
33
34
  from .widgets import ConversationView, InlineSelector, LoadingSpinner, ModelSelector
34
35
 
35
- WELCOME_MESSAGE = """# Welcome to Kader CLI! 🚀
36
+ WELCOME_MESSAGE = """
37
+ <div align="center">
36
38
 
37
- Your **modern AI-powered coding assistant**.
39
+ ```
40
+ ██╗ ██╗ ██╗ █████╗ ██████╗ ███████╗██████╗
41
+ ██╔╝ ██║ ██╔╝██╔══██╗██╔══██╗██╔════╝██╔══██╗
42
+ ██╔╝ █████╔╝ ███████║██║ ██║█████╗ ██████╔╝
43
+ ██╔╝ ██╔═██╗ ██╔══██║██║ ██║██╔══╝ ██╔══██╗
44
+ ██╔╝ ██║ ██╗██║ ██║██████╔╝███████╗██║ ██║
45
+ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝
46
+ ```
47
+
48
+ </div>
38
49
 
39
50
  Type a message below to start chatting, or use one of the commands:
40
51
 
@@ -45,10 +56,16 @@ Type a message below to start chatting, or use one of the commands:
45
56
  - `/save` - Save current session
46
57
  - `/load` - Load a saved session
47
58
  - `/sessions` - List saved sessions
59
+ - `/cost` - Show the cost of the conversation
48
60
  - `/exit` - Exit the application
49
61
  """
50
62
 
51
63
 
64
+ # Minimum terminal size to prevent UI breakage
65
+ MIN_WIDTH = 89
66
+ MIN_HEIGHT = 29
67
+
68
+
52
69
  class KaderApp(App):
53
70
  """Main Kader CLI application."""
54
71
 
@@ -81,6 +98,7 @@ class KaderApp(App):
81
98
  self._confirmation_result: tuple[bool, Optional[str]] = (True, None)
82
99
  self._inline_selector: Optional[InlineSelector] = None
83
100
  self._model_selector: Optional[ModelSelector] = None
101
+ self._update_info: Optional[str] = None # Latest version if update available
84
102
 
85
103
  self._agent = self._create_agent(self._current_model)
86
104
 
@@ -291,6 +309,71 @@ class KaderApp(App):
291
309
  # Focus the input
292
310
  self.query_one("#prompt-input", Input).focus()
293
311
 
312
+ # Check initial size
313
+ self._check_terminal_size()
314
+
315
+ # Start background update check
316
+ threading.Thread(target=self._check_for_updates, daemon=True).start()
317
+
318
+ def _check_for_updates(self) -> None:
319
+ """Check for package updates in background thread."""
320
+ try:
321
+ from outdated import check_outdated
322
+
323
+ current_version = get_version("kader")
324
+ is_outdated, latest_version = check_outdated("kader", current_version)
325
+
326
+ if is_outdated:
327
+ self._update_info = latest_version
328
+ # Schedule UI update on main thread
329
+ self.call_from_thread(self._show_update_notification)
330
+ except Exception:
331
+ # Silently ignore update check failures
332
+ pass
333
+
334
+ def _show_update_notification(self) -> None:
335
+ """Show update notification as a toast."""
336
+ if not self._update_info:
337
+ return
338
+
339
+ try:
340
+ current = get_version("kader")
341
+ message = (
342
+ f">> Update available! v{current} → v{self._update_info} "
343
+ f"Run: uv tool upgrade kader"
344
+ )
345
+ self.notify(message, severity="information", timeout=10)
346
+ except Exception:
347
+ pass
348
+
349
+ def on_resize(self) -> None:
350
+ """Handle terminal resize events."""
351
+ self._check_terminal_size()
352
+
353
+ def _check_terminal_size(self) -> None:
354
+ """Check if terminal is large enough and show warning if not."""
355
+ width = self.console.size.width
356
+ height = self.console.size.height
357
+
358
+ # Check if we need to show/hide the size warning
359
+ too_small = width < MIN_WIDTH or height < MIN_HEIGHT
360
+
361
+ try:
362
+ warning = self.query_one("#size-warning", Static)
363
+ if not too_small:
364
+ warning.remove()
365
+ except Exception:
366
+ if too_small:
367
+ # Show warning overlay
368
+ warning_text = f"""⚠️ Terminal Too Small
369
+
370
+ Current: {width}x{height}
371
+ Minimum: {MIN_WIDTH}x{MIN_HEIGHT}
372
+
373
+ Please resize your terminal."""
374
+ warning = Static(warning_text, id="size-warning")
375
+ self.mount(warning)
376
+
294
377
  async def on_input_submitted(self, event: Input.Submitted) -> None:
295
378
  """Handle user input submission."""
296
379
  user_input = event.value.strip()
@@ -324,6 +407,7 @@ class KaderApp(App):
324
407
  elif cmd == "/clear":
325
408
  conversation.clear_messages()
326
409
  self._agent.memory.clear()
410
+ self._agent.provider.reset_tracking() # Reset usage/cost tracking
327
411
  self._current_session_id = None
328
412
  self.notify("Conversation cleared!", severity="information")
329
413
  elif cmd == "/save":
@@ -342,6 +426,8 @@ class KaderApp(App):
342
426
  elif cmd == "/refresh":
343
427
  self._refresh_directory_tree()
344
428
  self.notify("Directory tree refreshed!", severity="information")
429
+ elif cmd == "/cost":
430
+ self._handle_cost(conversation)
345
431
  elif cmd == "/exit":
346
432
  self.exit()
347
433
  else:
@@ -536,6 +622,42 @@ class KaderApp(App):
536
622
  conversation.add_message(f"❌ Error listing sessions: {e}", "assistant")
537
623
  self.notify(f"Error: {e}", severity="error")
538
624
 
625
+ def _handle_cost(self, conversation: ConversationView) -> None:
626
+ """Display LLM usage costs."""
627
+ try:
628
+ # Get cost and usage from the provider
629
+ cost = self._agent.provider.total_cost
630
+ usage = self._agent.provider.total_usage
631
+ model = self._agent.provider.model
632
+
633
+ lines = [
634
+ "## Usage Costs 💰\n",
635
+ f"**Model:** `{model}`\n",
636
+ "### Cost Breakdown",
637
+ "| Type | Amount |",
638
+ "|------|--------|",
639
+ f"| Input Cost | ${cost.input_cost:.6f} |",
640
+ f"| Output Cost | ${cost.output_cost:.6f} |",
641
+ f"| **Total Cost** | **${cost.total_cost:.6f}** |",
642
+ "",
643
+ "### Token Usage",
644
+ "| Type | Tokens |",
645
+ "|------|--------|",
646
+ f"| Prompt Tokens | {usage.prompt_tokens:,} |",
647
+ f"| Completion Tokens | {usage.completion_tokens:,} |",
648
+ f"| **Total Tokens** | **{usage.total_tokens:,}** |",
649
+ ]
650
+
651
+ if cost.total_cost == 0.0:
652
+ lines.append(
653
+ "\n> 💡 *Note: Ollama runs locally, so there are no API costs.*"
654
+ )
655
+
656
+ conversation.add_message("\n".join(lines), "assistant")
657
+ except Exception as e:
658
+ conversation.add_message(f"❌ Error getting costs: {e}", "assistant")
659
+ self.notify(f"Error: {e}", severity="error")
660
+
539
661
 
540
662
  def main() -> None:
541
663
  """Run the Kader CLI application."""
@@ -17,6 +17,23 @@ $text-muted: #6c7086;
17
17
 
18
18
  Screen {
19
19
  background: $background;
20
+ min-width: 89;
21
+ min-height: 29;
22
+ }
23
+
24
+ /* ===== Size Warning Overlay ===== */
25
+
26
+ #size-warning {
27
+ dock: top;
28
+ width: 100%;
29
+ height: 100%;
30
+ background: $background 95%;
31
+ color: $warning;
32
+ text-align: center;
33
+ content-align: center middle;
34
+ text-style: bold;
35
+ padding: 2;
36
+ layer: overlay;
20
37
  }
21
38
 
22
39
  /* ===== Header ===== */
@@ -19,6 +19,7 @@ HELP_TEXT = """## Kader CLI Commands 📖
19
19
  | `/save` | Save current session |
20
20
  | `/load <id>` | Load a saved session |
21
21
  | `/sessions` | List saved sessions |
22
+ | `/cost` | Show usage costs |
22
23
  | `/refresh` | Refresh file tree |
23
24
  | `/exit` | Exit the CLI |
24
25
 
@@ -559,12 +559,13 @@ class BaseAgent:
559
559
  token_usage["total_tokens"],
560
560
  )
561
561
 
562
+ # estimate the cost...
563
+ estimated_cost = self.provider.estimate_cost(token_usage)
564
+
562
565
  # Calculate and log cost
563
566
  agent_logger.calculate_cost(
564
567
  self.logger_id,
565
- token_usage["prompt_tokens"],
566
- token_usage["completion_tokens"],
567
- getattr(self.provider, "model", ""),
568
+ estimated_cost.total_cost,
568
569
  )
569
570
 
570
571
  # Save session update
@@ -726,12 +727,13 @@ class BaseAgent:
726
727
  token_usage["total_tokens"],
727
728
  )
728
729
 
730
+ # estimate the cost...
731
+ estimated_cost = self.provider.estimate_cost(token_usage)
732
+
729
733
  # Calculate and log cost
730
734
  agent_logger.calculate_cost(
731
735
  self.logger_id,
732
- token_usage["prompt_tokens"],
733
- token_usage["completion_tokens"],
734
- getattr(self.provider, "model", ""),
736
+ estimated_cost.total_cost,
735
737
  )
736
738
 
737
739
  # Save session update
@@ -99,27 +99,9 @@ class AgentLogger:
99
99
  def calculate_cost(
100
100
  self,
101
101
  logger_id: str,
102
- prompt_tokens: int,
103
- completion_tokens: int,
104
- model_name: str = "",
105
- pricing_data: Optional[Dict[str, float]] = None,
102
+ total_cost: float,
106
103
  ):
107
104
  """Calculate and log cost based on token usage."""
108
- # Use provided pricing data or defaults
109
- if pricing_data is None:
110
- # Default pricing (these would come from a model registry in practice)
111
- pricing_data = {
112
- "input_cost_per_million": 0.5, # $0.50 per million input tokens
113
- "output_cost_per_million": 1.5, # $1.50 per million output tokens
114
- }
115
-
116
- input_cost = (prompt_tokens / 1_000_000) * pricing_data.get(
117
- "input_cost_per_million", 0.5
118
- )
119
- output_cost = (completion_tokens / 1_000_000) * pricing_data.get(
120
- "output_cost_per_million", 1.5
121
- )
122
- total_cost = input_cost + output_cost
123
105
 
124
106
  self.log_cost(logger_id, total_cost)
125
107
  return total_cost
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kader"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  description = "kader coding agent"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -14,6 +14,7 @@ dependencies = [
14
14
  "wcmatch>=10.1",
15
15
  "typing-extensions>=4.15.0",
16
16
  "jinja2>=3.1.6",
17
+ "outdated>=0.2.2",
17
18
  ]
18
19
 
19
20
  [project.scripts]
@@ -183,107 +183,10 @@ class TestAgentLogger:
183
183
  self.agent_logger.setup_logger("test_agent", "test_session")
184
184
 
185
185
  # Test with default pricing
186
- cost = self.agent_logger.calculate_cost(
187
- logger_id,
188
- prompt_tokens=1_000_000, # 1 million tokens
189
- completion_tokens=500_000, # 0.5 million tokens
190
- model_name="",
191
- pricing_data=None,
192
- )
186
+ cost = self.agent_logger.calculate_cost(logger_id, total_cost=0.5)
193
187
 
194
188
  # Default pricing: input_cost_per_million = 0.5, output_cost_per_million = 1.5
195
- expected_cost = (1.0 * 0.5) + (0.5 * 1.5) # 0.5 + 0.75 = 1.25
196
- assert cost == expected_cost
197
-
198
- def test_calculate_cost_with_custom_pricing(self):
199
- """Test cost calculation with custom pricing data."""
200
- logger_id = "test_agent_test_session"
201
-
202
- # Setup logger first
203
- with patch("kader.agent.logger.Path") as mock_path:
204
- mock_logs_dir = MagicMock()
205
- mock_path.return_value = mock_logs_dir
206
- mock_logs_dir.__truediv__.return_value = mock_logs_dir
207
- mock_logs_dir.mkdir.return_value = None
208
- mock_log_file_path = MagicMock()
209
- mock_logs_dir.__truediv__.return_value = mock_log_file_path
210
-
211
- with patch("kader.agent.logger.logger") as mock_logger:
212
- mock_bound_logger = MagicMock()
213
- mock_logger.bind.return_value = mock_bound_logger
214
- mock_bound_logger.add.return_value = 1 # handler ID
215
-
216
- # Setup the logger
217
- self.agent_logger.setup_logger("test_agent", "test_session")
218
-
219
- # Test with custom pricing
220
- custom_pricing = {
221
- "input_cost_per_million": 0.1, # $0.10 per million input tokens
222
- "output_cost_per_million": 0.2, # $0.20 per million output tokens
223
- }
224
- cost = self.agent_logger.calculate_cost(
225
- logger_id,
226
- prompt_tokens=1_000_000,
227
- completion_tokens=1_000_000,
228
- model_name="",
229
- pricing_data=custom_pricing,
230
- )
231
-
232
- expected_cost = (1.0 * 0.1) + (1.0 * 0.2) # 0.1 + 0.2 = 0.3
233
- assert cost == expected_cost
234
-
235
- def test_calculate_cost_edge_cases(self):
236
- """Test cost calculation with edge cases."""
237
- logger_id = "test_agent_test_session"
238
-
239
- # Setup logger first
240
- with patch("kader.agent.logger.Path") as mock_path:
241
- mock_logs_dir = MagicMock()
242
- mock_path.return_value = mock_logs_dir
243
- mock_logs_dir.__truediv__.return_value = mock_logs_dir
244
- mock_logs_dir.mkdir.return_value = None
245
- mock_log_file_path = MagicMock()
246
- mock_logs_dir.__truediv__.return_value = mock_log_file_path
247
-
248
- with patch("kader.agent.logger.logger") as mock_logger:
249
- mock_bound_logger = MagicMock()
250
- mock_logger.bind.return_value = mock_bound_logger
251
- mock_bound_logger.add.return_value = 1 # handler ID
252
-
253
- # Setup the logger
254
- self.agent_logger.setup_logger("test_agent", "test_session")
255
-
256
- # Test with zero tokens
257
- cost = self.agent_logger.calculate_cost(
258
- logger_id,
259
- prompt_tokens=0,
260
- completion_tokens=0,
261
- model_name="",
262
- pricing_data=None,
263
- )
264
- assert cost == 0.0
265
-
266
- # Test with very large token counts
267
- cost = self.agent_logger.calculate_cost(
268
- logger_id,
269
- prompt_tokens=1_000_000_000, # 1B tokens
270
- completion_tokens=500_000_000, # 500M tokens
271
- model_name="",
272
- pricing_data=None,
273
- )
274
- expected_cost = (1000.0 * 0.5) + (500.0 * 1.5) # 500 + 750 = 1250.0
275
- assert cost == expected_cost
276
-
277
- # Test with negative tokens (should be treated as negative cost)
278
- cost = self.agent_logger.calculate_cost(
279
- logger_id,
280
- prompt_tokens=-100_000,
281
- completion_tokens=-50_000,
282
- model_name="",
283
- pricing_data=None,
284
- )
285
- # For negative values, the calculation would give negative cost
286
- expected_cost = (-0.1 * 0.5) + (-0.05 * 1.5) # -0.05 + -0.075 = -0.125
189
+ expected_cost = 0.5
287
190
  assert cost == expected_cost
288
191
 
289
192
  def test_log_llm_response(self):
@@ -194,6 +194,79 @@ wheels = [
194
194
  { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
195
195
  ]
196
196
 
197
+ [[package]]
198
+ name = "charset-normalizer"
199
+ version = "3.4.4"
200
+ source = { registry = "https://pypi.org/simple" }
201
+ sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
202
+ wheels = [
203
+ { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
204
+ { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
205
+ { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
206
+ { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
207
+ { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
208
+ { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
209
+ { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
210
+ { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
211
+ { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
212
+ { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
213
+ { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
214
+ { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
215
+ { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
216
+ { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
217
+ { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
218
+ { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
219
+ { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
220
+ { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
221
+ { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
222
+ { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
223
+ { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
224
+ { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
225
+ { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
226
+ { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
227
+ { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
228
+ { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
229
+ { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
230
+ { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
231
+ { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
232
+ { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
233
+ { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
234
+ { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
235
+ { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
236
+ { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
237
+ { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
238
+ { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
239
+ { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
240
+ { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
241
+ { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
242
+ { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
243
+ { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
244
+ { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
245
+ { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
246
+ { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
247
+ { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
248
+ { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
249
+ { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
250
+ { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
251
+ { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
252
+ { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
253
+ { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
254
+ { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
255
+ { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
256
+ { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
257
+ { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
258
+ { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
259
+ { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
260
+ { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
261
+ { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
262
+ { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
263
+ { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
264
+ { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
265
+ { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
266
+ { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
267
+ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
268
+ ]
269
+
197
270
  [[package]]
198
271
  name = "click"
199
272
  version = "8.3.1"
@@ -414,13 +487,14 @@ wheels = [
414
487
 
415
488
  [[package]]
416
489
  name = "kader"
417
- version = "0.1.1"
490
+ version = "0.1.3"
418
491
  source = { editable = "." }
419
492
  dependencies = [
420
493
  { name = "faiss-cpu" },
421
494
  { name = "jinja2" },
422
495
  { name = "loguru" },
423
496
  { name = "ollama" },
497
+ { name = "outdated" },
424
498
  { name = "pyyaml" },
425
499
  { name = "tenacity" },
426
500
  { name = "textual", extra = ["syntax"] },
@@ -442,6 +516,7 @@ requires-dist = [
442
516
  { name = "jinja2", specifier = ">=3.1.6" },
443
517
  { name = "loguru", specifier = ">=0.7.3" },
444
518
  { name = "ollama", specifier = ">=0.6.1" },
519
+ { name = "outdated", specifier = ">=0.2.2" },
445
520
  { name = "pyyaml", specifier = ">=6.0.3" },
446
521
  { name = "tenacity", specifier = ">=9.1.2" },
447
522
  { name = "textual", extras = ["syntax"], specifier = ">=6.8.0" },
@@ -469,6 +544,15 @@ wheels = [
469
544
  { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" },
470
545
  ]
471
546
 
547
+ [[package]]
548
+ name = "littleutils"
549
+ version = "0.2.4"
550
+ source = { registry = "https://pypi.org/simple" }
551
+ sdist = { url = "https://files.pythonhosted.org/packages/01/f0/ddc72338ed9365ced4b109d8b6233b6689d28f41f1e2a4d97fd8c1eb3da9/littleutils-0.2.4.tar.gz", hash = "sha256:c7835b01020ced42e291118b7d78fb16bc2d9a1b4f3f42f3cb3787bb4da53d19", size = 9526, upload-time = "2024-07-07T22:04:35.844Z" }
552
+ wheels = [
553
+ { url = "https://files.pythonhosted.org/packages/19/ac/a89d28d7421fffc028d68cdfde5e3e056e690cb4b1bbef4a5fea661e16f5/littleutils-0.2.4-py3-none-any.whl", hash = "sha256:d10d5fe2e107c49fe2fc2904a08d6e5a302b41f8405921835ffcc323782d5dbc", size = 8145, upload-time = "2024-07-07T22:04:34.24Z" },
554
+ ]
555
+
472
556
  [[package]]
473
557
  name = "loguru"
474
558
  version = "0.7.3"
@@ -856,6 +940,20 @@ wheels = [
856
940
  { url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354, upload-time = "2025-11-13T23:02:16.292Z" },
857
941
  ]
858
942
 
943
+ [[package]]
944
+ name = "outdated"
945
+ version = "0.2.2"
946
+ source = { registry = "https://pypi.org/simple" }
947
+ dependencies = [
948
+ { name = "littleutils" },
949
+ { name = "requests" },
950
+ { name = "setuptools" },
951
+ ]
952
+ sdist = { url = "https://files.pythonhosted.org/packages/e9/43/ac45b6c53fc62c99f02dbc310939d8693aa76cdf9900afe74a60febc8266/outdated-0.2.2.tar.gz", hash = "sha256:4b7fdec88e36711120d096d485fc4d5035e4e5ffbd907cf3a6ce2af43058b970", size = 9325, upload-time = "2022-10-29T10:14:01.847Z" }
953
+ wheels = [
954
+ { url = "https://files.pythonhosted.org/packages/d3/04/7d2b9a0d1b81e30f39e6f358bac01f4f18b585f35b0ffc5c83fc274f146b/outdated-0.2.2-py2.py3-none-any.whl", hash = "sha256:3e9c2ee6d17e86ae8cc7bb71d70c4172690121cda367155a30994742172678c8", size = 7543, upload-time = "2022-10-29T10:13:59.919Z" },
955
+ ]
956
+
859
957
  [[package]]
860
958
  name = "packaging"
861
959
  version = "25.0"
@@ -1187,6 +1285,21 @@ wheels = [
1187
1285
  { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
1188
1286
  ]
1189
1287
 
1288
+ [[package]]
1289
+ name = "requests"
1290
+ version = "2.32.5"
1291
+ source = { registry = "https://pypi.org/simple" }
1292
+ dependencies = [
1293
+ { name = "certifi" },
1294
+ { name = "charset-normalizer" },
1295
+ { name = "idna" },
1296
+ { name = "urllib3" },
1297
+ ]
1298
+ sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
1299
+ wheels = [
1300
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
1301
+ ]
1302
+
1190
1303
  [[package]]
1191
1304
  name = "rich"
1192
1305
  version = "14.2.0"
@@ -1226,6 +1339,15 @@ wheels = [
1226
1339
  { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" },
1227
1340
  ]
1228
1341
 
1342
+ [[package]]
1343
+ name = "setuptools"
1344
+ version = "80.9.0"
1345
+ source = { registry = "https://pypi.org/simple" }
1346
+ sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
1347
+ wheels = [
1348
+ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
1349
+ ]
1350
+
1229
1351
  [[package]]
1230
1352
  name = "tenacity"
1231
1353
  version = "9.1.2"
@@ -1605,6 +1727,15 @@ wheels = [
1605
1727
  { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" },
1606
1728
  ]
1607
1729
 
1730
+ [[package]]
1731
+ name = "urllib3"
1732
+ version = "2.6.3"
1733
+ source = { registry = "https://pypi.org/simple" }
1734
+ sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
1735
+ wheels = [
1736
+ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
1737
+ ]
1738
+
1608
1739
  [[package]]
1609
1740
  name = "wcmatch"
1610
1741
  version = "10.1"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes