commit 6daf60fe2f7b0613a81e577e9591a7267c7729b6
parent ccea1dfd0be889ad7c00bf5c7b1b70a97585344f
Author: Andrew Laack <andrew@laack.co>
Date: Thu, 16 Apr 2026 11:52:51 -0700
Add Claude Opus 4.7 support, make it the default (#194)
* Add Claude Opus 4.7 support and make it the default model
- Add claude-opus-4-7 and claude-opus-4-7-long to AnthropicModelName enum
- Add ModelInfo entries with pricing from Anthropic docs ($5/$25 per MTok)
- Update DEFAULT_MODEL_ID from claude-opus-4-6 to claude-opus-4-7
- Update VetConfig defaults to use CLAUDE_4_7_OPUS
- Update long-context mapping for both streaming and non-streaming paths
- Update CLI help text, skill docs, and models.json opus alias
- Opus 4.6 remains fully defined and usable via --model claude-opus-4-6
* Keep 'opus' alias on 4.6 for forwards compatibility, add 'opus-4.7' alias
The 'opus' shorthand in .vet/models.json is used by configs.toml
CI profiles. Keep it stable on 4.6 to avoid breaking existing
workflows. Add a separate 'opus-4.7' alias for explicit use.
* Fix black formatting in changed files
* Rename 4.7 to be opus
* Skip temperature parameter for Opus 4.7 (not supported)
Opus 4.7 returns 400 if temperature, top_p, or top_k are set.
Pass NOT_GIVEN for temperature on Opus 4.7 and 4.7-long models.
* Fix models.json: correct max_output_tokens, keep opus on 4.6, disable temperature for 4.7
- opus alias stays on claude-opus-4-6 (forwards compatibility)
- opus-4.7 alias added with supports_temperature: false
- Fixed max_output_tokens: sonnet/haiku 64k, opus 128k (were all 16k)
- Removed stale opus-4.6 alias (opus already points to 4.6)
* Point 'opus' alias to 4.7, add 'opus-4.6' for explicit access
opus is the latest (4.7, no temperature). Use opus-4.6 to
explicitly select the previous version.
Diffstat:
7 files changed, 74 insertions(+), 39 deletions(-)
diff --git a/.vet/models.json b/.vet/models.json
@@ -41,20 +41,26 @@
"sonnet": {
"model_id": "claude-sonnet-4-6",
"context_window": 200000,
- "max_output_tokens": 16384,
+ "max_output_tokens": 64000,
"supports_temperature": true
},
"haiku": {
"model_id": "claude-haiku-4-5",
"context_window": 200000,
- "max_output_tokens": 16384,
+ "max_output_tokens": 64000,
"supports_temperature": true
},
- "opus": {
+ "opus-4.6": {
"model_id": "claude-opus-4-6",
"context_window": 200000,
- "max_output_tokens": 16384,
+ "max_output_tokens": 128000,
"supports_temperature": true
+ },
+ "opus": {
+ "model_id": "claude-opus-4-7",
+ "context_window": 200000,
+ "max_output_tokens": 128000,
+ "supports_temperature": false
}
}
},
diff --git a/skills/vet/SKILL.md b/skills/vet/SKILL.md
@@ -99,7 +99,7 @@ Vet analyzes the full git diff from the base commit. This may include changes fr
## Common Options
- `--base-commit REF`: Git ref for diff base (default: HEAD)
-- `--model MODEL`: LLM to use (default: claude-opus-4-6)
+- `--model MODEL`: LLM to use (default: claude-opus-4-7)
- `--list-models`: list all models that are supported by vet
- Run `vet --help` and look at the vet repo's readme for details about defining custom OpenAI-compatible models.
- `--update-models`: fetch the latest community model definitions from the remote registry and cache them locally. See "Updating the Model Registry" below for when to run this.
diff --git a/uv.lock b/uv.lock
@@ -1494,7 +1494,7 @@ wheels = [
[[package]]
name = "verify-everything"
-version = "0.2.7"
+version = "0.2.9"
source = { editable = "." }
dependencies = [
{ name = "anthropic" },
diff --git a/vet/cli/main.py b/vet/cli/main.py
@@ -152,7 +152,7 @@ def create_parser() -> argparse.ArgumentParser:
default=CLI_DEFAULTS.model,
metavar="MODEL",
# Hardcoded to avoid importing cli.models at module level (~1s of SDK imports).
- help="LLM to use for analysis (default: claude-opus-4-6).",
+ help="LLM to use for analysis (default: claude-opus-4-7).",
)
model_group.add_argument(
"--list-models",
diff --git a/vet/cli/models.py b/vet/cli/models.py
@@ -12,7 +12,7 @@ from vet.imbue_core.agents.llm_apis.common import get_all_model_names
from vet.imbue_core.agents.llm_apis.gemini_api import GeminiModelName
from vet.imbue_core.agents.llm_apis.openai_api import OpenAIModelName
-DEFAULT_MODEL_ID = AnthropicModelName.CLAUDE_4_6_OPUS.value
+DEFAULT_MODEL_ID = AnthropicModelName.CLAUDE_4_7_OPUS.value
class MissingProviderAPIKeyError(Exception):
diff --git a/vet/imbue_core/agents/llm_apis/anthropic_api.py b/vet/imbue_core/agents/llm_apis/anthropic_api.py
@@ -59,6 +59,7 @@ class AnthropicModelName(enum.StrEnum):
CLAUDE_4_1_OPUS = "claude-opus-4-1"
CLAUDE_4_5_OPUS = "claude-opus-4-5"
CLAUDE_4_6_OPUS = "claude-opus-4-6"
+ CLAUDE_4_7_OPUS = "claude-opus-4-7"
CLAUDE_4_SONNET = "claude-sonnet-4-0"
CLAUDE_4_5_SONNET = "claude-sonnet-4-5"
CLAUDE_4_6_SONNET = "claude-sonnet-4-6"
@@ -69,6 +70,7 @@ class AnthropicModelName(enum.StrEnum):
CLAUDE_4_SONNET_LONG = "claude-sonnet-4-0-long"
CLAUDE_4_5_SONNET_LONG = "claude-sonnet-4-5-long"
CLAUDE_4_6_OPUS_LONG = "claude-opus-4-6-long"
+ CLAUDE_4_7_OPUS_LONG = "claude-opus-4-7-long"
# Basic info is available at https://docs.anthropic.com/claude/reference/models
@@ -138,6 +140,21 @@ ANTHROPIC_MODEL_INFO_BY_NAME: FrozenMapping[AnthropicModelName, ModelInfo] = Fro
cost_per_cache_read_token=0.50 / 1_000_000,
),
),
+ AnthropicModelName.CLAUDE_4_7_OPUS: ModelInfo(
+ model_name=AnthropicModelName.CLAUDE_4_7_OPUS,
+ cost_per_input_token=5.00 / 1_000_000,
+ cost_per_output_token=25.00 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=128_000,
+ rate_limit_req=4000 / 60,
+ rate_limit_tok=2_000_000 / 60,
+ rate_limit_output_tok=400_000 / 60,
+ provider_specific_info=AnthropicModelInfo(
+ cost_per_5m_cache_write_token=6.25 / 1_000_000,
+ cost_per_1h_cache_write_token=10 / 1_000_000,
+ cost_per_cache_read_token=0.50 / 1_000_000,
+ ),
+ ),
AnthropicModelName.CLAUDE_4_SONNET: ModelInfo(
model_name=AnthropicModelName.CLAUDE_4_SONNET,
cost_per_input_token=3.00 / 1_000_000,
@@ -237,10 +254,30 @@ ANTHROPIC_MODEL_INFO_BY_NAME: FrozenMapping[AnthropicModelName, ModelInfo] = Fro
rate_limit_tok=1_000_000 / 60,
rate_limit_output_tok=200_000 / 60,
),
+ AnthropicModelName.CLAUDE_4_7_OPUS_LONG: ModelInfo(
+ model_name=AnthropicModelName.CLAUDE_4_7_OPUS_LONG,
+ # Opus 4.7 has a native 1M context window. Pricing tiers match 4.6 long-context.
+ cost_per_input_token=9.00 / 1_000_000,
+ cost_per_output_token=37.50 / 1_000_000,
+ max_input_tokens=1_000_000,
+ max_output_tokens=128_000,
+ rate_limit_req=None, # Currently no limit set in our dashboard
+ rate_limit_tok=1_000_000 / 60,
+ rate_limit_output_tok=200_000 / 60,
+ ),
}
)
+# Opus 4.7+ does not support temperature, top_p, or top_k parameters.
+# Passing any non-default value returns a 400 error.
+_MODELS_WITHOUT_TEMPERATURE: frozenset[AnthropicModelName] = frozenset(
+ {
+ AnthropicModelName.CLAUDE_4_7_OPUS,
+ AnthropicModelName.CLAUDE_4_7_OPUS_LONG,
+ }
+)
+
_ROLE_TO_ANTHROPIC_ROLE: Final[FrozenMapping[str, str]] = FrozenDict(
{
"HUMAN": "user",
@@ -462,26 +499,22 @@ class AnthropicAPI(LanguageModelAPI):
params = chill(param_with_max_tokens_evolver)
assert params.max_tokens is not None, "max_tokens must be provided for Anthropic API"
- if self.model_name in (
- AnthropicModelName.CLAUDE_4_5_SONNET_LONG,
- AnthropicModelName.CLAUDE_4_SONNET_LONG,
- AnthropicModelName.CLAUDE_4_6_OPUS_LONG,
- ):
+ _LONG_TO_STANDARD = {
+ AnthropicModelName.CLAUDE_4_5_SONNET_LONG: AnthropicModelName.CLAUDE_4_5_SONNET,
+ AnthropicModelName.CLAUDE_4_SONNET_LONG: AnthropicModelName.CLAUDE_4_SONNET,
+ AnthropicModelName.CLAUDE_4_6_OPUS_LONG: AnthropicModelName.CLAUDE_4_6_OPUS,
+ AnthropicModelName.CLAUDE_4_7_OPUS_LONG: AnthropicModelName.CLAUDE_4_7_OPUS,
+ }
+
+ if self.model_name in _LONG_TO_STANDARD:
# FIXME: Fix this once this is no longer beta or as this becomes required for more models
# Map the name back to the actual model name for the API call
- if self.model_name == AnthropicModelName.CLAUDE_4_5_SONNET_LONG:
- model_name = AnthropicModelName.CLAUDE_4_5_SONNET
- elif self.model_name == AnthropicModelName.CLAUDE_4_SONNET_LONG:
- model_name = AnthropicModelName.CLAUDE_4_SONNET
- elif self.model_name == AnthropicModelName.CLAUDE_4_6_OPUS_LONG:
- model_name = AnthropicModelName.CLAUDE_4_6_OPUS
- else:
- assert False, "unreachable"
+ model_name = _LONG_TO_STANDARD[self.model_name]
api_result = await client.beta.messages.create(
messages=non_system_messages,
stop_sequences=([params.stop] if params.stop is not None else NOT_GIVEN),
model=model_name,
- temperature=params.temperature,
+ temperature=NOT_GIVEN if self.model_name in _MODELS_WITHOUT_TEMPERATURE else params.temperature,
system=prepend_claude_code_system_prompt(system_messages),
max_tokens=params.max_tokens,
betas=["context-1m-2025-08-07"],
@@ -495,7 +528,7 @@ class AnthropicAPI(LanguageModelAPI):
messages=non_system_messages,
stop_sequences=([params.stop] if params.stop is not None else NOT_GIVEN),
model=self.model_name,
- temperature=params.temperature,
+ temperature=NOT_GIVEN if self.model_name in _MODELS_WITHOUT_TEMPERATURE else params.temperature,
system=prepend_claude_code_system_prompt(system_messages),
max_tokens=params.max_tokens,
)
@@ -547,21 +580,17 @@ class AnthropicAPI(LanguageModelAPI):
max_tokens = params.max_tokens if params.max_tokens is not None else self.model_info.max_output_tokens
assert max_tokens is not None, "max_tokens must be provided for Anthropic API"
- if self.model_name in (
- AnthropicModelName.CLAUDE_4_5_SONNET_LONG,
- AnthropicModelName.CLAUDE_4_SONNET_LONG,
- AnthropicModelName.CLAUDE_4_6_OPUS_LONG,
- ):
+ _LONG_TO_STANDARD_STREAM = {
+ AnthropicModelName.CLAUDE_4_5_SONNET_LONG: AnthropicModelName.CLAUDE_4_5_SONNET,
+ AnthropicModelName.CLAUDE_4_SONNET_LONG: AnthropicModelName.CLAUDE_4_SONNET,
+ AnthropicModelName.CLAUDE_4_6_OPUS_LONG: AnthropicModelName.CLAUDE_4_6_OPUS,
+ AnthropicModelName.CLAUDE_4_7_OPUS_LONG: AnthropicModelName.CLAUDE_4_7_OPUS,
+ }
+
+ if self.model_name in _LONG_TO_STANDARD_STREAM:
# FIXME: Fix this once this is no longer beta or as this becomes required for more models
# Map the name back to the actual model name for the API call
- if self.model_name == AnthropicModelName.CLAUDE_4_5_SONNET_LONG:
- model_name = AnthropicModelName.CLAUDE_4_5_SONNET
- elif self.model_name == AnthropicModelName.CLAUDE_4_SONNET_LONG:
- model_name = AnthropicModelName.CLAUDE_4_SONNET
- elif self.model_name == AnthropicModelName.CLAUDE_4_6_OPUS_LONG:
- model_name = AnthropicModelName.CLAUDE_4_6_OPUS
- else:
- assert False, "unreachable"
+ model_name = _LONG_TO_STANDARD_STREAM[self.model_name]
stream_fn = lambda **kwargs: client.beta.messages.stream(**kwargs, betas=["context-1m-2025-08-07"])
cache_info_maker = lambda api_result: AnthropicCachingInfo(
written_5m=api_result.usage.cache_creation.ephemeral_5m_input_tokens,
@@ -580,7 +609,7 @@ class AnthropicAPI(LanguageModelAPI):
model=model_name,
stop_sequences=([params.stop] if params.stop is not None else NOT_GIVEN),
system=system_messages or NOT_GIVEN,
- temperature=params.temperature,
+ temperature=NOT_GIVEN if self.model_name in _MODELS_WITHOUT_TEMPERATURE else params.temperature,
) as stream:
async for text_delta in stream.text_stream:
yield LanguageModelStreamDeltaEvent(delta=text_delta)
diff --git a/vet/imbue_tools/types/vet_config.py b/vet/imbue_tools/types/vet_config.py
@@ -31,7 +31,7 @@ class VetConfig(SerializableModel):
# Todo: Different models for different issue identifiers
language_model_generation_config: LanguageModelGenerationConfig = LanguageModelGenerationConfig(
- model_name=AnthropicModelName.CLAUDE_4_6_OPUS
+ model_name=AnthropicModelName.CLAUDE_4_7_OPUS
)
max_identifier_spend_dollars: float | None = None
max_output_tokens: int = 20000
@@ -75,7 +75,7 @@ class VetConfig(SerializableModel):
cache_full_prompt: bool = False,
) -> "VetConfig":
if not language_model_name:
- language_model_name = AnthropicModelName.CLAUDE_4_6_OPUS
+ language_model_name = AnthropicModelName.CLAUDE_4_7_OPUS
language_model_generation_config = LanguageModelGenerationConfig(
model_name=language_model_name,
cache_path=language_model_cache_path,