janito 0.10.1__py3-none-any.whl → 0.12.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.
Files changed (49) hide show
  1. janito/__init__.py +1 -1
  2. janito/__main__.py +3 -147
  3. janito/callbacks.py +13 -109
  4. janito/cli/__init__.py +6 -0
  5. janito/cli/agent.py +287 -0
  6. janito/cli/app.py +86 -0
  7. janito/cli/commands.py +329 -0
  8. janito/cli/output.py +29 -0
  9. janito/cli/utils.py +22 -0
  10. janito/config.py +338 -63
  11. janito/data/instructions_template.txt +27 -0
  12. janito/token_report.py +124 -43
  13. janito/tools/__init__.py +29 -1
  14. janito/tools/bash/bash.py +82 -0
  15. janito/tools/bash/unix_persistent_bash.py +182 -0
  16. janito/tools/bash/win_persistent_bash.py +306 -0
  17. janito/tools/decorators.py +90 -84
  18. janito/tools/delete_file.py +65 -44
  19. janito/tools/fetch_webpage/__init__.py +34 -0
  20. janito/tools/fetch_webpage/chunking.py +76 -0
  21. janito/tools/fetch_webpage/core.py +155 -0
  22. janito/tools/fetch_webpage/extractors.py +276 -0
  23. janito/tools/fetch_webpage/news.py +137 -0
  24. janito/tools/fetch_webpage/utils.py +108 -0
  25. janito/tools/find_files.py +108 -42
  26. janito/tools/move_file.py +72 -0
  27. janito/tools/prompt_user.py +57 -0
  28. janito/tools/replace_file.py +63 -0
  29. janito/tools/rich_console.py +139 -0
  30. janito/tools/search_text.py +33 -21
  31. janito/tools/str_replace_editor/editor.py +55 -43
  32. janito/tools/str_replace_editor/handlers/__init__.py +16 -0
  33. janito/tools/str_replace_editor/handlers/create.py +60 -0
  34. janito/tools/str_replace_editor/handlers/insert.py +100 -0
  35. janito/tools/str_replace_editor/handlers/str_replace.py +92 -0
  36. janito/tools/str_replace_editor/handlers/undo.py +64 -0
  37. janito/tools/str_replace_editor/handlers/view.py +153 -0
  38. janito/tools/str_replace_editor/utils.py +7 -62
  39. janito/tools/usage_tracker.py +136 -0
  40. janito-0.12.0.dist-info/METADATA +203 -0
  41. janito-0.12.0.dist-info/RECORD +47 -0
  42. janito/cli.py +0 -202
  43. janito/data/instructions.txt +0 -4
  44. janito/tools/str_replace_editor/handlers.py +0 -338
  45. janito-0.10.1.dist-info/METADATA +0 -86
  46. janito-0.10.1.dist-info/RECORD +0 -23
  47. {janito-0.10.1.dist-info → janito-0.12.0.dist-info}/WHEEL +0 -0
  48. {janito-0.10.1.dist-info → janito-0.12.0.dist-info}/entry_points.txt +0 -0
  49. {janito-0.10.1.dist-info → janito-0.12.0.dist-info}/licenses/LICENSE +0 -0
janito/config.py CHANGED
@@ -1,63 +1,338 @@
1
- """
2
- Configuration module for Janito.
3
- Provides a singleton Config class to access configuration values.
4
- """
5
- import os
6
- from pathlib import Path
7
- from typing import Optional
8
- import typer
9
-
10
- class Config:
11
- """Singleton configuration class for Janito."""
12
- _instance = None
13
-
14
- def __new__(cls):
15
- if cls._instance is None:
16
- cls._instance = super(Config, cls).__new__(cls)
17
- cls._instance._workspace_dir = os.getcwd()
18
- cls._instance._debug_mode = False
19
- return cls._instance
20
-
21
- @property
22
- def workspace_dir(self) -> str:
23
- """Get the current workspace directory."""
24
- return self._workspace_dir
25
-
26
- @workspace_dir.setter
27
- def workspace_dir(self, path: str) -> None:
28
- """Set the workspace directory."""
29
- # Convert to absolute path if not already
30
- if not os.path.isabs(path):
31
- path = os.path.normpath(os.path.abspath(path))
32
- else:
33
- # Ensure Windows paths are properly formatted
34
- path = os.path.normpath(path)
35
-
36
- # Check if the directory exists
37
- if not os.path.isdir(path):
38
- create_dir = typer.confirm(f"Workspace directory does not exist: {path}\nDo you want to create it?")
39
- if create_dir:
40
- try:
41
- os.makedirs(path, exist_ok=True)
42
- print(f"Created workspace directory: {path}")
43
- except Exception as e:
44
- raise ValueError(f"Failed to create workspace directory: {str(e)}")
45
- else:
46
- raise ValueError(f"Workspace directory does not exist: {path}")
47
-
48
- self._workspace_dir = path
49
-
50
- @property
51
- def debug_mode(self) -> bool:
52
- """Get the debug mode status."""
53
- return self._debug_mode
54
-
55
- @debug_mode.setter
56
- def debug_mode(self, value: bool) -> None:
57
- """Set the debug mode status."""
58
- self._debug_mode = value
59
-
60
- # Convenience function to get the config instance
61
- def get_config() -> Config:
62
- """Get the singleton Config instance."""
63
- return Config()
1
+ """
2
+ Configuration module for Janito.
3
+ Provides a singleton Config class to access configuration values.
4
+ """
5
+ import os
6
+ import json
7
+ from pathlib import Path
8
+ import typer
9
+ from typing import Dict, Any, Optional
10
+
11
+ # Predefined parameter profiles
12
+ PROFILES = {
13
+ "precise": {
14
+ "temperature": 0.2,
15
+ "top_p": 0.85,
16
+ "top_k": 20,
17
+ "description": "Factual answers, documentation, structured data, avoiding hallucinations"
18
+ },
19
+ "balanced": {
20
+ "temperature": 0.5,
21
+ "top_p": 0.9,
22
+ "top_k": 40,
23
+ "description": "Professional writing, summarization, everyday tasks with moderate creativity"
24
+ },
25
+ "conversational": {
26
+ "temperature": 0.7,
27
+ "top_p": 0.9,
28
+ "top_k": 45,
29
+ "description": "Natural dialogue, educational content, support conversations"
30
+ },
31
+ "creative": {
32
+ "temperature": 0.9,
33
+ "top_p": 0.95,
34
+ "top_k": 70,
35
+ "description": "Storytelling, brainstorming, marketing copy, poetry"
36
+ },
37
+ "technical": {
38
+ "temperature": 0.3,
39
+ "top_p": 0.95,
40
+ "top_k": 15,
41
+ "description": "Code generation, debugging, decision analysis, technical problem-solving"
42
+ }
43
+ }
44
+
45
+ class Config:
46
+ """Singleton configuration class for Janito."""
47
+ _instance = None
48
+
49
+ def __new__(cls):
50
+ if cls._instance is None:
51
+ cls._instance = super(Config, cls).__new__(cls)
52
+ cls._instance._workspace_dir = os.getcwd()
53
+ cls._instance._verbose = False
54
+ # Chat history context feature has been removed
55
+ cls._instance._ask_mode = False
56
+ # Set technical profile as default
57
+ profile_data = PROFILES["technical"]
58
+ cls._instance._temperature = profile_data["temperature"]
59
+ cls._instance._profile = "technical"
60
+ cls._instance._role = "software engineer"
61
+ cls._instance._gitbash_path = None # Default to None for auto-detection
62
+ cls._instance._load_config()
63
+ return cls._instance
64
+
65
+ def _load_config(self) -> None:
66
+ """Load configuration from file."""
67
+ config_path = Path(self._workspace_dir) / ".janito" / "config.json"
68
+ if config_path.exists():
69
+ try:
70
+ with open(config_path, "r", encoding="utf-8") as f:
71
+ config_data = json.load(f)
72
+ # Chat history context feature has been removed
73
+ if "debug_mode" in config_data:
74
+ self._verbose = config_data["debug_mode"]
75
+ if "ask_mode" in config_data:
76
+ self._ask_mode = config_data["ask_mode"]
77
+ if "temperature" in config_data:
78
+ self._temperature = config_data["temperature"]
79
+ if "profile" in config_data:
80
+ self._profile = config_data["profile"]
81
+ if "role" in config_data:
82
+ self._role = config_data["role"]
83
+ if "gitbash_path" in config_data:
84
+ self._gitbash_path = config_data["gitbash_path"]
85
+ except Exception as e:
86
+ print(f"Warning: Failed to load configuration: {str(e)}")
87
+
88
+ def _save_config(self) -> None:
89
+ """Save configuration to file."""
90
+ config_dir = Path(self._workspace_dir) / ".janito"
91
+ config_dir.mkdir(parents=True, exist_ok=True)
92
+ config_path = config_dir / "config.json"
93
+
94
+ config_data = {
95
+ # Chat history context feature has been removed
96
+ "verbose": self._verbose,
97
+ "ask_mode": self._ask_mode,
98
+ "temperature": self._temperature,
99
+ "role": self._role
100
+ }
101
+
102
+ # Save profile name if one is set
103
+ if self._profile:
104
+ config_data["profile"] = self._profile
105
+
106
+ # Save GitBash path if one is set
107
+ if self._gitbash_path:
108
+ config_data["gitbash_path"] = self._gitbash_path
109
+
110
+ try:
111
+ with open(config_path, "w", encoding="utf-8") as f:
112
+ json.dump(config_data, f, indent=2)
113
+ except Exception as e:
114
+ print(f"Warning: Failed to save configuration: {str(e)}")
115
+
116
+ def set_profile(self, profile_name: str) -> None:
117
+ """Set parameter values based on a predefined profile.
118
+
119
+ Args:
120
+ profile_name: Name of the profile to use (precise, balanced, conversational, creative, technical)
121
+
122
+ Raises:
123
+ ValueError: If the profile name is not recognized
124
+ """
125
+ profile_name = profile_name.lower()
126
+ if profile_name not in PROFILES:
127
+ valid_profiles = ", ".join(PROFILES.keys())
128
+ raise ValueError(f"Unknown profile: {profile_name}. Valid profiles are: {valid_profiles}")
129
+
130
+ profile = PROFILES[profile_name]
131
+ self._temperature = profile["temperature"]
132
+ self._profile = profile_name
133
+ self._save_config()
134
+
135
+ @property
136
+ def profile(self) -> Optional[str]:
137
+ """Get the current profile name."""
138
+ return self._profile
139
+
140
+ @staticmethod
141
+ def get_available_profiles() -> Dict[str, Dict[str, Any]]:
142
+ """Get all available predefined profiles."""
143
+ return PROFILES
144
+
145
+ @staticmethod
146
+ def set_api_key(api_key: str) -> None:
147
+ """Set the API key in the global configuration file.
148
+
149
+ Args:
150
+ api_key: The Anthropic API key to store
151
+
152
+ Returns:
153
+ None
154
+ """
155
+ # Create .janito directory in user's home directory if it doesn't exist
156
+ home_dir = Path.home()
157
+ config_dir = home_dir / ".janito"
158
+ config_dir.mkdir(parents=True, exist_ok=True)
159
+
160
+ # Create or update the config.json file
161
+ config_path = config_dir / "config.json"
162
+
163
+ # Load existing config if it exists
164
+ config_data = {}
165
+ if config_path.exists():
166
+ try:
167
+ with open(config_path, "r", encoding="utf-8") as f:
168
+ config_data = json.load(f)
169
+ except Exception as e:
170
+ print(f"Warning: Failed to load global configuration: {str(e)}")
171
+
172
+ # Update the API key
173
+ config_data["api_key"] = api_key
174
+
175
+ # Save the updated config
176
+ try:
177
+ with open(config_path, "w", encoding="utf-8") as f:
178
+ json.dump(config_data, f, indent=2)
179
+ print(f"API key saved to {config_path}")
180
+ except Exception as e:
181
+ raise ValueError(f"Failed to save API key: {str(e)}")
182
+
183
+ @staticmethod
184
+ def get_api_key() -> Optional[str]:
185
+ """Get the API key from the global configuration file.
186
+
187
+ Returns:
188
+ The API key if found, None otherwise
189
+ """
190
+ # Look for config.json in user's home directory
191
+ home_dir = Path.home()
192
+ config_path = home_dir / ".janito" / "config.json"
193
+
194
+ if config_path.exists():
195
+ try:
196
+ with open(config_path, "r", encoding="utf-8") as f:
197
+ config_data = json.load(f)
198
+ return config_data.get("api_key")
199
+ except Exception:
200
+ # Silently fail and return None
201
+ pass
202
+
203
+ return None
204
+
205
+ @property
206
+ def workspace_dir(self) -> str:
207
+ """Get the current workspace directory."""
208
+ return self._workspace_dir
209
+
210
+ @workspace_dir.setter
211
+ def workspace_dir(self, path: str) -> None:
212
+ """Set the workspace directory."""
213
+ # Convert to absolute path if not already
214
+ if not os.path.isabs(path):
215
+ path = os.path.normpath(os.path.abspath(path))
216
+ else:
217
+ # Ensure Windows paths are properly formatted
218
+ path = os.path.normpath(path)
219
+
220
+ # Check if the directory exists
221
+ if not os.path.isdir(path):
222
+ create_dir = typer.confirm(f"Workspace directory does not exist: {path}\nDo you want to create it?")
223
+ if create_dir:
224
+ try:
225
+ os.makedirs(path, exist_ok=True)
226
+ print(f"Created workspace directory: {path}")
227
+ except Exception as e:
228
+ raise ValueError(f"Failed to create workspace directory: {str(e)}") from e
229
+ else:
230
+ raise ValueError(f"Workspace directory does not exist: {path}")
231
+
232
+ self._workspace_dir = path
233
+
234
+ @property
235
+ def verbose(self) -> bool:
236
+ """Get the verbose mode status."""
237
+ return self._verbose
238
+
239
+ @verbose.setter
240
+ def verbose(self, value: bool) -> None:
241
+ """Set the verbose mode status."""
242
+ self._verbose = value
243
+
244
+ # For backward compatibility
245
+ @property
246
+ def debug_mode(self) -> bool:
247
+ """Get the debug mode status (alias for verbose)."""
248
+ return self._verbose
249
+
250
+ @debug_mode.setter
251
+ def debug_mode(self, value: bool) -> None:
252
+ """Set the debug mode status (alias for verbose)."""
253
+ self._verbose = value
254
+
255
+ # Chat history context feature has been removed
256
+
257
+ @property
258
+ def ask_mode(self) -> bool:
259
+ """Get the ask mode status."""
260
+ return self._ask_mode
261
+
262
+ @ask_mode.setter
263
+ def ask_mode(self, value: bool) -> None:
264
+ """Set the ask mode status."""
265
+ self._ask_mode = value
266
+ self._save_config()
267
+
268
+ @property
269
+ def temperature(self) -> float:
270
+ """Get the temperature value for model generation."""
271
+ return self._temperature
272
+
273
+ @temperature.setter
274
+ def temperature(self, value: float) -> None:
275
+ """Set the temperature value for model generation."""
276
+ if value < 0.0 or value > 1.0:
277
+ raise ValueError("Temperature must be between 0.0 and 1.0")
278
+ self._temperature = value
279
+ self._save_config()
280
+
281
+ # top_k and top_p are now only accessible through profiles
282
+
283
+ @property
284
+ def role(self) -> str:
285
+ """Get the role for the assistant."""
286
+ return self._role
287
+
288
+ @role.setter
289
+ def role(self, value: str) -> None:
290
+ """Set the role for the assistant."""
291
+ self._role = value
292
+ self._save_config()
293
+
294
+ @property
295
+ def gitbash_path(self) -> Optional[str]:
296
+ """Get the path to the GitBash executable."""
297
+ return self._gitbash_path
298
+
299
+ @gitbash_path.setter
300
+ def gitbash_path(self, value: Optional[str]) -> None:
301
+ """Set the path to the GitBash executable.
302
+
303
+ Args:
304
+ value: Path to the GitBash executable, or None to use auto-detection
305
+ """
306
+ # If a path is provided, verify it exists
307
+ if value is not None and not os.path.exists(value):
308
+ raise ValueError(f"GitBash executable not found at: {value}")
309
+
310
+ self._gitbash_path = value
311
+ self._save_config()
312
+
313
+ def reset_config(self) -> bool:
314
+ """Reset configuration by removing the config file.
315
+
316
+ Returns:
317
+ bool: True if the config file was removed, False if it didn't exist
318
+ """
319
+ config_path = Path(self._workspace_dir) / ".janito" / "config.json"
320
+ if config_path.exists():
321
+ config_path.unlink()
322
+ # Reset instance variables to defaults
323
+ self._verbose = False
324
+ # Chat history context feature has been removed
325
+ self._ask_mode = False
326
+ # Set technical profile as default
327
+ profile_data = PROFILES["technical"]
328
+ self._temperature = profile_data["temperature"]
329
+ self._profile = "technical"
330
+ self._role = "software engineer"
331
+ self._gitbash_path = None # Reset to auto-detection
332
+ return True
333
+ return False
334
+
335
+ # Convenience function to get the config instance
336
+ def get_config() -> Config:
337
+ """Get the singleton Config instance."""
338
+ return Config()
@@ -0,0 +1,27 @@
1
+ You are a {{ role }}, using the name Janito .
2
+ You will be assisting an user using a computer system on a {{ platform }} platform.
3
+ You can find more about the current project using the tools in the workspace directory.
4
+ If the question is related to the project, use the tools using the relative path "." .
5
+
6
+ If creating or editing files with a large number of lines, organize them into smaller files.
7
+ If creating or editing files in an existing directory check surrounding files for the used patterns.
8
+
9
+ # Structure Discovery (.janito/docs/STRUCTURE.md)
10
+ Always start exploring the project by viewing for the file .janito/docs/STRUCTURE.md.
11
+ Do not track files or directories wich are in .gitignore in the structure.
12
+ At the end of responding to the user, update the structure file based on the files and directories you have interacted with,
13
+ be precise focusing on the most important files and directories, avoid adding extra information like architecture or design patterns.
14
+
15
+
16
+ # Tools
17
+ The bash tool does not support commands which will require user input.
18
+ Prefer the str_replace_editor tool to view directories and file contents.
19
+
20
+ </IMPORTANT>
21
+ Call the tool user_prompt when:
22
+ - There are multiple options to apply a certain change
23
+ - The next operation risk is moderated or high
24
+ - The implementation plan is complex, requiring a review
25
+ Proceed according to the user answer.
26
+ <IMPORTANT/>
27
+
janito/token_report.py CHANGED
@@ -3,71 +3,152 @@ Module for generating token usage reports.
3
3
  """
4
4
 
5
5
  from rich.console import Console
6
- from claudine.token_tracking import MODEL_PRICING, DEFAULT_MODEL
7
6
 
8
- def generate_token_report(agent, verbose=False):
7
+ def generate_token_report(agent, verbose=False, interrupted=False):
9
8
  """
10
9
  Generate a token usage report.
11
10
 
12
11
  Args:
13
12
  agent: The Claude agent instance
14
13
  verbose: Whether to show detailed token usage information
14
+ interrupted: Whether the request was interrupted
15
15
 
16
16
  Returns:
17
17
  None - prints the report to the console
18
18
  """
19
19
  console = Console()
20
- usage = agent.get_token_usage()
20
+ usage = agent.get_tokens()
21
+ cost = agent.get_token_cost()
22
+
21
23
  text_usage = usage.text_usage
22
24
  tools_usage = usage.tools_usage
23
25
 
24
26
  if verbose:
25
27
  total_usage = usage.total_usage
26
28
 
27
- # Get the pricing model
28
- pricing = MODEL_PRICING.get(DEFAULT_MODEL)
29
+ # Get costs from the cost object
30
+ text_input_cost = cost.input_cost
31
+ text_output_cost = cost.output_cost
32
+ text_cache_creation_cost = cost.cache_creation_cost
33
+ text_cache_read_cost = cost.cache_read_cost
29
34
 
30
- # Calculate costs manually
31
- text_input_cost = pricing.input_tokens.calculate_cost(text_usage.input_tokens)
32
- text_output_cost = pricing.output_tokens.calculate_cost(text_usage.output_tokens)
33
- tools_input_cost = pricing.input_tokens.calculate_cost(tools_usage.input_tokens)
34
- tools_output_cost = pricing.output_tokens.calculate_cost(tools_usage.output_tokens)
35
+ tools_input_cost = cost.input_cost
36
+ tools_output_cost = cost.output_cost
37
+ tools_cache_creation_cost = cost.cache_creation_cost
38
+ tools_cache_read_cost = cost.cache_read_cost
35
39
 
36
40
  # Format costs
37
- format_cost = lambda cost: f"{cost * 100:.2f}¢" if cost < 1.0 else f"${cost:.6f}"
41
+ def format_cost(cost):
42
+ return f"{cost * 100:.2f}¢ USD" if cost < 1.0 else f"${cost:.6f} USD"
38
43
 
39
- console.print("\n[bold blue]Detailed Token Usage:[/bold blue]")
40
- console.print(f"Text Input tokens: {text_usage.input_tokens}")
41
- console.print(f"Text Output tokens: {text_usage.output_tokens}")
42
- console.print(f"Text Total tokens: {text_usage.input_tokens + text_usage.output_tokens}")
43
- console.print(f"Tool Input tokens: {tools_usage.input_tokens}")
44
- console.print(f"Tool Output tokens: {tools_usage.output_tokens}")
45
- console.print(f"Tool Total tokens: {tools_usage.input_tokens + tools_usage.output_tokens}")
46
- console.print(f"Total tokens: {total_usage.input_tokens + total_usage.output_tokens}")
44
+ console.print("\n[bold blue]📊 Detailed Token Usage:[/bold blue]")
45
+ console.print(f"📝 Text Input tokens: {text_usage.input_tokens}")
46
+ console.print(f"📤 Text Output tokens: {text_usage.output_tokens}")
47
+ console.print(f"💾 Text Cache Creation tokens: {text_usage.cache_creation_input_tokens}")
48
+ console.print(f"📖 Text Cache Read tokens: {text_usage.cache_read_input_tokens}")
49
+ console.print(f"📋 Text Total tokens: {text_usage.input_tokens + text_usage.output_tokens + text_usage.cache_creation_input_tokens + text_usage.cache_read_input_tokens}")
47
50
 
48
- console.print("\n[bold blue]Pricing Information:[/bold blue]")
49
- console.print(f"Input pricing: ${pricing.input_tokens.cost_per_million_tokens}/million tokens")
50
- console.print(f"Output pricing: ${pricing.output_tokens.cost_per_million_tokens}/million tokens")
51
- console.print(f"Text Input cost: {format_cost(text_input_cost)}")
52
- console.print(f"Text Output cost: {format_cost(text_output_cost)}")
53
- console.print(f"Text Total cost: {format_cost(text_input_cost + text_output_cost)}")
54
- console.print(f"Tool Input cost: {format_cost(tools_input_cost)}")
55
- console.print(f"Tool Output cost: {format_cost(tools_output_cost)}")
56
- console.print(f"Tool Total cost: {format_cost(tools_input_cost + tools_output_cost)}")
57
- console.print(f"Total cost: {format_cost(text_input_cost + text_output_cost + tools_input_cost + tools_output_cost)}")
58
-
59
- # Display per-tool breakdown if available
51
+ console.print(f"🔧 Tool Input tokens: {tools_usage.input_tokens}")
52
+ console.print(f"🔨 Tool Output tokens: {tools_usage.output_tokens}")
53
+ console.print(f"💾 Tool Cache Creation tokens: {tools_usage.cache_creation_input_tokens}")
54
+ console.print(f"📖 Tool Cache Read tokens: {tools_usage.cache_read_input_tokens}")
55
+ console.print(f"🧰 Tool Total tokens: {tools_usage.input_tokens + tools_usage.output_tokens + tools_usage.cache_creation_input_tokens + tools_usage.cache_read_input_tokens}")
56
+
57
+ console.print(f"🔢 Total tokens: {total_usage.input_tokens + total_usage.output_tokens + total_usage.cache_creation_input_tokens + total_usage.cache_read_input_tokens}")
58
+
59
+ console.print("\n[bold blue]💰 Pricing Information:[/bold blue]")
60
+ console.print(f"📝 Text Input cost: {format_cost(text_input_cost)}")
61
+ console.print(f"📤 Text Output cost: {format_cost(text_output_cost)}")
62
+ console.print(f"💾 Text Cache Creation cost: {format_cost(text_cache_creation_cost)}")
63
+ console.print(f"📖 Text Cache Read cost: {format_cost(text_cache_read_cost)}")
64
+ console.print(f"📋 Text Total cost: {format_cost(text_input_cost + text_output_cost + text_cache_creation_cost + text_cache_read_cost)}")
65
+
66
+ console.print(f"🔧 Tool Input cost: {format_cost(tools_input_cost)}")
67
+ console.print(f"🔨 Tool Output cost: {format_cost(tools_output_cost)}")
68
+ console.print(f"💾 Tool Cache Creation cost: {format_cost(tools_cache_creation_cost)}")
69
+ console.print(f"📖 Tool Cache Read cost: {format_cost(tools_cache_read_cost)}")
70
+ console.print(f"🧰 Tool Total cost: {format_cost(tools_input_cost + tools_output_cost + tools_cache_creation_cost + tools_cache_read_cost)}")
71
+
72
+ total_cost_text = f"💵 Total cost: {format_cost(text_input_cost + text_output_cost + text_cache_creation_cost + text_cache_read_cost + tools_input_cost + tools_output_cost + tools_cache_creation_cost + tools_cache_read_cost)}"
73
+ if interrupted:
74
+ total_cost_text += " (interrupted request not accounted)"
75
+ console.print(total_cost_text)
76
+
77
+ # Show cache delta if available
78
+ if hasattr(cost, 'cache_delta') and cost.cache_delta:
79
+ cache_delta = cost.cache_delta
80
+ console.print(f"\n[bold green]💰 Cache Savings:[/bold green] {format_cost(cache_delta)}")
81
+
82
+ # Calculate percentage savings
83
+ total_cost_without_cache = cost.total_cost + cache_delta
84
+ if total_cost_without_cache > 0:
85
+ savings_percentage = (cache_delta / total_cost_without_cache) * 100
86
+ console.print(f"[bold green]📊 Cache Savings Percentage:[/bold green] {savings_percentage:.2f}%")
87
+ console.print(f"[bold green]💸 Cost without cache:[/bold green] {format_cost(total_cost_without_cache)}")
88
+ console.print(f"[bold green]💲 Cost with cache:[/bold green] {format_cost(cost.total_cost)}")
89
+
90
+ # Per-tool breakdown
60
91
  if usage.by_tool:
61
- console.print("\n[bold blue]Per-Tool Breakdown:[/bold blue]")
62
- for tool_name, tool_usage in usage.by_tool.items():
63
- tool_input_cost = pricing.input_tokens.calculate_cost(tool_usage.input_tokens)
64
- tool_output_cost = pricing.output_tokens.calculate_cost(tool_usage.output_tokens)
65
- console.print(f" Tool: {tool_name}")
66
- console.print(f" Input tokens: {tool_usage.input_tokens}")
67
- console.print(f" Output tokens: {tool_usage.output_tokens}")
68
- console.print(f" Total tokens: {tool_usage.input_tokens + tool_usage.output_tokens}")
69
- console.print(f" Total cost: {format_cost(tool_input_cost + tool_output_cost)}")
92
+ console.print("\n[bold blue]🔧 Per-Tool Breakdown:[/bold blue]")
93
+ try:
94
+ if hasattr(cost, 'by_tool') and cost.by_tool:
95
+ for tool_name, tool_usage in usage.by_tool.items():
96
+ tool_input_cost = cost.by_tool[tool_name].input_cost
97
+ tool_output_cost = cost.by_tool[tool_name].output_cost
98
+ tool_cache_creation_cost = cost.by_tool[tool_name].cache_creation_cost
99
+ tool_cache_read_cost = cost.by_tool[tool_name].cache_read_cost
100
+ tool_total_cost = tool_input_cost + tool_output_cost + tool_cache_creation_cost + tool_cache_read_cost
101
+
102
+ console.print(f" 🔧 Tool: {tool_name}")
103
+ console.print(f" 📥 Input tokens: {tool_usage.input_tokens}")
104
+ console.print(f" 📤 Output tokens: {tool_usage.output_tokens}")
105
+ console.print(f" 💾 Cache Creation tokens: {tool_usage.cache_creation_input_tokens}")
106
+ console.print(f" 📖 Cache Read tokens: {tool_usage.cache_read_input_tokens}")
107
+ console.print(f" 🔢 Total tokens: {tool_usage.input_tokens + tool_usage.output_tokens + tool_usage.cache_creation_input_tokens + tool_usage.cache_read_input_tokens}")
108
+ console.print(f" 💵 Total cost: {format_cost(tool_total_cost)}")
109
+ else:
110
+ # Calculate costs manually for each tool if cost.by_tool is not available
111
+ for tool_name, tool_usage in usage.by_tool.items():
112
+ # Estimate costs based on overall pricing
113
+ total_tokens = tool_usage.input_tokens + tool_usage.output_tokens + tool_usage.cache_creation_input_tokens + tool_usage.cache_read_input_tokens
114
+ estimated_cost = (total_tokens / (usage.total_usage.total_tokens + usage.total_usage.total_cache_tokens)) * cost.total_cost if usage.total_usage.total_tokens > 0 else 0
115
+
116
+ console.print(f" 🔧 Tool: {tool_name}")
117
+ console.print(f" 📥 Input tokens: {tool_usage.input_tokens}")
118
+ console.print(f" 📤 Output tokens: {tool_usage.output_tokens}")
119
+ console.print(f" 💾 Cache Creation tokens: {tool_usage.cache_creation_input_tokens}")
120
+ console.print(f" 📖 Cache Read tokens: {tool_usage.cache_read_input_tokens}")
121
+ console.print(f" 🔢 Total tokens: {tool_usage.input_tokens + tool_usage.output_tokens + tool_usage.cache_creation_input_tokens + tool_usage.cache_read_input_tokens}")
122
+ console.print(f" 💵 Total cost: {format_cost(estimated_cost)}")
123
+ except Exception as e:
124
+ console.print(f"❌ Error: {str(e)}")
70
125
  else:
71
- total_tokens = text_usage.input_tokens + text_usage.output_tokens + tools_usage.input_tokens + tools_usage.output_tokens
72
- cost_info = agent.get_cost()
73
- console.rule(f"[bold blue]Total tokens: {total_tokens} | Cost: {cost_info.format_total_cost()}[/bold blue]")
126
+ total_tokens = (text_usage.input_tokens + text_usage.output_tokens +
127
+ text_usage.cache_creation_input_tokens + text_usage.cache_read_input_tokens +
128
+ tools_usage.input_tokens + tools_usage.output_tokens +
129
+ tools_usage.cache_creation_input_tokens + tools_usage.cache_read_input_tokens)
130
+
131
+ # Format costs
132
+ def format_cost(cost):
133
+ return f"{cost * 100:.2f}¢ USD" if cost < 1.0 else f"${cost:.6f} USD"
134
+
135
+ # Prepare summary message
136
+ cost_text = f"Cost: {format_cost(cost.total_cost)}"
137
+ if interrupted:
138
+ cost_text += " (interrupted request not accounted)"
139
+
140
+ summary = f"Total tokens: {total_tokens} | {cost_text}"
141
+
142
+ # Add cache savings if available
143
+ if hasattr(cost, 'cache_delta') and cost.cache_delta != 0:
144
+ cache_delta = cost.cache_delta
145
+ total_cost_without_cache = cost.total_cost + cache_delta
146
+ savings_percentage = 0
147
+ if total_cost_without_cache > 0:
148
+ savings_percentage = (cache_delta / total_cost_without_cache) * 100
149
+
150
+ summary += f" | Cache savings: {format_cost(cache_delta)} ({savings_percentage:.1f}%)"
151
+
152
+ # Display with a rule
153
+ console.rule("[blue]Token Usage[/blue]")
154
+ console.print(f"[blue]{summary}[/blue]", justify="center")