Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **Default "capable" model is now Claude Sonnet 5
(`claude-sonnet-5`).** Replaces `claude-sonnet-4-6` across the model
registry, adaptive routing, providers, telemetry, and token
estimation. Pricing is unchanged ($3/$15 per MTok standard;
introductory $2/$10 through 2026-08-31). 1M context, 128K max output,
adaptive thinking; `effort` defaults to `high`.

## [9.3.0] — 2026-06-30

**Dynamic communication to improve the human/AI exchange.** Attune now
Expand Down
4 changes: 2 additions & 2 deletions src/attune/agent_factory/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,11 @@ def get_model_for_tier(self, tier: str, provider: str = "anthropic") -> str:
defaults = {
"anthropic": {
"cheap": "claude-haiku-4-5",
"capable": "claude-sonnet-4-6",
"capable": "claude-sonnet-5",
"premium": "claude-opus-4-8",
},
}
return defaults.get(provider, defaults["anthropic"]).get(
tier,
"claude-sonnet-4-6",
"claude-sonnet-5",
)
2 changes: 1 addition & 1 deletion src/attune/agents/release/release_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@

MODEL_CONFIG = {
"cheap": "claude-haiku-4-5",
"capable": "claude-sonnet-4-6",
"capable": "claude-sonnet-5",
"premium": "claude-opus-4-8",
}

Expand Down
2 changes: 1 addition & 1 deletion src/attune/cli_commands/memory_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

# CAPABLE-tier stable alias — tracks the latest Sonnet minor, never retires
# (avoids pinning a dated snapshot that EOLs).
_DEFAULT_MODEL = "claude-sonnet-4-6"
_DEFAULT_MODEL = "claude-sonnet-5"
# Required beta header for the memory_20250818 tool + context management.
_MEMORY_BETA = "context-management-2025-06-27"
_DEFAULT_MAX_TOKENS = 4096
Expand Down
2 changes: 1 addition & 1 deletion src/attune/cli_minimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ def _add_misc_subparsers(subparsers: argparse._SubParsersAction) -> None:
"--path", help="Memory namespace root the agent addresses (default /memories)"
)
memory_agent_parser.add_argument(
"--model", help="Model id (default: claude-sonnet-4-6, the CAPABLE tier)"
"--model", help="Model id (default: claude-sonnet-5, the CAPABLE tier)"
)
memory_agent_parser.add_argument("--user-id", dest="user_id", help="Namespace memory per user")
memory_agent_parser.add_argument(
Expand Down
4 changes: 2 additions & 2 deletions src/attune/config/agent_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,14 @@ def get_model_id(self) -> str:
models = {
Provider.ANTHROPIC: {
ModelTier.CHEAP: "claude-haiku-4-5",
ModelTier.CAPABLE: "claude-sonnet-4-6",
ModelTier.CAPABLE: "claude-sonnet-5",
ModelTier.PREMIUM: "claude-opus-4-8",
},
}

return models.get(self.provider, {}).get(
self.model_tier,
"claude-sonnet-4-6", # Fallback
"claude-sonnet-5", # Fallback
)

def for_book_production(self) -> "BookProductionConfig":
Expand Down
4 changes: 2 additions & 2 deletions src/attune/config/sections/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class RoutingConfig:

default_tier: Literal["cheap", "capable", "premium"] = "capable"
cheap_model: str = "claude-haiku-4-5"
capable_model: str = "claude-sonnet-4-6"
capable_model: str = "claude-sonnet-5"
premium_model: str = "claude-opus-4-8"
auto_tier_selection: bool = True
cost_optimization: bool = True
Expand Down Expand Up @@ -67,7 +67,7 @@ def from_dict(cls, data: dict) -> "RoutingConfig":
return cls(
default_tier=data.get("default_tier", "capable"),
cheap_model=data.get("cheap_model", "claude-haiku-4-5"),
capable_model=data.get("capable_model", "claude-sonnet-4-6"),
capable_model=data.get("capable_model", "claude-sonnet-5"),
premium_model=data.get("premium_model", "claude-opus-4-8"),
auto_tier_selection=data.get("auto_tier_selection", True),
cost_optimization=data.get("cost_optimization", True),
Expand Down
4 changes: 2 additions & 2 deletions src/attune/config/xml_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ class AdaptiveConfig:
model_tier_mapping: dict[str, str] = field(
default_factory=lambda: {
"simple": "claude-haiku-4-5",
"moderate": "claude-sonnet-4-6",
"complex": "claude-sonnet-4-6",
"moderate": "claude-sonnet-5",
"complex": "claude-sonnet-5",
"very_complex": "claude-opus-4-8",
},
)
Expand Down
2 changes: 1 addition & 1 deletion src/attune/help/polish.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def _call_llm(
# Stable alias — claude-sonnet-4-20250514 retires 2026-06-15.
# The stable alias routes to the same checkpoint and stays
# valid across model upgrades.
model="claude-sonnet-4-6",
model="claude-sonnet-5",
max_tokens=4096,
system=_SYSTEM_PROMPT,
messages=[{"role": "user", "content": user_message}],
Expand Down
2 changes: 1 addition & 1 deletion src/attune/llm/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ def _create_provider(
if provider == "anthropic":
return AnthropicProvider(
api_key=api_key,
model=model or "claude-sonnet-4-6",
model=model or "claude-sonnet-5",
**kwargs,
)
raise ValueError(f"Unknown provider: {provider}")
Expand Down
4 changes: 2 additions & 2 deletions src/attune/llm/providers/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class AnthropicProvider(BaseLLMProvider):
def __init__(
self,
api_key: str | None = None,
model: str = "claude-sonnet-4-6",
model: str = "claude-sonnet-5",
use_prompt_caching: bool = False, # Deprecated
use_thinking: bool = False, # Deprecated
thinking_budget: int = 10000,
Expand Down Expand Up @@ -411,7 +411,7 @@ def get_model_info(self) -> dict[str, Any]:
"supports_thinking": True,
"ideal_for": "Complex reasoning, large codebases",
},
"claude-sonnet-4-6": {
"claude-sonnet-5": {
"max_tokens": 200000,
"cost_per_1m_input": 3.00,
"cost_per_1m_output": 15.00,
Expand Down
4 changes: 2 additions & 2 deletions src/attune/llm/providers/anthropic_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def create_batch(self, requests: list[dict[str, Any]], job_id: str | None = None
... {
... "custom_id": "task_1",
... "params": {
... "model": "claude-sonnet-4-6",
... "model": "claude-sonnet-5",
... "messages": [{"role": "user", "content": "Test"}],
... "max_tokens": 1024
... }
Expand All @@ -99,7 +99,7 @@ def create_batch(self, requests: list[dict[str, Any]], job_id: str | None = None
formatted_req = {
"custom_id": req.get("custom_id", f"req_{id(req)}"),
"params": {
"model": req.get("model", "claude-sonnet-4-6"),
"model": req.get("model", "claude-sonnet-5"),
"messages": req.get("messages", []),
"max_tokens": req.get("max_tokens", 4096),
},
Expand Down
2 changes: 1 addition & 1 deletion src/attune/metrics/prompt_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class PromptMetrics:
workflow: Workflow name (e.g., "code_review", "bug_predict")
agent_role: Role of the agent (e.g., "Code Reviewer")
task_description: Brief description of task (truncated to 100 chars)
model: Model name (e.g., "claude-sonnet-4-6", "claude-haiku-4-5")
model: Model name (e.g., "claude-sonnet-5", "claude-haiku-4-5")
prompt_tokens: Number of input tokens
completion_tokens: Number of output tokens
total_tokens: Total tokens (input + output)
Expand Down
2 changes: 1 addition & 1 deletion src/attune/models/adaptive_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def _get_default_model(self, tier: str = "CHEAP") -> str:
# Fallback to known models if registry lookup fails
fallbacks = {
"cheap": "claude-haiku-4-5",
"capable": "claude-sonnet-4-6",
"capable": "claude-sonnet-5",
"premium": "claude-opus-4-8",
}
return fallbacks.get(tier_lower, "claude-haiku-4-5")
Expand Down
2 changes: 1 addition & 1 deletion src/attune/models/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class LLMResponse:

Attributes:
content: The LLM response text
model_id: Model identifier (e.g., "claude-sonnet-4-6")
model_id: Model identifier (e.g., "claude-sonnet-5")
provider: Provider name (e.g., "anthropic")
tier: Model tier ("cheap", "capable", "premium")
tokens_input: Number of input tokens used
Expand Down
15 changes: 9 additions & 6 deletions src/attune/models/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def to_cost_tracker_pricing(self) -> dict[str, float]:
MODEL_REGISTRY: dict[str, dict[str, ModelInfo]] = {
# -------------------------------------------------------------------------
# Anthropic Claude Models
# Intelligent fallback: Sonnet 4.6 → Opus 4.8 (~1.67x input cost: $3 → $5/M)
# Intelligent fallback: Sonnet 5 → Opus 4.8 (~1.67x input cost: $3 → $5/M)
# -------------------------------------------------------------------------
"anthropic": {
"cheap": ModelInfo(
Expand All @@ -139,7 +139,7 @@ def to_cost_tracker_pricing(self) -> dict[str, float]:
supports_tools=True,
),
"capable": ModelInfo(
id="claude-sonnet-4-6",
id="claude-sonnet-5",
provider="anthropic",
tier="capable",
input_cost_per_million=3.00,
Expand Down Expand Up @@ -177,7 +177,7 @@ class ModelRegistry:
>>> registry = ModelRegistry()
>>> model = registry.get_model("anthropic", "capable")
>>> print(model.id)
claude-sonnet-4-6
claude-sonnet-5

>>> models = registry.get_models_by_tier("cheap")
>>> print(len(models))
Expand Down Expand Up @@ -239,7 +239,7 @@ def get_model(self, provider: str, tier: str) -> ModelInfo | None:
>>> registry = ModelRegistry()
>>> model = registry.get_model("anthropic", "capable")
>>> print(model.id)
claude-sonnet-4-6
claude-sonnet-5

"""
if provider.lower() != "anthropic":
Expand Down Expand Up @@ -360,7 +360,7 @@ def get_pricing_for_model(self, model_id: str) -> dict[str, float] | None:

Example:
>>> registry = ModelRegistry()
>>> pricing = registry.get_pricing_for_model("claude-sonnet-4-6")
>>> pricing = registry.get_pricing_for_model("claude-sonnet-5")
>>> print(pricing)
{'input': 3.0, 'output': 15.0}

Expand Down Expand Up @@ -463,6 +463,9 @@ def get_tiers() -> list[str]:

TIER_PRICING: dict[str, dict[str, float]] = {
"cheap": {"input": 1.00, "output": 5.00}, # Haiku 4.5 pricing
"capable": {"input": 3.00, "output": 15.00}, # Sonnet 4.6 pricing
"capable": {
"input": 3.00,
"output": 15.00,
}, # Sonnet 5 pricing (std; intro $2/$10 until 2026-08-31)
"premium": {"input": 5.00, "output": 25.00}, # Opus 4.8 pricing
}
4 changes: 2 additions & 2 deletions src/attune/models/telemetry/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def sonnet_opus_fallback_analysis(
c
for c in calls
if c.provider == "anthropic"
and c.model_id in ["claude-sonnet-4-6", "claude-opus-4-6", "claude-opus-4-8"]
and c.model_id in ["claude-sonnet-5", "claude-opus-4-6", "claude-opus-4-8"]
]

if not anthropic_calls:
Expand All @@ -222,7 +222,7 @@ def sonnet_opus_fallback_analysis(
total = len(anthropic_calls)

# Count Sonnet attempts and successes
sonnet_calls = [c for c in anthropic_calls if c.model_id == "claude-sonnet-4-6"]
sonnet_calls = [c for c in anthropic_calls if c.model_id == "claude-sonnet-5"]
sonnet_successes = sum(1 for c in sonnet_calls if c.success)

# Count Opus fallbacks (calls with fallback_used and ended up on Opus)
Expand Down
2 changes: 1 addition & 1 deletion src/attune/models/token_estimator.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def _get_encoding(model_id: str) -> Any:
return tiktoken.get_encoding("cl100k_base")


def estimate_tokens(text: str, model_id: str = "claude-sonnet-4-6") -> int:
def estimate_tokens(text: str, model_id: str = "claude-sonnet-5") -> int:
"""Estimate token count for text using accurate token counting.

Uses empathy_llm_toolkit's token counter which leverages tiktoken for fast,
Expand Down
2 changes: 1 addition & 1 deletion src/attune/orchestration/tools/test_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ class Foo:
# latest Sonnet checkpoint. The old claude-3-5-sonnet-2024xxxx
# dated snapshots were retired by Anthropic 2025-10-28.
models_to_try = [
"claude-sonnet-4-6", # Sonnet 4.6 (stable alias)
"claude-sonnet-5", # Sonnet 5 (stable alias)
]

response = None
Expand Down
2 changes: 1 addition & 1 deletion src/attune/orchestration/tools/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ class Foo:
# latest Sonnet checkpoint. The old claude-3-5-sonnet-2024xxxx
# dated snapshots were retired by Anthropic 2025-10-28.
models_to_try = [
"claude-sonnet-4-6", # Sonnet 4.6 (stable alias)
"claude-sonnet-5", # Sonnet 5 (stable alias)
]

response = None
Expand Down
2 changes: 1 addition & 1 deletion src/attune/routing/model_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ def route(
>>> router.route("summarize")
'claude-haiku-4-5'
>>> router.route("fix_bug")
'claude-sonnet-4-6'
'claude-sonnet-5'
>>> router.route("coordinate")
'claude-opus-4-8'

Expand Down
4 changes: 2 additions & 2 deletions src/attune/template_defs_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
provider: "anthropic"
models:
cheap: "claude-haiku-4-5"
capable: "claude-sonnet-4-6"
capable: "claude-sonnet-5"
premium: "claude-opus-4-8"

# Claude Code integration
Expand Down Expand Up @@ -82,7 +82,7 @@
provider: "anthropic"
models:
cheap: "claude-haiku-4-5"
capable: "claude-sonnet-4-6"
capable: "claude-sonnet-5"
premium: "claude-opus-4-8"

claude_sync:
Expand Down
4 changes: 2 additions & 2 deletions src/attune/template_defs_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
provider: "anthropic"
models:
cheap: "claude-haiku-4-5"
capable: "claude-sonnet-4-6"
capable: "claude-sonnet-5"
premium: "claude-opus-4-8"

claude_sync:
Expand Down Expand Up @@ -199,7 +199,7 @@ async def health():
provider: "anthropic"
models:
cheap: "claude-haiku-4-5"
capable: "claude-sonnet-4-6"
capable: "claude-sonnet-5"
premium: "claude-opus-4-8"
task_overrides:
summarize: "cheap"
Expand Down
8 changes: 4 additions & 4 deletions src/attune/utils/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def _count_tokens_heuristic(text: str) -> int:
return max(1, len(text) // 4)


def count_tokens(text: str, model: str = "claude-sonnet-4-6", use_api: bool = False) -> int:
def count_tokens(text: str, model: str = "claude-sonnet-5", use_api: bool = False) -> int:
"""Count tokens using best available method.

By default, uses tiktoken for fast local estimation (~98% accurate).
Expand Down Expand Up @@ -149,7 +149,7 @@ def count_tokens(text: str, model: str = "claude-sonnet-4-6", use_api: bool = Fa
def count_message_tokens(
messages: list[dict[str, str]],
system_prompt: str | None = None,
model: str = "claude-sonnet-4-6",
model: str = "claude-sonnet-5",
use_api: bool = False,
) -> dict[str, int]:
"""Count tokens in a conversation.
Expand Down Expand Up @@ -231,7 +231,7 @@ def count_message_tokens(
return counts


def estimate_cost(input_tokens: int, output_tokens: int, model: str = "claude-sonnet-4-6") -> float:
def estimate_cost(input_tokens: int, output_tokens: int, model: str = "claude-sonnet-5") -> float:
"""Estimate cost in USD based on token counts.

Args:
Expand Down Expand Up @@ -275,7 +275,7 @@ def calculate_cost_with_cache(
output_tokens: int,
cache_creation_tokens: int,
cache_read_tokens: int,
model: str = "claude-sonnet-4-6",
model: str = "claude-sonnet-5",
) -> dict[str, Any]:
"""Calculate cost including Anthropic prompt caching.

Expand Down
6 changes: 3 additions & 3 deletions src/attune/workflows/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ def create_example_config() -> str:
custom_models:
anthropic:
cheap: claude-haiku-4-5
capable: claude-sonnet-4-6
capable: claude-sonnet-5
premium: claude-opus-4-8
openai:
cheap: gpt-4o-mini
Expand All @@ -573,7 +573,7 @@ def create_example_config() -> str:
# Create your own hybrid mix:
hybrid:
cheap: gpt-4o-mini # OpenAI - cheapest per token
capable: claude-sonnet-4-6 # Anthropic - best code/reasoning
capable: claude-sonnet-5 # Anthropic - best code/reasoning
premium: claude-opus-4-8 # Anthropic - best overall

# =============================================================================
Expand Down Expand Up @@ -654,6 +654,6 @@ def create_example_config() -> str:
# EMPATHY_WORKFLOW_PROVIDER=hybrid # Default provider
# EMPATHY_WORKFLOW_RESEARCH_PROVIDER=anthropic # Per-workflow
# EMPATHY_MODEL_CHEAP=gpt-4o-mini # Tier model override
# EMPATHY_MODEL_CAPABLE=claude-sonnet-4-6
# EMPATHY_MODEL_CAPABLE=claude-sonnet-5
# EMPATHY_MODEL_PREMIUM=claude-opus-4-8
"""
Loading
Loading