claude-dev-cli 0.9.0__tar.gz → 0.10.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of claude-dev-cli might be problematic. Click here for more details.

Files changed (42) hide show
  1. {claude_dev_cli-0.9.0/src/claude_dev_cli.egg-info → claude_dev_cli-0.10.1}/PKG-INFO +53 -3
  2. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/README.md +52 -2
  3. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/pyproject.toml +1 -1
  4. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/__init__.py +1 -1
  5. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/cli.py +193 -0
  6. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/config.py +204 -1
  7. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/core.py +55 -7
  8. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/usage.py +25 -18
  9. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1/src/claude_dev_cli.egg-info}/PKG-INFO +53 -3
  10. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/tests/test_core.py +2 -1
  11. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/tests/test_usage.py +6 -5
  12. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/LICENSE +0 -0
  13. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/MANIFEST.in +0 -0
  14. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/setup.cfg +0 -0
  15. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/commands.py +0 -0
  16. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/context.py +0 -0
  17. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/history.py +0 -0
  18. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/plugins/__init__.py +0 -0
  19. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/plugins/base.py +0 -0
  20. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/plugins/diff_editor/__init__.py +0 -0
  21. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/plugins/diff_editor/plugin.py +0 -0
  22. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/plugins/diff_editor/viewer.py +0 -0
  23. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/secure_storage.py +0 -0
  24. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/template_manager.py +0 -0
  25. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/templates.py +0 -0
  26. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/toon_utils.py +0 -0
  27. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/warp_integration.py +0 -0
  28. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli/workflows.py +0 -0
  29. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli.egg-info/SOURCES.txt +0 -0
  30. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli.egg-info/dependency_links.txt +0 -0
  31. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli.egg-info/entry_points.txt +0 -0
  32. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli.egg-info/requires.txt +0 -0
  33. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/src/claude_dev_cli.egg-info/top_level.txt +0 -0
  34. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/tests/test_cli.py +0 -0
  35. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/tests/test_commands.py +0 -0
  36. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/tests/test_config.py +0 -0
  37. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/tests/test_context.py +0 -0
  38. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/tests/test_diff_editor.py +0 -0
  39. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/tests/test_history.py +0 -0
  40. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/tests/test_secure_storage.py +0 -0
  41. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/tests/test_template_manager.py +0 -0
  42. {claude_dev_cli-0.9.0 → claude_dev_cli-0.10.1}/tests/test_toon_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-dev-cli
3
- Version: 0.9.0
3
+ Version: 0.10.1
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
  [![PyPI version](https://badge.fury.io/py/claude-dev-cli.svg)](https://badge.fury.io/py/claude-dev-cli)
48
48
  [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
49
- [![Tests](https://img.shields.io/badge/tests-259%20passing-brightgreen.svg)](https://github.com/thinmanj/claude-dev-cli)
49
+ [![Tests](https://img.shields.io/badge/tests-260%20passing-brightgreen.svg)](https://github.com/thinmanj/claude-dev-cli)
50
50
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
51
51
  [![Homebrew](https://img.shields.io/badge/homebrew-available-orange.svg)](https://github.com/thinmanj/homebrew-tap)
52
52
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](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
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![PyPI version](https://badge.fury.io/py/claude-dev-cli.svg)](https://badge.fury.io/py/claude-dev-cli)
4
4
  [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
5
- [![Tests](https://img.shields.io/badge/tests-259%20passing-brightgreen.svg)](https://github.com/thinmanj/claude-dev-cli)
5
+ [![Tests](https://img.shields.io/badge/tests-260%20passing-brightgreen.svg)](https://github.com/thinmanj/claude-dev-cli)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
  [![Homebrew](https://img.shields.io/badge/homebrew-available-orange.svg)](https://github.com/thinmanj/homebrew-tap)
8
8
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
@@ -17,6 +17,17 @@ A powerful command-line tool for developers using Claude AI with multi-API routi
17
17
  - Automatic API selection based on project configuration
18
18
  - Automatic migration from plaintext to secure storage
19
19
 
20
+ ### 🎯 Model Profiles (v0.10.0+)
21
+ - **Named Profiles**: Use friendly names instead of full model IDs (`fast`, `smart`, `powerful`)
22
+ - **Custom Pricing**: Define input/output costs per Mtok for accurate usage tracking
23
+ - **API-Specific Profiles**: Different models and pricing per API config
24
+ - **Project Defaults**: Set per-project model preferences
25
+ - **Dynamic Resolution**: Profile names automatically resolve to model IDs
26
+ - **Built-in Profiles**:
27
+ - `fast`: Claude 3.5 Haiku ($0.80/$4.00 per Mtok)
28
+ - `smart`: Claude Sonnet 4 ($3.00/$15.00 per Mtok) - default
29
+ - `powerful`: Claude Opus 4 ($15.00/$75.00 per Mtok)
30
+
20
31
  ### 🧪 Developer Tools
21
32
  - **Test Generation**: Automatic pytest test generation for Python code
22
33
  - **Code Review**: Comprehensive code reviews with security, performance, and best practice checks
@@ -119,9 +130,13 @@ cdc config migrate-keys
119
130
  ### 2. Basic Usage
120
131
 
121
132
  ```bash
122
- # Ask a question
133
+ # Ask a question (uses 'smart' profile by default)
123
134
  cdc ask "explain asyncio in Python"
124
135
 
136
+ # Use a specific model profile
137
+ cdc ask -m fast "quick question" # Uses Haiku (fast & cheap)
138
+ cdc ask -m powerful "complex task" # Uses Opus 4 (most capable)
139
+
125
140
  # With file context
126
141
  cdc ask -f mycode.py "review this code"
127
142
 
@@ -135,6 +150,41 @@ cdc interactive
135
150
  cdc ask -a client "generate tests for this function"
136
151
  ```
137
152
 
153
+ ### 3. Model Profile Management (v0.10.0+)
154
+
155
+ ```bash
156
+ # List available model profiles
157
+ cdc model list
158
+ # ┌───────────┬──────────────────────────┬────────┬────────┐
159
+ # │ Profile │ Model ID │ Input │ Output │
160
+ # │ fast │ claude-3-5-haiku-... │ $0.80 │ $4.00 │
161
+ # │ smart │ claude-sonnet-4-... │ $3.00 │ $15.00 │ ← default
162
+ # │ powerful │ claude-opus-4-... │ $15.00 │ $75.00 │
163
+ # └───────────┴──────────────────────────┴────────┴────────┘
164
+
165
+ # Add a custom profile
166
+ cdc model add lightning claude-3-5-haiku-20241022 \
167
+ --input-price 0.80 --output-price 4.00 \
168
+ --description "Ultra-fast model for simple tasks"
169
+
170
+ # Set default model profile
171
+ cdc model set-default fast # Use Haiku by default
172
+
173
+ # Set default for specific API config
174
+ cdc model set-default powerful --api enterprise # Opus for enterprise API
175
+
176
+ # Show profile details
177
+ cdc model show smart
178
+
179
+ # Remove a profile
180
+ cdc model remove lightning
181
+
182
+ # Use profiles in any command
183
+ cdc ask -m fast "simple question"
184
+ cdc review -m powerful complex_file.py # More thorough review
185
+ cdc generate tests -m smart mymodule.py # Balanced approach
186
+ ```
187
+
138
188
  ### 3. Developer Commands
139
189
 
140
190
  ```bash
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "claude-dev-cli"
7
- version = "0.9.0"
7
+ version = "0.10.1"
8
8
  description = "A powerful CLI tool for developers using Claude AI with multi-API routing, test generation, code review, and usage tracking"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -9,7 +9,7 @@ Features:
9
9
  - Interactive and single-shot modes
10
10
  """
11
11
 
12
- __version__ = "0.9.0"
12
+ __version__ = "0.10.1"
13
13
  __author__ = "Julio"
14
14
  __license__ = "MIT"
15
15
 
@@ -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."""
@@ -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
- "default_model": "claude-sonnet-4-5-20250929",
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()
@@ -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
- model = model or self.model
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
@@ -58,7 +101,7 @@ class ClaudeClient:
58
101
  system_prompt = project_profile.system_prompt
59
102
 
60
103
  kwargs: Dict[str, Any] = {
61
- "model": model,
104
+ "model": resolved_model,
62
105
  "max_tokens": max_tokens,
63
106
  "temperature": temperature,
64
107
  "messages": [{"role": "user", "content": prompt}]
@@ -75,7 +118,7 @@ class ClaudeClient:
75
118
  self._log_usage(
76
119
  prompt=prompt,
77
120
  response=response,
78
- model=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
- model = model or self.model
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": model,
155
+ "model": resolved_model,
108
156
  "max_tokens": max_tokens,
109
157
  "temperature": temperature,
110
158
  "messages": [{"role": "user", "content": prompt}]
@@ -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(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
-
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.9.0
3
+ Version: 0.10.1
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
  [![PyPI version](https://badge.fury.io/py/claude-dev-cli.svg)](https://badge.fury.io/py/claude-dev-cli)
48
48
  [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
49
- [![Tests](https://img.shields.io/badge/tests-259%20passing-brightgreen.svg)](https://github.com/thinmanj/claude-dev-cli)
49
+ [![Tests](https://img.shields.io/badge/tests-260%20passing-brightgreen.svg)](https://github.com/thinmanj/claude-dev-cli)
50
50
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
51
51
  [![Homebrew](https://img.shields.io/badge/homebrew-available-orange.svg)](https://github.com/thinmanj/homebrew-tap)
52
52
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](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
@@ -150,7 +150,8 @@ class TestClaudeClient:
150
150
  log_entry = json.loads(f.read())
151
151
 
152
152
  assert log_entry["api_config"] == "personal"
153
- assert log_entry["model"] == "claude-3-5-sonnet-20241022"
153
+ # Should log the resolved model ID from 'smart' profile
154
+ assert log_entry["model"] == "claude-sonnet-4-5-20250929"
154
155
  assert log_entry["input_tokens"] == 100
155
156
  assert log_entry["output_tokens"] == 200
156
157
  assert "timestamp" in log_entry
@@ -8,7 +8,7 @@ from io import StringIO
8
8
  import pytest
9
9
  from rich.console import Console
10
10
 
11
- from claude_dev_cli.usage import UsageTracker, MODEL_PRICING
11
+ from claude_dev_cli.usage import UsageTracker
12
12
 
13
13
 
14
14
  class TestUsageTracker:
@@ -120,17 +120,18 @@ class TestUsageTracker:
120
120
  assert cost == expected
121
121
 
122
122
  def test_calculate_cost_haiku(self) -> None:
123
- """Test cost calculation for Haiku model."""
123
+ """Test cost calculation for Haiku model using model profiles."""
124
124
  tracker = UsageTracker()
125
125
 
126
+ # Use current Haiku model ID from default 'fast' profile
126
127
  cost = tracker._calculate_cost(
127
- "claude-3-haiku-20240307",
128
+ "claude-3-5-haiku-20241022",
128
129
  input_tokens=1_000_000,
129
130
  output_tokens=1_000_000
130
131
  )
131
132
 
132
- # Haiku: $0.25/M input, $1.25/M output
133
- expected = (1_000_000 / 1_000_000) * 0.25 + (1_000_000 / 1_000_000) * 1.25
133
+ # Fast profile (Haiku 3.5): $0.80/M input, $4.00/M output
134
+ expected = (1_000_000 / 1_000_000) * 0.80 + (1_000_000 / 1_000_000) * 4.00
134
135
  assert cost == expected
135
136
 
136
137
  def test_calculate_cost_unknown_model(self) -> None:
File without changes