Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Improve: Update model selection to use aliases instead of specific ve…
…rsions

Changes:
- Updated AgentExecution.tsx and CreateAgent.tsx UI to display 'Sonnet'
  and 'Opus' instead of 'Claude 4 Sonnet' and 'Claude 4 Opus'
- Enhanced pricing logic in usage.rs to recognize both model aliases
  ('opus', 'sonnet') and full model names ('claude-opus-4', 'claude-sonnet-4')
- Added case-insensitive model name matching
- Updated cc_agents/README.md to reflect proper model alias usage per
  Claude Code documentation
- Added comprehensive unit tests for calculate_cost function with alias
  support (test_calculate_cost_with_aliases, test_calculate_cost_partial_tokens)

Rationale:
Claude Code recommends using model aliases (sonnet, opus) which
automatically map to the latest model versions, avoiding hardcoded
version numbers that become outdated. This aligns with best practices
from https://docs.claude.com/en/docs/claude-code/model-config#model-aliases

Testing:
- Manually tested agent execution with both 'opus' and 'sonnet' aliases
- Both models execute successfully and display correct alias names in UI
- Unit tests added but cannot run due to pre-existing compilation error
  (now fixed in previous commit)

Backward Compatibility:
- Fully backward compatible: existing agents using full model names will
  continue to work
- Pricing calculation now supports both aliases and full names via OR logic

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
  • Loading branch information
austinbjohnson and claude committed Oct 2, 2025
commit a9f2823ffb6f7bd7c58486995996c714bb24c3f8
12 changes: 6 additions & 6 deletions cc_agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@

## 📦 Available Agents

| Agent | Model | Description | Default Task |
| Agent | Model Alias | Description | Default Task |
|-------|-------|-------------|--------------|
| **🎯 Git Commit Bot**<br/>🤖 `bot` | <img src="https://img.shields.io/badge/Sonnet-blue?style=flat-square" alt="Sonnet"> | **Automate your Git workflow with intelligent commit messages**<br/><br/>Analyzes Git repository changes, generates detailed commit messages following Conventional Commits specification, and pushes changes to remote repository. | "Push all changes." |
| **🛡️ Security Scanner**<br/>🛡️ `shield` | <img src="https://img.shields.io/badge/Opus-purple?style=flat-square" alt="Opus"> | **Advanced AI-powered Static Application Security Testing (SAST)**<br/><br/>Performs comprehensive security audits by spawning specialized sub-agents for: codebase intelligence gathering, threat modeling (STRIDE), vulnerability scanning (OWASP Top 10, CWE), exploit validation, remediation design, and professional report generation. | "Review the codebase for security issues." |
| **🧪 Unit Tests Bot**<br/>💻 `code` | <img src="https://img.shields.io/badge/Opus-purple?style=flat-square" alt="Opus"> | **Automated comprehensive unit test generation for any codebase**<br/><br/>Analyzes codebase and generates comprehensive unit tests by: analyzing code structure, creating test plans, writing tests matching your style, verifying execution, optimizing coverage (>80% overall, 100% critical paths), and generating documentation. | "Generate unit tests for this codebase." |
| **🎯 Git Commit Bot**<br/>🤖 `bot` | <img src="https://img.shields.io/badge/sonnet-blue?style=flat-square" alt="Sonnet"> | **Automate your Git workflow with intelligent commit messages**<br/><br/>Analyzes Git repository changes, generates detailed commit messages following Conventional Commits specification, and pushes changes to remote repository. Uses the `sonnet` alias to automatically use the latest Sonnet model. | "Push all changes." |
| **🛡️ Security Scanner**<br/>🛡️ `shield` | <img src="https://img.shields.io/badge/opus-purple?style=flat-square" alt="Opus"> | **Advanced AI-powered Static Application Security Testing (SAST)**<br/><br/>Performs comprehensive security audits by spawning specialized sub-agents for: codebase intelligence gathering, threat modeling (STRIDE), vulnerability scanning (OWASP Top 10, CWE), exploit validation, remediation design, and professional report generation. Uses the `opus` alias to automatically use the latest Opus model. | "Review the codebase for security issues." |
| **🧪 Unit Tests Bot**<br/>💻 `code` | <img src="https://img.shields.io/badge/opus-purple?style=flat-square" alt="Opus"> | **Automated comprehensive unit test generation for any codebase**<br/><br/>Analyzes codebase and generates comprehensive unit tests by: analyzing code structure, creating test plans, writing tests matching your style, verifying execution, optimizing coverage (>80% overall, 100% critical paths), and generating documentation. Uses the `opus` alias to automatically use the latest Opus model. | "Generate unit tests for this codebase." |

### Available Icons

Expand Down Expand Up @@ -74,7 +74,7 @@ All agents are stored in `.opcode.json` format with the following structure:
"agent": {
"name": "Your Agent Name",
"icon": "bot",
"model": "opus|sonnet|haiku",
"model": "sonnet|opus",
"system_prompt": "Your agent's instructions...",
"default_task": "Default task description"
}
Expand Down Expand Up @@ -128,7 +128,7 @@ Export your agent to a `.opcode.json` file with a descriptive name.

- **Single Purpose**: Each agent should excel at one specific task
- **Clear Documentation**: Write comprehensive system prompts
- **Model Choice**: Use Haiku for simple tasks, Sonnet for general purpose, Opus for complex reasoning
- **Model Aliases**: Use `sonnet` for general purpose tasks (fast and efficient), `opus` for complex reasoning and advanced capabilities. Model aliases automatically use the latest version of each model family.
- **Naming**: Use descriptive names that clearly indicate the agent's function

## 📜 License
Expand Down
124 changes: 104 additions & 20 deletions src-tauri/src/commands/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,26 +107,35 @@ fn calculate_cost(model: &str, usage: &UsageData) -> f64 {
let cache_creation_tokens = usage.cache_creation_input_tokens.unwrap_or(0) as f64;
let cache_read_tokens = usage.cache_read_input_tokens.unwrap_or(0) as f64;

// Calculate cost based on model
let (input_price, output_price, cache_write_price, cache_read_price) =
if model.contains("opus-4") || model.contains("claude-opus-4") {
(
OPUS_4_INPUT_PRICE,
OPUS_4_OUTPUT_PRICE,
OPUS_4_CACHE_WRITE_PRICE,
OPUS_4_CACHE_READ_PRICE,
)
} else if model.contains("sonnet-4") || model.contains("claude-sonnet-4") {
(
SONNET_4_INPUT_PRICE,
SONNET_4_OUTPUT_PRICE,
SONNET_4_CACHE_WRITE_PRICE,
SONNET_4_CACHE_READ_PRICE,
)
} else {
// Return 0 for unknown models to avoid incorrect cost estimations.
(0.0, 0.0, 0.0, 0.0)
};
// Normalize model name to lowercase for matching
let model_lower = model.to_lowercase();

// Calculate cost based on model - check for both aliases and full model names
let (input_price, output_price, cache_write_price, cache_read_price) = if model_lower
.contains("opus-4")
|| model_lower.contains("claude-opus-4")
|| model_lower == "opus"
{
(
OPUS_4_INPUT_PRICE,
OPUS_4_OUTPUT_PRICE,
OPUS_4_CACHE_WRITE_PRICE,
OPUS_4_CACHE_READ_PRICE,
)
} else if model_lower.contains("sonnet-4")
|| model_lower.contains("claude-sonnet-4")
|| model_lower == "sonnet"
{
(
SONNET_4_INPUT_PRICE,
SONNET_4_OUTPUT_PRICE,
SONNET_4_CACHE_WRITE_PRICE,
SONNET_4_CACHE_READ_PRICE,
)
} else {
// Return 0 for unknown models to avoid incorrect cost estimations.
(0.0, 0.0, 0.0, 0.0)
};

// Calculate cost (prices are per million tokens)
let cost = (input_tokens * input_price / 1_000_000.0)
Expand Down Expand Up @@ -712,3 +721,78 @@ pub fn get_session_stats(

Ok(by_session)
}

#[cfg(test)]
mod tests {
use super::*;

/// Test that calculate_cost correctly recognizes model aliases
#[test]
fn test_calculate_cost_with_aliases() {
let test_usage = UsageData {
input_tokens: Some(1_000_000), // 1M tokens
output_tokens: Some(1_000_000), // 1M tokens
cache_creation_input_tokens: Some(1_000_000), // 1M tokens
cache_read_input_tokens: Some(1_000_000), // 1M tokens
};

// Test "opus" alias
let opus_cost = calculate_cost("opus", &test_usage);
let expected_opus = 15.0 + 75.0 + 18.75 + 1.50; // Input + Output + Cache Write + Cache Read
assert_eq!(
opus_cost, expected_opus,
"Opus alias should use Opus 4 pricing"
);

// Test "sonnet" alias
let sonnet_cost = calculate_cost("sonnet", &test_usage);
let expected_sonnet = 3.0 + 15.0 + 3.75 + 0.30;
assert_eq!(
sonnet_cost, expected_sonnet,
"Sonnet alias should use Sonnet 4 pricing"
);

// Test full model names still work
let opus_full = calculate_cost("claude-opus-4-20250514", &test_usage);
assert_eq!(
opus_full, expected_opus,
"Full Opus model name should work"
);

let sonnet_full = calculate_cost("claude-sonnet-4-5-20250929", &test_usage);
assert_eq!(
sonnet_full, expected_sonnet,
"Full Sonnet model name should work"
);

// Test case insensitivity
let opus_upper = calculate_cost("OPUS", &test_usage);
assert_eq!(
opus_upper, expected_opus,
"Model names should be case insensitive"
);

// Test unknown model returns 0
let unknown_cost = calculate_cost("gpt-4", &test_usage);
assert_eq!(unknown_cost, 0.0, "Unknown models should return 0 cost");
}

/// Test calculate_cost with partial token usage
#[test]
fn test_calculate_cost_partial_tokens() {
let test_usage = UsageData {
input_tokens: Some(100_000), // 100K tokens
output_tokens: Some(50_000), // 50K tokens
cache_creation_input_tokens: None,
cache_read_input_tokens: None,
};

let opus_cost = calculate_cost("opus", &test_usage);
// 100K * 15.0 / 1M + 50K * 75.0 / 1M = 1.5 + 3.75 = 5.25
assert_eq!(opus_cost, 5.25, "Should handle partial token usage");

let sonnet_cost = calculate_cost("sonnet", &test_usage);
// 100K * 3.0 / 1M + 50K * 15.0 / 1M = 0.3 + 0.75 = 1.05
assert_eq!(sonnet_cost, 1.05, "Should handle partial token usage");
}
}
22 changes: 11 additions & 11 deletions src/components/AgentExecution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
const handleCopyAsMarkdown = async () => {
let markdown = `# Agent Execution: ${agent.name}\n\n`;
markdown += `**Task:** ${task}\n`;
markdown += `**Model:** ${model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}\n`;
markdown += `**Model:** ${model}\n`;
markdown += `**Date:** ${new Date().toISOString()}\n\n`;
markdown += `---\n\n`;

Expand Down Expand Up @@ -553,7 +553,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
<div>
<h1 className="text-heading-1">{agent.name}</h1>
<p className="mt-1 text-body-small text-muted-foreground">
{isRunning ? 'Running' : messages.length > 0 ? 'Complete' : 'Ready'} • {model === 'opus' ? 'Claude 4 Opus' : 'Claude 4 Sonnet'}
{isRunning ? 'Running' : messages.length > 0 ? 'Complete' : 'Ready'} • {model === 'opus' ? 'Opus' : 'Sonnet'}
</p>
</div>
</div>
Expand Down Expand Up @@ -600,8 +600,8 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
transition={{ duration: 0.15 }}
className={cn(
"flex-1 px-4 py-3 rounded-md border transition-all",
model === "sonnet"
? "border-primary bg-primary/10 text-primary"
model === "sonnet"
? "border-primary bg-primary/10 text-primary"
: "border-border hover:border-primary/50 hover:bg-accent",
isRunning && "opacity-50 cursor-not-allowed"
)}
Expand All @@ -617,21 +617,21 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
)}
</div>
<div className="text-left">
<div className="text-body-small font-medium">Claude 4 Sonnet</div>
<div className="text-caption text-muted-foreground">Faster, efficient</div>
<div className="text-body-small font-medium">Sonnet</div>
<div className="text-caption text-muted-foreground">Uses latest Sonnet model</div>
</div>
</div>
</motion.button>

<motion.button
type="button"
onClick={() => !isRunning && setModel("opus")}
whileTap={{ scale: 0.97 }}
transition={{ duration: 0.15 }}
className={cn(
"flex-1 px-4 py-3 rounded-md border transition-all",
model === "opus"
? "border-primary bg-primary/10 text-primary"
model === "opus"
? "border-primary bg-primary/10 text-primary"
: "border-border hover:border-primary/50 hover:bg-accent",
isRunning && "opacity-50 cursor-not-allowed"
)}
Expand All @@ -647,8 +647,8 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
)}
</div>
<div className="text-left">
<div className="text-body-small font-medium">Claude 4 Opus</div>
<div className="text-caption text-muted-foreground">More capable</div>
<div className="text-body-small font-medium">Opus</div>
<div className="text-caption text-muted-foreground">Uses latest Opus model</div>
</div>
</div>
</motion.button>
Expand Down
18 changes: 9 additions & 9 deletions src/components/CreateAgent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,8 +246,8 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
transition={{ duration: 0.15 }}
className={cn(
"flex-1 px-4 py-3 rounded-md border transition-all",
model === "sonnet"
? "border-primary bg-primary/10 text-primary"
model === "sonnet"
? "border-primary bg-primary/10 text-primary"
: "border-border hover:border-primary/50 hover:bg-accent"
)}
>
Expand All @@ -257,21 +257,21 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
model === "sonnet" ? "text-primary" : "text-muted-foreground"
)} />
<div className="text-left">
<div className="text-body-small font-medium">Claude 4 Sonnet</div>
<div className="text-caption text-muted-foreground">Faster, efficient for most tasks</div>
<div className="text-body-small font-medium">Sonnet</div>
<div className="text-caption text-muted-foreground">Uses latest Sonnet model</div>
</div>
</div>
</motion.button>

<motion.button
type="button"
onClick={() => setModel("opus")}
whileTap={{ scale: 0.97 }}
transition={{ duration: 0.15 }}
className={cn(
"flex-1 px-4 py-3 rounded-md border transition-all",
model === "opus"
? "border-primary bg-primary/10 text-primary"
model === "opus"
? "border-primary bg-primary/10 text-primary"
: "border-border hover:border-primary/50 hover:bg-accent"
)}
>
Expand All @@ -281,8 +281,8 @@ export const CreateAgent: React.FC<CreateAgentProps> = ({
model === "opus" ? "text-primary" : "text-muted-foreground"
)} />
<div className="text-left">
<div className="text-body-small font-medium">Claude 4 Opus</div>
<div className="text-caption text-muted-foreground">More capable, better for complex tasks</div>
<div className="text-body-small font-medium">Opus</div>
<div className="text-caption text-muted-foreground">Uses latest Opus model</div>
</div>
</div>
</motion.button>
Expand Down