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.
- janito/__init__.py +1 -1
- janito/__main__.py +3 -147
- janito/callbacks.py +13 -109
- janito/cli/__init__.py +6 -0
- janito/cli/agent.py +287 -0
- janito/cli/app.py +86 -0
- janito/cli/commands.py +329 -0
- janito/cli/output.py +29 -0
- janito/cli/utils.py +22 -0
- janito/config.py +338 -63
- janito/data/instructions_template.txt +27 -0
- janito/token_report.py +124 -43
- janito/tools/__init__.py +29 -1
- janito/tools/bash/bash.py +82 -0
- janito/tools/bash/unix_persistent_bash.py +182 -0
- janito/tools/bash/win_persistent_bash.py +306 -0
- janito/tools/decorators.py +90 -84
- janito/tools/delete_file.py +65 -44
- janito/tools/fetch_webpage/__init__.py +34 -0
- janito/tools/fetch_webpage/chunking.py +76 -0
- janito/tools/fetch_webpage/core.py +155 -0
- janito/tools/fetch_webpage/extractors.py +276 -0
- janito/tools/fetch_webpage/news.py +137 -0
- janito/tools/fetch_webpage/utils.py +108 -0
- janito/tools/find_files.py +108 -42
- janito/tools/move_file.py +72 -0
- janito/tools/prompt_user.py +57 -0
- janito/tools/replace_file.py +63 -0
- janito/tools/rich_console.py +139 -0
- janito/tools/search_text.py +33 -21
- janito/tools/str_replace_editor/editor.py +55 -43
- janito/tools/str_replace_editor/handlers/__init__.py +16 -0
- janito/tools/str_replace_editor/handlers/create.py +60 -0
- janito/tools/str_replace_editor/handlers/insert.py +100 -0
- janito/tools/str_replace_editor/handlers/str_replace.py +92 -0
- janito/tools/str_replace_editor/handlers/undo.py +64 -0
- janito/tools/str_replace_editor/handlers/view.py +153 -0
- janito/tools/str_replace_editor/utils.py +7 -62
- janito/tools/usage_tracker.py +136 -0
- janito-0.12.0.dist-info/METADATA +203 -0
- janito-0.12.0.dist-info/RECORD +47 -0
- janito/cli.py +0 -202
- janito/data/instructions.txt +0 -4
- janito/tools/str_replace_editor/handlers.py +0 -338
- janito-0.10.1.dist-info/METADATA +0 -86
- janito-0.10.1.dist-info/RECORD +0 -23
- {janito-0.10.1.dist-info → janito-0.12.0.dist-info}/WHEEL +0 -0
- {janito-0.10.1.dist-info → janito-0.12.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
7
|
-
from
|
8
|
-
import typer
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
"""
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
""
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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.
|
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
|
28
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
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
|
43
|
-
console.print(f"
|
44
|
-
console.print(f"
|
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("
|
49
|
-
console.print(f"
|
50
|
-
console.print(f"
|
51
|
-
console.print(f"
|
52
|
-
console.print(f"
|
53
|
-
|
54
|
-
console.print(f"
|
55
|
-
|
56
|
-
console.print(
|
57
|
-
console.print(f"
|
58
|
-
|
59
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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 +
|
72
|
-
|
73
|
-
|
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")
|