claude-dev-cli 0.9.0__py3-none-any.whl → 0.10.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 +1 -1
- claude_dev_cli/cli.py +193 -0
- claude_dev_cli/config.py +204 -1
- claude_dev_cli/core.py +54 -6
- claude_dev_cli/usage.py +25 -18
- {claude_dev_cli-0.9.0.dist-info → claude_dev_cli-0.10.0.dist-info}/METADATA +53 -3
- {claude_dev_cli-0.9.0.dist-info → claude_dev_cli-0.10.0.dist-info}/RECORD +11 -11
- {claude_dev_cli-0.9.0.dist-info → claude_dev_cli-0.10.0.dist-info}/WHEEL +0 -0
- {claude_dev_cli-0.9.0.dist-info → claude_dev_cli-0.10.0.dist-info}/entry_points.txt +0 -0
- {claude_dev_cli-0.9.0.dist-info → claude_dev_cli-0.10.0.dist-info}/licenses/LICENSE +0 -0
- {claude_dev_cli-0.9.0.dist-info → claude_dev_cli-0.10.0.dist-info}/top_level.txt +0 -0
claude_dev_cli/__init__.py
CHANGED
claude_dev_cli/cli.py
CHANGED
|
@@ -606,6 +606,199 @@ def config_set_model(ctx: click.Context, model: str) -> None:
|
|
|
606
606
|
sys.exit(1)
|
|
607
607
|
|
|
608
608
|
|
|
609
|
+
@main.group()
|
|
610
|
+
def model() -> None:
|
|
611
|
+
"""Manage model profiles and pricing."""
|
|
612
|
+
pass
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
@model.command('add')
|
|
616
|
+
@click.argument('name')
|
|
617
|
+
@click.argument('model_id')
|
|
618
|
+
@click.option('--input-price', type=float, required=True, help='Input price per Mtok (USD)')
|
|
619
|
+
@click.option('--output-price', type=float, required=True, help='Output price per Mtok (USD)')
|
|
620
|
+
@click.option('--description', help='Model profile description')
|
|
621
|
+
@click.option('--api-config', help='Tie to specific API config')
|
|
622
|
+
@click.option('--default', is_flag=True, help='Set as default')
|
|
623
|
+
@click.pass_context
|
|
624
|
+
def model_add(
|
|
625
|
+
ctx: click.Context,
|
|
626
|
+
name: str,
|
|
627
|
+
model_id: str,
|
|
628
|
+
input_price: float,
|
|
629
|
+
output_price: float,
|
|
630
|
+
description: Optional[str],
|
|
631
|
+
api_config: Optional[str],
|
|
632
|
+
default: bool
|
|
633
|
+
) -> None:
|
|
634
|
+
"""Add a model profile.
|
|
635
|
+
|
|
636
|
+
Examples:
|
|
637
|
+
cdc model add fast claude-3-5-haiku-20241022 --input-price 0.80 --output-price 4.00
|
|
638
|
+
cdc model add enterprise-smart claude-sonnet-4-5-20250929 --input-price 2.50 --output-price 12.50 --api-config enterprise
|
|
639
|
+
"""
|
|
640
|
+
console = ctx.obj['console']
|
|
641
|
+
|
|
642
|
+
try:
|
|
643
|
+
config = Config()
|
|
644
|
+
config.add_model_profile(
|
|
645
|
+
name=name,
|
|
646
|
+
model_id=model_id,
|
|
647
|
+
input_price=input_price,
|
|
648
|
+
output_price=output_price,
|
|
649
|
+
description=description,
|
|
650
|
+
api_config_name=api_config,
|
|
651
|
+
make_default=default
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
scope = f" for API '{api_config}'" if api_config else " (global)"
|
|
655
|
+
console.print(f"[green]✓[/green] Model profile '{name}' added{scope}")
|
|
656
|
+
console.print(f"[dim]Model: {model_id}[/dim]")
|
|
657
|
+
console.print(f"[dim]Pricing: ${input_price}/Mtok input, ${output_price}/Mtok output[/dim]")
|
|
658
|
+
|
|
659
|
+
if default:
|
|
660
|
+
console.print(f"[green]Set as default{scope}[/green]")
|
|
661
|
+
|
|
662
|
+
except Exception as e:
|
|
663
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
664
|
+
sys.exit(1)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
@model.command('list')
|
|
668
|
+
@click.option('--api-config', help='Filter by API config')
|
|
669
|
+
@click.pass_context
|
|
670
|
+
def model_list(ctx: click.Context, api_config: Optional[str]) -> None:
|
|
671
|
+
"""List model profiles."""
|
|
672
|
+
console = ctx.obj['console']
|
|
673
|
+
|
|
674
|
+
try:
|
|
675
|
+
from rich.table import Table
|
|
676
|
+
|
|
677
|
+
config = Config()
|
|
678
|
+
profiles = config.list_model_profiles(api_config_name=api_config)
|
|
679
|
+
|
|
680
|
+
if not profiles:
|
|
681
|
+
console.print("[yellow]No model profiles found.[/yellow]")
|
|
682
|
+
console.print("Run 'cdc model add' to create one.")
|
|
683
|
+
return
|
|
684
|
+
|
|
685
|
+
# Get default profile
|
|
686
|
+
default_profile = config.get_default_model_profile(api_config_name=api_config)
|
|
687
|
+
|
|
688
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
689
|
+
table.add_column("Name", style="cyan")
|
|
690
|
+
table.add_column("Model ID", style="green")
|
|
691
|
+
table.add_column("Input $/Mtok", justify="right", style="yellow")
|
|
692
|
+
table.add_column("Output $/Mtok", justify="right", style="yellow")
|
|
693
|
+
table.add_column("Scope", style="blue")
|
|
694
|
+
table.add_column("Description")
|
|
695
|
+
|
|
696
|
+
for profile in profiles:
|
|
697
|
+
default_marker = " ⭐" if profile.name == default_profile else ""
|
|
698
|
+
scope = profile.api_config_name or "global"
|
|
699
|
+
|
|
700
|
+
table.add_row(
|
|
701
|
+
profile.name + default_marker,
|
|
702
|
+
profile.model_id,
|
|
703
|
+
f"${profile.input_price_per_mtok:.2f}",
|
|
704
|
+
f"${profile.output_price_per_mtok:.2f}",
|
|
705
|
+
scope,
|
|
706
|
+
profile.description or ""
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
console.print(table)
|
|
710
|
+
console.print(f"\n[dim]Default profile: {default_profile} ⭐[/dim]")
|
|
711
|
+
|
|
712
|
+
except Exception as e:
|
|
713
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
714
|
+
sys.exit(1)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
@model.command('show')
|
|
718
|
+
@click.argument('name')
|
|
719
|
+
@click.pass_context
|
|
720
|
+
def model_show(ctx: click.Context, name: str) -> None:
|
|
721
|
+
"""Show model profile details."""
|
|
722
|
+
console = ctx.obj['console']
|
|
723
|
+
|
|
724
|
+
try:
|
|
725
|
+
config = Config()
|
|
726
|
+
profile = config.get_model_profile(name)
|
|
727
|
+
|
|
728
|
+
if not profile:
|
|
729
|
+
console.print(f"[red]Model profile '{name}' not found[/red]")
|
|
730
|
+
sys.exit(1)
|
|
731
|
+
|
|
732
|
+
scope = profile.api_config_name or "global"
|
|
733
|
+
cost_1k_in = profile.input_price_per_mtok / 1000
|
|
734
|
+
cost_1k_out = profile.output_price_per_mtok / 1000
|
|
735
|
+
|
|
736
|
+
console.print(Panel(
|
|
737
|
+
f"[bold]{profile.name}[/bold]\n\n"
|
|
738
|
+
f"[dim]{profile.description or 'No description'}[/dim]\n\n"
|
|
739
|
+
f"Model ID: [green]{profile.model_id}[/green]\n"
|
|
740
|
+
f"Scope: [blue]{scope}[/blue]\n\n"
|
|
741
|
+
f"Pricing:\n"
|
|
742
|
+
f" Input: ${profile.input_price_per_mtok:.2f}/Mtok (${cost_1k_in:.4f}/1K tokens)\n"
|
|
743
|
+
f" Output: ${profile.output_price_per_mtok:.2f}/Mtok (${cost_1k_out:.4f}/1K tokens)\n\n"
|
|
744
|
+
f"Use cases: {', '.join(profile.use_cases) if profile.use_cases else 'None specified'}",
|
|
745
|
+
title="Model Profile",
|
|
746
|
+
border_style="blue"
|
|
747
|
+
))
|
|
748
|
+
|
|
749
|
+
except Exception as e:
|
|
750
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
751
|
+
sys.exit(1)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
@model.command('remove')
|
|
755
|
+
@click.argument('name')
|
|
756
|
+
@click.pass_context
|
|
757
|
+
def model_remove(ctx: click.Context, name: str) -> None:
|
|
758
|
+
"""Remove a model profile."""
|
|
759
|
+
console = ctx.obj['console']
|
|
760
|
+
|
|
761
|
+
try:
|
|
762
|
+
config = Config()
|
|
763
|
+
if config.remove_model_profile(name):
|
|
764
|
+
console.print(f"[green]✓[/green] Model profile '{name}' removed")
|
|
765
|
+
else:
|
|
766
|
+
console.print(f"[red]Model profile '{name}' not found[/red]")
|
|
767
|
+
sys.exit(1)
|
|
768
|
+
|
|
769
|
+
except Exception as e:
|
|
770
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
771
|
+
sys.exit(1)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
@model.command('set-default')
|
|
775
|
+
@click.argument('name')
|
|
776
|
+
@click.option('--api-config', help='Set default for specific API config')
|
|
777
|
+
@click.pass_context
|
|
778
|
+
def model_set_default(ctx: click.Context, name: str, api_config: Optional[str]) -> None:
|
|
779
|
+
"""Set default model profile.
|
|
780
|
+
|
|
781
|
+
Examples:
|
|
782
|
+
cdc model set-default smart
|
|
783
|
+
cdc model set-default enterprise-smart --api-config enterprise
|
|
784
|
+
"""
|
|
785
|
+
console = ctx.obj['console']
|
|
786
|
+
|
|
787
|
+
try:
|
|
788
|
+
config = Config()
|
|
789
|
+
|
|
790
|
+
if api_config:
|
|
791
|
+
config.set_api_default_model_profile(api_config, name)
|
|
792
|
+
console.print(f"[green]✓[/green] Default model for API '{api_config}' set to: {name}")
|
|
793
|
+
else:
|
|
794
|
+
config.set_default_model_profile(name)
|
|
795
|
+
console.print(f"[green]✓[/green] Global default model set to: {name}")
|
|
796
|
+
|
|
797
|
+
except Exception as e:
|
|
798
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
799
|
+
sys.exit(1)
|
|
800
|
+
|
|
801
|
+
|
|
609
802
|
@main.group()
|
|
610
803
|
def generate() -> None:
|
|
611
804
|
"""Generate code, tests, and documentation."""
|
claude_dev_cli/config.py
CHANGED
|
@@ -37,6 +37,19 @@ class APIConfig(BaseModel):
|
|
|
37
37
|
api_key: str
|
|
38
38
|
description: Optional[str] = None
|
|
39
39
|
default: bool = False
|
|
40
|
+
default_model_profile: Optional[str] = None # Default model profile for this API
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ModelProfile(BaseModel):
|
|
44
|
+
"""Model profile with pricing information."""
|
|
45
|
+
|
|
46
|
+
name: str # User-friendly alias (e.g., "fast", "smart", "powerful")
|
|
47
|
+
model_id: str # Actual Claude model ID
|
|
48
|
+
description: Optional[str] = None
|
|
49
|
+
input_price_per_mtok: float # Input cost per million tokens (USD)
|
|
50
|
+
output_price_per_mtok: float # Output cost per million tokens (USD)
|
|
51
|
+
use_cases: List[str] = Field(default_factory=list) # Task types
|
|
52
|
+
api_config_name: Optional[str] = None # Tied to specific API config, or None for global
|
|
40
53
|
|
|
41
54
|
|
|
42
55
|
class ProjectProfile(BaseModel):
|
|
@@ -46,6 +59,7 @@ class ProjectProfile(BaseModel):
|
|
|
46
59
|
api_config: str # Name of the API config to use
|
|
47
60
|
system_prompt: Optional[str] = None
|
|
48
61
|
allowed_commands: List[str] = Field(default_factory=lambda: ["all"])
|
|
62
|
+
model_profile: Optional[str] = None # Preferred model profile for this project
|
|
49
63
|
|
|
50
64
|
# Project memory - preferences and patterns
|
|
51
65
|
auto_context: bool = False # Default value for --auto-context flag
|
|
@@ -108,7 +122,9 @@ class Config:
|
|
|
108
122
|
default_config = {
|
|
109
123
|
"api_configs": [],
|
|
110
124
|
"project_profiles": [],
|
|
111
|
-
"
|
|
125
|
+
"model_profiles": self._get_default_model_profiles(),
|
|
126
|
+
"default_model": "claude-sonnet-4-5-20250929", # Legacy, kept for backwards compat
|
|
127
|
+
"default_model_profile": "smart",
|
|
112
128
|
"max_tokens": 4096,
|
|
113
129
|
"context": ContextConfig().model_dump(),
|
|
114
130
|
"summarization": SummarizationConfig().model_dump(),
|
|
@@ -132,6 +148,10 @@ class Config:
|
|
|
132
148
|
config["context"] = ContextConfig().model_dump()
|
|
133
149
|
if "summarization" not in config:
|
|
134
150
|
config["summarization"] = SummarizationConfig().model_dump()
|
|
151
|
+
if "model_profiles" not in config:
|
|
152
|
+
config["model_profiles"] = self._get_default_model_profiles()
|
|
153
|
+
if "default_model_profile" not in config:
|
|
154
|
+
config["default_model_profile"] = "smart"
|
|
135
155
|
|
|
136
156
|
return config
|
|
137
157
|
except (json.JSONDecodeError, IOError) as e:
|
|
@@ -147,6 +167,38 @@ class Config:
|
|
|
147
167
|
with open(self.config_file, 'w') as f:
|
|
148
168
|
json.dump(data, f, indent=2)
|
|
149
169
|
|
|
170
|
+
def _get_default_model_profiles(self) -> List[Dict]:
|
|
171
|
+
"""Get default model profiles with current Anthropic pricing."""
|
|
172
|
+
return [
|
|
173
|
+
{
|
|
174
|
+
"name": "fast",
|
|
175
|
+
"model_id": "claude-3-5-haiku-20241022",
|
|
176
|
+
"description": "Fast and economical for simple tasks",
|
|
177
|
+
"input_price_per_mtok": 0.80,
|
|
178
|
+
"output_price_per_mtok": 4.00,
|
|
179
|
+
"use_cases": ["quick", "simple", "classification"],
|
|
180
|
+
"api_config_name": None
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "smart",
|
|
184
|
+
"model_id": "claude-sonnet-4-5-20250929",
|
|
185
|
+
"description": "Balanced performance and cost for most tasks",
|
|
186
|
+
"input_price_per_mtok": 3.00,
|
|
187
|
+
"output_price_per_mtok": 15.00,
|
|
188
|
+
"use_cases": ["general", "coding", "analysis"],
|
|
189
|
+
"api_config_name": None
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"name": "powerful",
|
|
193
|
+
"model_id": "claude-opus-4-20250514",
|
|
194
|
+
"description": "Maximum capability for complex tasks",
|
|
195
|
+
"input_price_per_mtok": 15.00,
|
|
196
|
+
"output_price_per_mtok": 75.00,
|
|
197
|
+
"use_cases": ["complex", "research", "creative"],
|
|
198
|
+
"api_config_name": None
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
|
|
150
202
|
def _auto_migrate_keys(self) -> None:
|
|
151
203
|
"""Automatically migrate plaintext API keys to secure storage."""
|
|
152
204
|
api_configs = self._data.get("api_configs", [])
|
|
@@ -322,3 +374,154 @@ class Config:
|
|
|
322
374
|
"""Get conversation summarization configuration."""
|
|
323
375
|
summ_data = self._data.get("summarization", {})
|
|
324
376
|
return SummarizationConfig(**summ_data) if summ_data else SummarizationConfig()
|
|
377
|
+
|
|
378
|
+
# Model Profile Management
|
|
379
|
+
|
|
380
|
+
def add_model_profile(
|
|
381
|
+
self,
|
|
382
|
+
name: str,
|
|
383
|
+
model_id: str,
|
|
384
|
+
input_price: float,
|
|
385
|
+
output_price: float,
|
|
386
|
+
description: Optional[str] = None,
|
|
387
|
+
use_cases: Optional[List[str]] = None,
|
|
388
|
+
api_config_name: Optional[str] = None,
|
|
389
|
+
make_default: bool = False
|
|
390
|
+
) -> None:
|
|
391
|
+
"""Add a model profile."""
|
|
392
|
+
profiles = self._data.get("model_profiles", [])
|
|
393
|
+
|
|
394
|
+
# Check if name already exists
|
|
395
|
+
for profile in profiles:
|
|
396
|
+
if profile["name"] == name:
|
|
397
|
+
raise ValueError(f"Model profile '{name}' already exists")
|
|
398
|
+
|
|
399
|
+
profile = ModelProfile(
|
|
400
|
+
name=name,
|
|
401
|
+
model_id=model_id,
|
|
402
|
+
description=description,
|
|
403
|
+
input_price_per_mtok=input_price,
|
|
404
|
+
output_price_per_mtok=output_price,
|
|
405
|
+
use_cases=use_cases or [],
|
|
406
|
+
api_config_name=api_config_name
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
profiles.append(profile.model_dump())
|
|
410
|
+
self._data["model_profiles"] = profiles
|
|
411
|
+
|
|
412
|
+
if make_default:
|
|
413
|
+
if api_config_name:
|
|
414
|
+
# Set as default for specific API config
|
|
415
|
+
self.set_api_default_model_profile(api_config_name, name)
|
|
416
|
+
else:
|
|
417
|
+
# Set as global default
|
|
418
|
+
self._data["default_model_profile"] = name
|
|
419
|
+
|
|
420
|
+
self._save_config()
|
|
421
|
+
|
|
422
|
+
def get_model_profile(
|
|
423
|
+
self,
|
|
424
|
+
name: str,
|
|
425
|
+
api_config_name: Optional[str] = None
|
|
426
|
+
) -> Optional[ModelProfile]:
|
|
427
|
+
"""Get model profile by name.
|
|
428
|
+
|
|
429
|
+
If api_config_name is provided, prefer API-specific profiles.
|
|
430
|
+
"""
|
|
431
|
+
profiles = self._data.get("model_profiles", [])
|
|
432
|
+
|
|
433
|
+
# First, try to find API-specific profile
|
|
434
|
+
if api_config_name:
|
|
435
|
+
for p in profiles:
|
|
436
|
+
if p["name"] == name and p.get("api_config_name") == api_config_name:
|
|
437
|
+
return ModelProfile(**p)
|
|
438
|
+
|
|
439
|
+
# Fall back to global profile (api_config_name = None)
|
|
440
|
+
for p in profiles:
|
|
441
|
+
if p["name"] == name and p.get("api_config_name") is None:
|
|
442
|
+
return ModelProfile(**p)
|
|
443
|
+
|
|
444
|
+
# Fall back to any profile with that name
|
|
445
|
+
for p in profiles:
|
|
446
|
+
if p["name"] == name:
|
|
447
|
+
return ModelProfile(**p)
|
|
448
|
+
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
def list_model_profiles(
|
|
452
|
+
self,
|
|
453
|
+
api_config_name: Optional[str] = None
|
|
454
|
+
) -> List[ModelProfile]:
|
|
455
|
+
"""List model profiles.
|
|
456
|
+
|
|
457
|
+
If api_config_name is provided, include both global and API-specific profiles.
|
|
458
|
+
"""
|
|
459
|
+
profiles = self._data.get("model_profiles", [])
|
|
460
|
+
result = []
|
|
461
|
+
|
|
462
|
+
for p in profiles:
|
|
463
|
+
profile_api = p.get("api_config_name")
|
|
464
|
+
# Include if: global profile OR matches requested API
|
|
465
|
+
if profile_api is None or (api_config_name and profile_api == api_config_name):
|
|
466
|
+
result.append(ModelProfile(**p))
|
|
467
|
+
|
|
468
|
+
return result
|
|
469
|
+
|
|
470
|
+
def remove_model_profile(self, name: str) -> bool:
|
|
471
|
+
"""Remove a model profile."""
|
|
472
|
+
profiles = self._data.get("model_profiles", [])
|
|
473
|
+
original_count = len(profiles)
|
|
474
|
+
|
|
475
|
+
self._data["model_profiles"] = [
|
|
476
|
+
p for p in profiles if p["name"] != name
|
|
477
|
+
]
|
|
478
|
+
|
|
479
|
+
if len(self._data["model_profiles"]) < original_count:
|
|
480
|
+
self._save_config()
|
|
481
|
+
return True
|
|
482
|
+
return False
|
|
483
|
+
|
|
484
|
+
def set_default_model_profile(self, name: str) -> None:
|
|
485
|
+
"""Set global default model profile."""
|
|
486
|
+
# Verify profile exists
|
|
487
|
+
if not self.get_model_profile(name):
|
|
488
|
+
raise ValueError(f"Model profile '{name}' not found")
|
|
489
|
+
|
|
490
|
+
self._data["default_model_profile"] = name
|
|
491
|
+
self._save_config()
|
|
492
|
+
|
|
493
|
+
def get_default_model_profile(self, api_config_name: Optional[str] = None) -> str:
|
|
494
|
+
"""Get default model profile name.
|
|
495
|
+
|
|
496
|
+
Resolution order:
|
|
497
|
+
1. API-specific default
|
|
498
|
+
2. Global default
|
|
499
|
+
3. Fallback to 'smart'
|
|
500
|
+
"""
|
|
501
|
+
# Check API-specific default
|
|
502
|
+
if api_config_name:
|
|
503
|
+
api_configs = self._data.get("api_configs", [])
|
|
504
|
+
for config in api_configs:
|
|
505
|
+
if config["name"] == api_config_name:
|
|
506
|
+
api_default = config.get("default_model_profile")
|
|
507
|
+
if api_default:
|
|
508
|
+
return api_default
|
|
509
|
+
|
|
510
|
+
# Global default
|
|
511
|
+
return self._data.get("default_model_profile", "smart")
|
|
512
|
+
|
|
513
|
+
def set_api_default_model_profile(self, api_config_name: str, profile_name: str) -> None:
|
|
514
|
+
"""Set default model profile for a specific API config."""
|
|
515
|
+
api_configs = self._data.get("api_configs", [])
|
|
516
|
+
|
|
517
|
+
found = False
|
|
518
|
+
for config in api_configs:
|
|
519
|
+
if config["name"] == api_config_name:
|
|
520
|
+
config["default_model_profile"] = profile_name
|
|
521
|
+
found = True
|
|
522
|
+
break
|
|
523
|
+
|
|
524
|
+
if not found:
|
|
525
|
+
raise ValueError(f"API config '{api_config_name}' not found")
|
|
526
|
+
|
|
527
|
+
self._save_config()
|
claude_dev_cli/core.py
CHANGED
|
@@ -39,6 +39,44 @@ class ClaudeClient:
|
|
|
39
39
|
self.model = self.config.get_model()
|
|
40
40
|
self.max_tokens = self.config.get_max_tokens()
|
|
41
41
|
|
|
42
|
+
def _resolve_model(self, model_or_profile: Optional[str] = None) -> str:
|
|
43
|
+
"""Resolve model profile name or ID to actual model ID.
|
|
44
|
+
|
|
45
|
+
Resolution hierarchy:
|
|
46
|
+
1. Explicit model_or_profile parameter (if provided)
|
|
47
|
+
2. Project-specific model profile
|
|
48
|
+
3. API-specific default model profile
|
|
49
|
+
4. Global default model profile
|
|
50
|
+
5. Legacy default model setting
|
|
51
|
+
|
|
52
|
+
Returns actual model ID for API calls.
|
|
53
|
+
"""
|
|
54
|
+
# Start with explicit parameter or legacy model setting
|
|
55
|
+
profile_or_id = model_or_profile or self.model
|
|
56
|
+
|
|
57
|
+
# If no explicit model, check hierarchical defaults
|
|
58
|
+
if not model_or_profile:
|
|
59
|
+
# Check project profile
|
|
60
|
+
project_profile = self.config.get_project_profile()
|
|
61
|
+
if project_profile and project_profile.model_profile:
|
|
62
|
+
profile_or_id = project_profile.model_profile
|
|
63
|
+
else:
|
|
64
|
+
# Get API or global default profile name
|
|
65
|
+
profile_or_id = self.config.get_default_model_profile(
|
|
66
|
+
api_config_name=self.api_config.name
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Try to resolve as profile name
|
|
70
|
+
profile = self.config.get_model_profile(
|
|
71
|
+
profile_or_id,
|
|
72
|
+
api_config_name=self.api_config.name
|
|
73
|
+
)
|
|
74
|
+
if profile:
|
|
75
|
+
return profile.model_id
|
|
76
|
+
|
|
77
|
+
# Assume it's already a model ID
|
|
78
|
+
return profile_or_id
|
|
79
|
+
|
|
42
80
|
def call(
|
|
43
81
|
self,
|
|
44
82
|
prompt: str,
|
|
@@ -48,8 +86,13 @@ class ClaudeClient:
|
|
|
48
86
|
temperature: float = 1.0,
|
|
49
87
|
stream: bool = False
|
|
50
88
|
) -> str:
|
|
51
|
-
"""Make a call to Claude API.
|
|
52
|
-
|
|
89
|
+
"""Make a call to Claude API.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
model: Model ID or profile name (e.g., 'fast', 'smart', 'powerful')
|
|
93
|
+
"""
|
|
94
|
+
# Resolve profile name to model ID
|
|
95
|
+
resolved_model = self._resolve_model(model)
|
|
53
96
|
max_tokens = max_tokens or self.max_tokens
|
|
54
97
|
|
|
55
98
|
# Check project profile for system prompt
|
|
@@ -75,7 +118,7 @@ class ClaudeClient:
|
|
|
75
118
|
self._log_usage(
|
|
76
119
|
prompt=prompt,
|
|
77
120
|
response=response,
|
|
78
|
-
model=
|
|
121
|
+
model=resolved_model,
|
|
79
122
|
duration_ms=int((end_time - start_time).total_seconds() * 1000),
|
|
80
123
|
api_config_name=self.api_config.name
|
|
81
124
|
)
|
|
@@ -94,8 +137,13 @@ class ClaudeClient:
|
|
|
94
137
|
max_tokens: Optional[int] = None,
|
|
95
138
|
temperature: float = 1.0
|
|
96
139
|
):
|
|
97
|
-
"""Make a streaming call to Claude API.
|
|
98
|
-
|
|
140
|
+
"""Make a streaming call to Claude API.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
model: Model ID or profile name (e.g., 'fast', 'smart', 'powerful')
|
|
144
|
+
"""
|
|
145
|
+
# Resolve profile name to model ID
|
|
146
|
+
resolved_model = self._resolve_model(model)
|
|
99
147
|
max_tokens = max_tokens or self.max_tokens
|
|
100
148
|
|
|
101
149
|
# Check project profile for system prompt
|
|
@@ -104,7 +152,7 @@ class ClaudeClient:
|
|
|
104
152
|
system_prompt = project_profile.system_prompt
|
|
105
153
|
|
|
106
154
|
kwargs: Dict[str, Any] = {
|
|
107
|
-
"model":
|
|
155
|
+
"model": resolved_model,
|
|
108
156
|
"max_tokens": max_tokens,
|
|
109
157
|
"temperature": temperature,
|
|
110
158
|
"messages": [{"role": "user", "content": prompt}]
|
claude_dev_cli/usage.py
CHANGED
|
@@ -13,16 +13,6 @@ from rich.panel import Panel
|
|
|
13
13
|
from claude_dev_cli.config import Config
|
|
14
14
|
|
|
15
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
16
|
class UsageTracker:
|
|
27
17
|
"""Track and display API usage statistics."""
|
|
28
18
|
|
|
@@ -62,13 +52,30 @@ class UsageTracker:
|
|
|
62
52
|
|
|
63
53
|
return logs
|
|
64
54
|
|
|
65
|
-
def _calculate_cost(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
55
|
+
def _calculate_cost(
|
|
56
|
+
self,
|
|
57
|
+
model: str,
|
|
58
|
+
input_tokens: int,
|
|
59
|
+
output_tokens: int,
|
|
60
|
+
api_config_name: Optional[str] = None
|
|
61
|
+
) -> float:
|
|
62
|
+
"""Calculate cost for a given usage.
|
|
63
|
+
|
|
64
|
+
Looks up pricing from model profiles. Falls back to:
|
|
65
|
+
1. Finding profile with matching model_id
|
|
66
|
+
2. Default Sonnet pricing if no match found
|
|
67
|
+
"""
|
|
68
|
+
# Try to find profile by model_id
|
|
69
|
+
profiles = self.config.list_model_profiles(api_config_name=api_config_name)
|
|
70
|
+
for profile in profiles:
|
|
71
|
+
if profile.model_id == model:
|
|
72
|
+
input_cost = (input_tokens / 1_000_000) * profile.input_price_per_mtok
|
|
73
|
+
output_cost = (output_tokens / 1_000_000) * profile.output_price_per_mtok
|
|
74
|
+
return input_cost + output_cost
|
|
75
|
+
|
|
76
|
+
# Fallback to default Sonnet pricing
|
|
77
|
+
input_cost = (input_tokens / 1_000_000) * 3.00
|
|
78
|
+
output_cost = (output_tokens / 1_000_000) * 15.00
|
|
72
79
|
return input_cost + output_cost
|
|
73
80
|
|
|
74
81
|
def display_usage(
|
|
@@ -101,7 +108,7 @@ class UsageTracker:
|
|
|
101
108
|
api = entry["api_config"]
|
|
102
109
|
date = entry["timestamp"][:10]
|
|
103
110
|
|
|
104
|
-
cost = self._calculate_cost(model, input_tokens, output_tokens)
|
|
111
|
+
cost = self._calculate_cost(model, input_tokens, output_tokens, api_config_name=api)
|
|
105
112
|
|
|
106
113
|
total_input += input_tokens
|
|
107
114
|
total_output += output_tokens
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-dev-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: A powerful CLI tool for developers using Claude AI with multi-API routing, test generation, code review, and usage tracking
|
|
5
5
|
Author-email: Julio <thinmanj@users.noreply.github.com>
|
|
6
6
|
License: MIT
|
|
@@ -46,7 +46,7 @@ Dynamic: license-file
|
|
|
46
46
|
|
|
47
47
|
[](https://badge.fury.io/py/claude-dev-cli)
|
|
48
48
|
[](https://www.python.org/downloads/)
|
|
49
|
-
[](https://github.com/thinmanj/claude-dev-cli)
|
|
50
50
|
[](https://opensource.org/licenses/MIT)
|
|
51
51
|
[](https://github.com/thinmanj/homebrew-tap)
|
|
52
52
|
[](https://github.com/psf/black)
|
|
@@ -61,6 +61,17 @@ A powerful command-line tool for developers using Claude AI with multi-API routi
|
|
|
61
61
|
- Automatic API selection based on project configuration
|
|
62
62
|
- Automatic migration from plaintext to secure storage
|
|
63
63
|
|
|
64
|
+
### 🎯 Model Profiles (v0.10.0+)
|
|
65
|
+
- **Named Profiles**: Use friendly names instead of full model IDs (`fast`, `smart`, `powerful`)
|
|
66
|
+
- **Custom Pricing**: Define input/output costs per Mtok for accurate usage tracking
|
|
67
|
+
- **API-Specific Profiles**: Different models and pricing per API config
|
|
68
|
+
- **Project Defaults**: Set per-project model preferences
|
|
69
|
+
- **Dynamic Resolution**: Profile names automatically resolve to model IDs
|
|
70
|
+
- **Built-in Profiles**:
|
|
71
|
+
- `fast`: Claude 3.5 Haiku ($0.80/$4.00 per Mtok)
|
|
72
|
+
- `smart`: Claude Sonnet 4 ($3.00/$15.00 per Mtok) - default
|
|
73
|
+
- `powerful`: Claude Opus 4 ($15.00/$75.00 per Mtok)
|
|
74
|
+
|
|
64
75
|
### 🧪 Developer Tools
|
|
65
76
|
- **Test Generation**: Automatic pytest test generation for Python code
|
|
66
77
|
- **Code Review**: Comprehensive code reviews with security, performance, and best practice checks
|
|
@@ -163,9 +174,13 @@ cdc config migrate-keys
|
|
|
163
174
|
### 2. Basic Usage
|
|
164
175
|
|
|
165
176
|
```bash
|
|
166
|
-
# Ask a question
|
|
177
|
+
# Ask a question (uses 'smart' profile by default)
|
|
167
178
|
cdc ask "explain asyncio in Python"
|
|
168
179
|
|
|
180
|
+
# Use a specific model profile
|
|
181
|
+
cdc ask -m fast "quick question" # Uses Haiku (fast & cheap)
|
|
182
|
+
cdc ask -m powerful "complex task" # Uses Opus 4 (most capable)
|
|
183
|
+
|
|
169
184
|
# With file context
|
|
170
185
|
cdc ask -f mycode.py "review this code"
|
|
171
186
|
|
|
@@ -179,6 +194,41 @@ cdc interactive
|
|
|
179
194
|
cdc ask -a client "generate tests for this function"
|
|
180
195
|
```
|
|
181
196
|
|
|
197
|
+
### 3. Model Profile Management (v0.10.0+)
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
# List available model profiles
|
|
201
|
+
cdc model list
|
|
202
|
+
# ┌───────────┬──────────────────────────┬────────┬────────┐
|
|
203
|
+
# │ Profile │ Model ID │ Input │ Output │
|
|
204
|
+
# │ fast │ claude-3-5-haiku-... │ $0.80 │ $4.00 │
|
|
205
|
+
# │ smart │ claude-sonnet-4-... │ $3.00 │ $15.00 │ ← default
|
|
206
|
+
# │ powerful │ claude-opus-4-... │ $15.00 │ $75.00 │
|
|
207
|
+
# └───────────┴──────────────────────────┴────────┴────────┘
|
|
208
|
+
|
|
209
|
+
# Add a custom profile
|
|
210
|
+
cdc model add lightning claude-3-5-haiku-20241022 \
|
|
211
|
+
--input-price 0.80 --output-price 4.00 \
|
|
212
|
+
--description "Ultra-fast model for simple tasks"
|
|
213
|
+
|
|
214
|
+
# Set default model profile
|
|
215
|
+
cdc model set-default fast # Use Haiku by default
|
|
216
|
+
|
|
217
|
+
# Set default for specific API config
|
|
218
|
+
cdc model set-default powerful --api enterprise # Opus for enterprise API
|
|
219
|
+
|
|
220
|
+
# Show profile details
|
|
221
|
+
cdc model show smart
|
|
222
|
+
|
|
223
|
+
# Remove a profile
|
|
224
|
+
cdc model remove lightning
|
|
225
|
+
|
|
226
|
+
# Use profiles in any command
|
|
227
|
+
cdc ask -m fast "simple question"
|
|
228
|
+
cdc review -m powerful complex_file.py # More thorough review
|
|
229
|
+
cdc generate tests -m smart mymodule.py # Balanced approach
|
|
230
|
+
```
|
|
231
|
+
|
|
182
232
|
### 3. Developer Commands
|
|
183
233
|
|
|
184
234
|
```bash
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
claude_dev_cli/__init__.py,sha256=
|
|
2
|
-
claude_dev_cli/cli.py,sha256=
|
|
1
|
+
claude_dev_cli/__init__.py,sha256=VPv4UMGva44cEmCged38XThRzT1wbvPMvPu5IE2nkr0,470
|
|
2
|
+
claude_dev_cli/cli.py,sha256=kdEocOqRYTFsFlsfZMe5LTk9gBj6iIEqOG1JB42L1WI,68521
|
|
3
3
|
claude_dev_cli/commands.py,sha256=RKGx2rv56PM6eErvA2uoQ20hY8babuI5jav8nCUyUOk,3964
|
|
4
|
-
claude_dev_cli/config.py,sha256=
|
|
4
|
+
claude_dev_cli/config.py,sha256=ZnPvzwlXsoY9YhqTl4S__fwY1MzJXKIaYK0nIIelNXk,19978
|
|
5
5
|
claude_dev_cli/context.py,sha256=1TlLzpREFZDEIuU7RAtlkjxARKWZpnxHHvK283sUAZE,26714
|
|
6
|
-
claude_dev_cli/core.py,sha256
|
|
6
|
+
claude_dev_cli/core.py,sha256=-EoD-xY0i9RSM-LkQoYvL1Dsaut0eg4mgKFnSm_eZmw,6612
|
|
7
7
|
claude_dev_cli/history.py,sha256=26EjNW68JuFQJhUp1j8UdB19S-eYz3eqevkpCOATwP0,10510
|
|
8
8
|
claude_dev_cli/secure_storage.py,sha256=KcZuQMLTbQpMAi2Cyh-_JkNcK9vHzAITOgjTcM9sr98,8161
|
|
9
9
|
claude_dev_cli/template_manager.py,sha256=wtcrNuxFoJLJIPmIxUzrPKrE8kUvdqEd53EnG3jARhg,9277
|
|
10
10
|
claude_dev_cli/templates.py,sha256=lKxH943ySfUKgyHaWa4W3LVv91SgznKgajRtSRp_4UY,2260
|
|
11
11
|
claude_dev_cli/toon_utils.py,sha256=S3px2UvmNEaltmTa5K-h21n2c0CPvYjZc9mc7kHGqNQ,2828
|
|
12
|
-
claude_dev_cli/usage.py,sha256=
|
|
12
|
+
claude_dev_cli/usage.py,sha256=7F92mVUenSsONZxmug3ZLbd-8l1qcbqCAzzLbsrJF5Y,7700
|
|
13
13
|
claude_dev_cli/warp_integration.py,sha256=PDufAJecl7uN0DGz6XW4uDSEeu7Ssg53DOIFKm-AXVg,6909
|
|
14
14
|
claude_dev_cli/workflows.py,sha256=WpLq9I_0MmDsJIbCi9-f662JVyn8iKTs1KZ50w-GlZU,12202
|
|
15
15
|
claude_dev_cli/plugins/__init__.py,sha256=BdiZlylBzEgnwK2tuEdn8cITxhAZRVbTnDbWhdDhgqs,1340
|
|
@@ -17,9 +17,9 @@ claude_dev_cli/plugins/base.py,sha256=H4HQet1I-a3WLCfE9F06Lp8NuFvVoIlou7sIgyJFK-
|
|
|
17
17
|
claude_dev_cli/plugins/diff_editor/__init__.py,sha256=gqR5S2TyIVuq-sK107fegsutQ7Z-sgAIEbtc71FhXIM,101
|
|
18
18
|
claude_dev_cli/plugins/diff_editor/plugin.py,sha256=M1bUoqpasD3ZNQo36Fu_8g92uySPZyG_ujMbj5UplsU,3073
|
|
19
19
|
claude_dev_cli/plugins/diff_editor/viewer.py,sha256=1IOXIKw_01ppJx5C1dQt9Kr6U1TdAHT8_iUT5r_q0NM,17169
|
|
20
|
-
claude_dev_cli-0.
|
|
21
|
-
claude_dev_cli-0.
|
|
22
|
-
claude_dev_cli-0.
|
|
23
|
-
claude_dev_cli-0.
|
|
24
|
-
claude_dev_cli-0.
|
|
25
|
-
claude_dev_cli-0.
|
|
20
|
+
claude_dev_cli-0.10.0.dist-info/licenses/LICENSE,sha256=DGueuJwMJtMwgLO5mWlS0TaeBrFwQuNpNZ22PU9J2bw,1062
|
|
21
|
+
claude_dev_cli-0.10.0.dist-info/METADATA,sha256=5KCr5ng6VgwpiLoL07exV5g09zTgfQKXkuNHnukcWdw,19569
|
|
22
|
+
claude_dev_cli-0.10.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
23
|
+
claude_dev_cli-0.10.0.dist-info/entry_points.txt,sha256=zymgUIIVpFTARkFmxAuW2A4BQsNITh_L0uU-XunytHg,85
|
|
24
|
+
claude_dev_cli-0.10.0.dist-info/top_level.txt,sha256=m7MF6LOIuTe41IT5Fgt0lc-DK1EgM4gUU_IZwWxK0pg,15
|
|
25
|
+
claude_dev_cli-0.10.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|