claude-dev-cli 0.1.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.
Potentially problematic release.
This version of claude-dev-cli might be problematic. Click here for more details.
- claude_dev_cli/__init__.py +19 -0
- claude_dev_cli/cli.py +398 -0
- claude_dev_cli/commands.py +133 -0
- claude_dev_cli/config.py +174 -0
- claude_dev_cli/core.py +132 -0
- claude_dev_cli/templates.py +104 -0
- claude_dev_cli/usage.py +202 -0
- claude_dev_cli-0.1.0.dist-info/METADATA +392 -0
- claude_dev_cli-0.1.0.dist-info/RECORD +13 -0
- claude_dev_cli-0.1.0.dist-info/WHEEL +5 -0
- claude_dev_cli-0.1.0.dist-info/entry_points.txt +3 -0
- claude_dev_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- claude_dev_cli-0.1.0.dist-info/top_level.txt +1 -0
claude_dev_cli/config.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Configuration management for Claude Dev CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Optional, List
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class APIConfig(BaseModel):
|
|
11
|
+
"""Configuration for a Claude API key."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
api_key: str
|
|
15
|
+
description: Optional[str] = None
|
|
16
|
+
default: bool = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ProjectProfile(BaseModel):
|
|
20
|
+
"""Project-specific configuration."""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
api_config: str # Name of the API config to use
|
|
24
|
+
system_prompt: Optional[str] = None
|
|
25
|
+
allowed_commands: List[str] = Field(default_factory=lambda: ["all"])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Config:
|
|
29
|
+
"""Manages configuration for Claude Dev CLI."""
|
|
30
|
+
|
|
31
|
+
CONFIG_DIR = Path.home() / ".claude-dev-cli"
|
|
32
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
33
|
+
USAGE_LOG = CONFIG_DIR / "usage.jsonl"
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
"""Initialize configuration."""
|
|
37
|
+
self.config_dir = self.CONFIG_DIR
|
|
38
|
+
self.config_file = self.CONFIG_FILE
|
|
39
|
+
self.usage_log = self.USAGE_LOG
|
|
40
|
+
self._ensure_config_dir()
|
|
41
|
+
self._data: Dict = self._load_config()
|
|
42
|
+
|
|
43
|
+
def _ensure_config_dir(self) -> None:
|
|
44
|
+
"""Ensure configuration directory exists."""
|
|
45
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
self.usage_log.touch(exist_ok=True)
|
|
47
|
+
|
|
48
|
+
def _load_config(self) -> Dict:
|
|
49
|
+
"""Load configuration from file."""
|
|
50
|
+
if not self.config_file.exists():
|
|
51
|
+
default_config = {
|
|
52
|
+
"api_configs": [],
|
|
53
|
+
"project_profiles": [],
|
|
54
|
+
"default_model": "claude-3-5-sonnet-20241022",
|
|
55
|
+
"max_tokens": 4096,
|
|
56
|
+
}
|
|
57
|
+
self._save_config(default_config)
|
|
58
|
+
return default_config
|
|
59
|
+
|
|
60
|
+
with open(self.config_file, 'r') as f:
|
|
61
|
+
return json.load(f)
|
|
62
|
+
|
|
63
|
+
def _save_config(self, data: Optional[Dict] = None) -> None:
|
|
64
|
+
"""Save configuration to file."""
|
|
65
|
+
if data is None:
|
|
66
|
+
data = self._data
|
|
67
|
+
|
|
68
|
+
with open(self.config_file, 'w') as f:
|
|
69
|
+
json.dump(data, f, indent=2)
|
|
70
|
+
|
|
71
|
+
def add_api_config(
|
|
72
|
+
self,
|
|
73
|
+
name: str,
|
|
74
|
+
api_key: Optional[str] = None,
|
|
75
|
+
description: Optional[str] = None,
|
|
76
|
+
make_default: bool = False
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Add a new API configuration."""
|
|
79
|
+
# Get API key from env if not provided
|
|
80
|
+
if api_key is None:
|
|
81
|
+
env_var = f"{name.upper()}_ANTHROPIC_API_KEY"
|
|
82
|
+
api_key = os.environ.get(env_var)
|
|
83
|
+
if not api_key:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"API key not provided and {env_var} environment variable not set"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Check if name already exists
|
|
89
|
+
api_configs = self._data.get("api_configs", [])
|
|
90
|
+
for config in api_configs:
|
|
91
|
+
if config["name"] == name:
|
|
92
|
+
raise ValueError(f"API config with name '{name}' already exists")
|
|
93
|
+
|
|
94
|
+
# If this is the first config or make_default is True, set as default
|
|
95
|
+
if make_default or not api_configs:
|
|
96
|
+
for config in api_configs:
|
|
97
|
+
config["default"] = False
|
|
98
|
+
|
|
99
|
+
api_config = APIConfig(
|
|
100
|
+
name=name,
|
|
101
|
+
api_key=api_key,
|
|
102
|
+
description=description,
|
|
103
|
+
default=make_default or not api_configs
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
api_configs.append(api_config.model_dump())
|
|
107
|
+
self._data["api_configs"] = api_configs
|
|
108
|
+
self._save_config()
|
|
109
|
+
|
|
110
|
+
def get_api_config(self, name: Optional[str] = None) -> Optional[APIConfig]:
|
|
111
|
+
"""Get API configuration by name or default."""
|
|
112
|
+
api_configs = self._data.get("api_configs", [])
|
|
113
|
+
|
|
114
|
+
if name:
|
|
115
|
+
for config in api_configs:
|
|
116
|
+
if config["name"] == name:
|
|
117
|
+
return APIConfig(**config)
|
|
118
|
+
else:
|
|
119
|
+
# Return default
|
|
120
|
+
for config in api_configs:
|
|
121
|
+
if config.get("default", False):
|
|
122
|
+
return APIConfig(**config)
|
|
123
|
+
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
def list_api_configs(self) -> List[APIConfig]:
|
|
127
|
+
"""List all API configurations."""
|
|
128
|
+
return [APIConfig(**c) for c in self._data.get("api_configs", [])]
|
|
129
|
+
|
|
130
|
+
def add_project_profile(
|
|
131
|
+
self,
|
|
132
|
+
name: str,
|
|
133
|
+
api_config: str,
|
|
134
|
+
system_prompt: Optional[str] = None,
|
|
135
|
+
allowed_commands: Optional[List[str]] = None
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Add a project profile."""
|
|
138
|
+
profiles = self._data.get("project_profiles", [])
|
|
139
|
+
|
|
140
|
+
profile = ProjectProfile(
|
|
141
|
+
name=name,
|
|
142
|
+
api_config=api_config,
|
|
143
|
+
system_prompt=system_prompt,
|
|
144
|
+
allowed_commands=allowed_commands or ["all"]
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
profiles.append(profile.model_dump())
|
|
148
|
+
self._data["project_profiles"] = profiles
|
|
149
|
+
self._save_config()
|
|
150
|
+
|
|
151
|
+
def get_project_profile(self, cwd: Optional[Path] = None) -> Optional[ProjectProfile]:
|
|
152
|
+
"""Get project profile for current directory."""
|
|
153
|
+
if cwd is None:
|
|
154
|
+
cwd = Path.cwd()
|
|
155
|
+
|
|
156
|
+
# Check for .claude-dev-cli file in current or parent directories
|
|
157
|
+
current = cwd
|
|
158
|
+
while current != current.parent:
|
|
159
|
+
config_file = current / ".claude-dev-cli"
|
|
160
|
+
if config_file.exists():
|
|
161
|
+
with open(config_file, 'r') as f:
|
|
162
|
+
data = json.load(f)
|
|
163
|
+
return ProjectProfile(**data)
|
|
164
|
+
current = current.parent
|
|
165
|
+
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def get_model(self) -> str:
|
|
169
|
+
"""Get default model."""
|
|
170
|
+
return self._data.get("default_model", "claude-3-5-sonnet-20241022")
|
|
171
|
+
|
|
172
|
+
def get_max_tokens(self) -> int:
|
|
173
|
+
"""Get default max tokens."""
|
|
174
|
+
return self._data.get("max_tokens", 4096)
|
claude_dev_cli/core.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Core Claude API client with routing and tracking."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Dict, Any, List
|
|
7
|
+
from anthropic import Anthropic
|
|
8
|
+
|
|
9
|
+
from claude_dev_cli.config import Config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ClaudeClient:
|
|
13
|
+
"""Claude API client with multi-key routing and usage tracking."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, config: Optional[Config] = None, api_config_name: Optional[str] = None):
|
|
16
|
+
"""Initialize Claude client."""
|
|
17
|
+
self.config = config or Config()
|
|
18
|
+
|
|
19
|
+
# Determine which API config to use
|
|
20
|
+
project_profile = self.config.get_project_profile()
|
|
21
|
+
if project_profile:
|
|
22
|
+
api_config_name = project_profile.api_config
|
|
23
|
+
|
|
24
|
+
self.api_config = self.config.get_api_config(api_config_name)
|
|
25
|
+
if not self.api_config:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
"No API configuration found. Run 'cdc config add' to set up an API key."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
self.client = Anthropic(api_key=self.api_config.api_key)
|
|
31
|
+
self.model = self.config.get_model()
|
|
32
|
+
self.max_tokens = self.config.get_max_tokens()
|
|
33
|
+
|
|
34
|
+
def call(
|
|
35
|
+
self,
|
|
36
|
+
prompt: str,
|
|
37
|
+
system_prompt: Optional[str] = None,
|
|
38
|
+
model: Optional[str] = None,
|
|
39
|
+
max_tokens: Optional[int] = None,
|
|
40
|
+
temperature: float = 1.0,
|
|
41
|
+
stream: bool = False
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Make a call to Claude API."""
|
|
44
|
+
model = model or self.model
|
|
45
|
+
max_tokens = max_tokens or self.max_tokens
|
|
46
|
+
|
|
47
|
+
# Check project profile for system prompt
|
|
48
|
+
project_profile = self.config.get_project_profile()
|
|
49
|
+
if project_profile and project_profile.system_prompt and not system_prompt:
|
|
50
|
+
system_prompt = project_profile.system_prompt
|
|
51
|
+
|
|
52
|
+
kwargs: Dict[str, Any] = {
|
|
53
|
+
"model": model,
|
|
54
|
+
"max_tokens": max_tokens,
|
|
55
|
+
"temperature": temperature,
|
|
56
|
+
"messages": [{"role": "user", "content": prompt}]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if system_prompt:
|
|
60
|
+
kwargs["system"] = system_prompt
|
|
61
|
+
|
|
62
|
+
start_time = datetime.utcnow()
|
|
63
|
+
response = self.client.messages.create(**kwargs)
|
|
64
|
+
end_time = datetime.utcnow()
|
|
65
|
+
|
|
66
|
+
# Log usage
|
|
67
|
+
self._log_usage(
|
|
68
|
+
prompt=prompt,
|
|
69
|
+
response=response,
|
|
70
|
+
model=model,
|
|
71
|
+
duration_ms=int((end_time - start_time).total_seconds() * 1000),
|
|
72
|
+
api_config_name=self.api_config.name
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Extract text from response
|
|
76
|
+
text_blocks = [
|
|
77
|
+
block.text for block in response.content if hasattr(block, 'text')
|
|
78
|
+
]
|
|
79
|
+
return '\n'.join(text_blocks)
|
|
80
|
+
|
|
81
|
+
def call_streaming(
|
|
82
|
+
self,
|
|
83
|
+
prompt: str,
|
|
84
|
+
system_prompt: Optional[str] = None,
|
|
85
|
+
model: Optional[str] = None,
|
|
86
|
+
max_tokens: Optional[int] = None,
|
|
87
|
+
temperature: float = 1.0
|
|
88
|
+
):
|
|
89
|
+
"""Make a streaming call to Claude API."""
|
|
90
|
+
model = model or self.model
|
|
91
|
+
max_tokens = max_tokens or self.max_tokens
|
|
92
|
+
|
|
93
|
+
# Check project profile for system prompt
|
|
94
|
+
project_profile = self.config.get_project_profile()
|
|
95
|
+
if project_profile and project_profile.system_prompt and not system_prompt:
|
|
96
|
+
system_prompt = project_profile.system_prompt
|
|
97
|
+
|
|
98
|
+
kwargs: Dict[str, Any] = {
|
|
99
|
+
"model": model,
|
|
100
|
+
"max_tokens": max_tokens,
|
|
101
|
+
"temperature": temperature,
|
|
102
|
+
"messages": [{"role": "user", "content": prompt}]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if system_prompt:
|
|
106
|
+
kwargs["system"] = system_prompt
|
|
107
|
+
|
|
108
|
+
with self.client.messages.stream(**kwargs) as stream:
|
|
109
|
+
for text in stream.text_stream:
|
|
110
|
+
yield text
|
|
111
|
+
|
|
112
|
+
def _log_usage(
|
|
113
|
+
self,
|
|
114
|
+
prompt: str,
|
|
115
|
+
response: Any,
|
|
116
|
+
model: str,
|
|
117
|
+
duration_ms: int,
|
|
118
|
+
api_config_name: str
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Log API usage to file."""
|
|
121
|
+
log_entry = {
|
|
122
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
123
|
+
"api_config": api_config_name,
|
|
124
|
+
"model": model,
|
|
125
|
+
"prompt_preview": prompt[:100],
|
|
126
|
+
"input_tokens": response.usage.input_tokens,
|
|
127
|
+
"output_tokens": response.usage.output_tokens,
|
|
128
|
+
"duration_ms": duration_ms,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
with open(self.config.usage_log, 'a') as f:
|
|
132
|
+
f.write(json.dumps(log_entry) + '\n')
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Prompt templates for various commands."""
|
|
2
|
+
|
|
3
|
+
TEST_GENERATION_PROMPT = """Generate comprehensive pytest tests for the following Python code from {filename}.
|
|
4
|
+
|
|
5
|
+
Code:
|
|
6
|
+
```python
|
|
7
|
+
{code}
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Requirements:
|
|
11
|
+
- Use pytest framework
|
|
12
|
+
- Include fixtures where appropriate
|
|
13
|
+
- Test normal cases, edge cases, and error conditions
|
|
14
|
+
- Use descriptive test names
|
|
15
|
+
- Add docstrings to test functions
|
|
16
|
+
- Mock external dependencies if needed
|
|
17
|
+
|
|
18
|
+
Please provide only the test code without explanations."""
|
|
19
|
+
|
|
20
|
+
CODE_REVIEW_PROMPT = """Review the following Python code from {filename} for:
|
|
21
|
+
|
|
22
|
+
1. **Bugs and Logic Errors**: Identify potential bugs
|
|
23
|
+
2. **Security Issues**: Check for vulnerabilities
|
|
24
|
+
3. **Performance**: Suggest optimizations
|
|
25
|
+
4. **Best Practices**: Python idioms and patterns
|
|
26
|
+
5. **Code Quality**: Readability and maintainability
|
|
27
|
+
|
|
28
|
+
Code:
|
|
29
|
+
```python
|
|
30
|
+
{code}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Please provide a structured review with specific line numbers where applicable."""
|
|
34
|
+
|
|
35
|
+
DEBUG_PROMPT = """Analyze the following error and code to identify the root cause and provide a fix.
|
|
36
|
+
|
|
37
|
+
File: {filename}
|
|
38
|
+
|
|
39
|
+
Code:
|
|
40
|
+
```python
|
|
41
|
+
{code}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Error:
|
|
45
|
+
```
|
|
46
|
+
{error}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Please provide:
|
|
50
|
+
1. Root cause analysis
|
|
51
|
+
2. Specific fix with code
|
|
52
|
+
3. Explanation of why the error occurred"""
|
|
53
|
+
|
|
54
|
+
DOCS_GENERATION_PROMPT = """Generate comprehensive documentation for the following Python code from {filename}.
|
|
55
|
+
|
|
56
|
+
Code:
|
|
57
|
+
```python
|
|
58
|
+
{code}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Please provide:
|
|
62
|
+
1. Module-level docstring
|
|
63
|
+
2. Function/class docstrings in Google style
|
|
64
|
+
3. Usage examples
|
|
65
|
+
4. A README section explaining the module
|
|
66
|
+
|
|
67
|
+
Use clear, concise language suitable for developers."""
|
|
68
|
+
|
|
69
|
+
REFACTOR_PROMPT = """Analyze the following Python code from {filename} and suggest refactoring improvements.
|
|
70
|
+
|
|
71
|
+
Code:
|
|
72
|
+
```python
|
|
73
|
+
{code}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Focus on:
|
|
77
|
+
1. Code duplication (DRY principle)
|
|
78
|
+
2. Function/class complexity
|
|
79
|
+
3. Naming conventions
|
|
80
|
+
4. Code organization
|
|
81
|
+
5. Design patterns that could be applied
|
|
82
|
+
6. Type hints and documentation
|
|
83
|
+
|
|
84
|
+
Please provide refactored code with explanations."""
|
|
85
|
+
|
|
86
|
+
GIT_COMMIT_PROMPT = """Generate a conventional commit message for the following git diff.
|
|
87
|
+
|
|
88
|
+
Diff:
|
|
89
|
+
```
|
|
90
|
+
{diff}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Format:
|
|
94
|
+
```
|
|
95
|
+
<type>(<scope>): <subject>
|
|
96
|
+
|
|
97
|
+
<body>
|
|
98
|
+
|
|
99
|
+
<footer>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Types: feat, fix, docs, style, refactor, test, chore
|
|
103
|
+
|
|
104
|
+
Keep the subject under 50 characters. Use present tense. Be specific about what changed and why."""
|
claude_dev_cli/usage.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Usage tracking and statistics."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, Dict, Any, List
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
|
|
13
|
+
from claude_dev_cli.config import Config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Pricing per 1M tokens (as of Dec 2024)
|
|
17
|
+
MODEL_PRICING = {
|
|
18
|
+
"claude-3-5-sonnet-20241022": {"input": 3.00, "output": 15.00},
|
|
19
|
+
"claude-3-5-sonnet-20240620": {"input": 3.00, "output": 15.00},
|
|
20
|
+
"claude-3-opus-20240229": {"input": 15.00, "output": 75.00},
|
|
21
|
+
"claude-3-sonnet-20240229": {"input": 3.00, "output": 15.00},
|
|
22
|
+
"claude-3-haiku-20240307": {"input": 0.25, "output": 1.25},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UsageTracker:
|
|
27
|
+
"""Track and display API usage statistics."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, config: Optional[Config] = None):
|
|
30
|
+
"""Initialize usage tracker."""
|
|
31
|
+
self.config = config or Config()
|
|
32
|
+
|
|
33
|
+
def _read_logs(
|
|
34
|
+
self,
|
|
35
|
+
days: Optional[int] = None,
|
|
36
|
+
api_config: Optional[str] = None
|
|
37
|
+
) -> List[Dict[str, Any]]:
|
|
38
|
+
"""Read usage logs with optional filters."""
|
|
39
|
+
if not self.config.usage_log.exists():
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
cutoff = None
|
|
43
|
+
if days:
|
|
44
|
+
cutoff = datetime.utcnow() - timedelta(days=days)
|
|
45
|
+
|
|
46
|
+
logs = []
|
|
47
|
+
with open(self.config.usage_log, 'r') as f:
|
|
48
|
+
for line in f:
|
|
49
|
+
try:
|
|
50
|
+
entry = json.loads(line)
|
|
51
|
+
timestamp = datetime.fromisoformat(entry["timestamp"])
|
|
52
|
+
|
|
53
|
+
# Apply filters
|
|
54
|
+
if cutoff and timestamp < cutoff:
|
|
55
|
+
continue
|
|
56
|
+
if api_config and entry.get("api_config") != api_config:
|
|
57
|
+
continue
|
|
58
|
+
|
|
59
|
+
logs.append(entry)
|
|
60
|
+
except json.JSONDecodeError:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
return logs
|
|
64
|
+
|
|
65
|
+
def _calculate_cost(self, model: str, input_tokens: int, output_tokens: int) -> float:
|
|
66
|
+
"""Calculate cost for a given usage."""
|
|
67
|
+
pricing = MODEL_PRICING.get(model, {"input": 3.00, "output": 15.00})
|
|
68
|
+
|
|
69
|
+
input_cost = (input_tokens / 1_000_000) * pricing["input"]
|
|
70
|
+
output_cost = (output_tokens / 1_000_000) * pricing["output"]
|
|
71
|
+
|
|
72
|
+
return input_cost + output_cost
|
|
73
|
+
|
|
74
|
+
def display_usage(
|
|
75
|
+
self,
|
|
76
|
+
console: Console,
|
|
77
|
+
days: Optional[int] = None,
|
|
78
|
+
api_config: Optional[str] = None
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Display usage statistics."""
|
|
81
|
+
logs = self._read_logs(days=days, api_config=api_config)
|
|
82
|
+
|
|
83
|
+
if not logs:
|
|
84
|
+
console.print("[yellow]No usage data found.[/yellow]")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
# Calculate totals
|
|
88
|
+
total_input = 0
|
|
89
|
+
total_output = 0
|
|
90
|
+
total_cost = 0.0
|
|
91
|
+
total_calls = len(logs)
|
|
92
|
+
|
|
93
|
+
by_api = defaultdict(lambda: {"input": 0, "output": 0, "calls": 0, "cost": 0.0})
|
|
94
|
+
by_date = defaultdict(lambda: {"input": 0, "output": 0, "calls": 0, "cost": 0.0})
|
|
95
|
+
by_model = defaultdict(lambda: {"input": 0, "output": 0, "calls": 0, "cost": 0.0})
|
|
96
|
+
|
|
97
|
+
for entry in logs:
|
|
98
|
+
input_tokens = entry["input_tokens"]
|
|
99
|
+
output_tokens = entry["output_tokens"]
|
|
100
|
+
model = entry["model"]
|
|
101
|
+
api = entry["api_config"]
|
|
102
|
+
date = entry["timestamp"][:10]
|
|
103
|
+
|
|
104
|
+
cost = self._calculate_cost(model, input_tokens, output_tokens)
|
|
105
|
+
|
|
106
|
+
total_input += input_tokens
|
|
107
|
+
total_output += output_tokens
|
|
108
|
+
total_cost += cost
|
|
109
|
+
|
|
110
|
+
by_api[api]["input"] += input_tokens
|
|
111
|
+
by_api[api]["output"] += output_tokens
|
|
112
|
+
by_api[api]["calls"] += 1
|
|
113
|
+
by_api[api]["cost"] += cost
|
|
114
|
+
|
|
115
|
+
by_date[date]["input"] += input_tokens
|
|
116
|
+
by_date[date]["output"] += output_tokens
|
|
117
|
+
by_date[date]["calls"] += 1
|
|
118
|
+
by_date[date]["cost"] += cost
|
|
119
|
+
|
|
120
|
+
by_model[model]["input"] += input_tokens
|
|
121
|
+
by_model[model]["output"] += output_tokens
|
|
122
|
+
by_model[model]["calls"] += 1
|
|
123
|
+
by_model[model]["cost"] += cost
|
|
124
|
+
|
|
125
|
+
# Display summary
|
|
126
|
+
title = "Usage Summary"
|
|
127
|
+
if days:
|
|
128
|
+
title += f" (Last {days} days)"
|
|
129
|
+
if api_config:
|
|
130
|
+
title += f" - {api_config}"
|
|
131
|
+
|
|
132
|
+
summary = f"""[bold]Total Calls:[/bold] {total_calls:,}
|
|
133
|
+
[bold]Input Tokens:[/bold] {total_input:,}
|
|
134
|
+
[bold]Output Tokens:[/bold] {total_output:,}
|
|
135
|
+
[bold]Total Tokens:[/bold] {total_input + total_output:,}
|
|
136
|
+
[bold]Estimated Cost:[/bold] ${total_cost:.2f}"""
|
|
137
|
+
|
|
138
|
+
console.print(Panel(summary, title=title, border_style="green"))
|
|
139
|
+
|
|
140
|
+
# Display by API config
|
|
141
|
+
if len(by_api) > 1:
|
|
142
|
+
console.print("\n[bold]By API Config:[/bold]")
|
|
143
|
+
api_table = Table(show_header=True, header_style="bold cyan")
|
|
144
|
+
api_table.add_column("API Config")
|
|
145
|
+
api_table.add_column("Calls", justify="right")
|
|
146
|
+
api_table.add_column("Input Tokens", justify="right")
|
|
147
|
+
api_table.add_column("Output Tokens", justify="right")
|
|
148
|
+
api_table.add_column("Cost", justify="right")
|
|
149
|
+
|
|
150
|
+
for api_name in sorted(by_api.keys()):
|
|
151
|
+
stats = by_api[api_name]
|
|
152
|
+
api_table.add_row(
|
|
153
|
+
api_name,
|
|
154
|
+
f"{stats['calls']:,}",
|
|
155
|
+
f"{stats['input']:,}",
|
|
156
|
+
f"{stats['output']:,}",
|
|
157
|
+
f"${stats['cost']:.2f}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
console.print(api_table)
|
|
161
|
+
|
|
162
|
+
# Display by model
|
|
163
|
+
if len(by_model) > 1:
|
|
164
|
+
console.print("\n[bold]By Model:[/bold]")
|
|
165
|
+
model_table = Table(show_header=True, header_style="bold cyan")
|
|
166
|
+
model_table.add_column("Model")
|
|
167
|
+
model_table.add_column("Calls", justify="right")
|
|
168
|
+
model_table.add_column("Input Tokens", justify="right")
|
|
169
|
+
model_table.add_column("Output Tokens", justify="right")
|
|
170
|
+
model_table.add_column("Cost", justify="right")
|
|
171
|
+
|
|
172
|
+
for model_name in sorted(by_model.keys()):
|
|
173
|
+
stats = by_model[model_name]
|
|
174
|
+
model_table.add_row(
|
|
175
|
+
model_name.split("-")[-1], # Show short version
|
|
176
|
+
f"{stats['calls']:,}",
|
|
177
|
+
f"{stats['input']:,}",
|
|
178
|
+
f"{stats['output']:,}",
|
|
179
|
+
f"${stats['cost']:.2f}"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
console.print(model_table)
|
|
183
|
+
|
|
184
|
+
# Display by date (last 7 days)
|
|
185
|
+
console.print("\n[bold]By Date:[/bold]")
|
|
186
|
+
date_table = Table(show_header=True, header_style="bold cyan")
|
|
187
|
+
date_table.add_column("Date")
|
|
188
|
+
date_table.add_column("Calls", justify="right")
|
|
189
|
+
date_table.add_column("Tokens", justify="right")
|
|
190
|
+
date_table.add_column("Cost", justify="right")
|
|
191
|
+
|
|
192
|
+
for date in sorted(by_date.keys(), reverse=True)[:7]:
|
|
193
|
+
stats = by_date[date]
|
|
194
|
+
total_tokens = stats['input'] + stats['output']
|
|
195
|
+
date_table.add_row(
|
|
196
|
+
date,
|
|
197
|
+
f"{stats['calls']:,}",
|
|
198
|
+
f"{total_tokens:,}",
|
|
199
|
+
f"${stats['cost']:.2f}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
console.print(date_table)
|