Skip to content

Commit 1401bd3

Browse files
authored
perf: switch Investigator/Critic baseline to Opus 4.7 (#737)
1 parent b2be0e6 commit 1401bd3

3 files changed

Lines changed: 85 additions & 9 deletions

File tree

src/scripts/issue_bot/bedrock_client.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import re
23

34
import boto3
45
from botocore.config import Config as BotoConfig
@@ -8,6 +9,23 @@
89

910
_CIRCUIT_BREAKER_THRESHOLD = 3
1011

12+
# Models that reject `temperature` (and `top_p`, `top_k`) in inferenceConfig.
13+
# Opus 4.7 was the first; pattern is anchored at a non-digit boundary so we
14+
# match `claude-opus-4-7` and `claude-opus-4-7-mini-...` but not a hypothetical
15+
# `opus-4-70`. Add new families here as they drop sampling params.
16+
_NO_SAMPLING_PARAMS_PATTERN = re.compile(r"opus-4-7(?!\d)")
17+
18+
19+
def _build_inference_config(max_tokens, temperature, model_id):
20+
"""Compose inferenceConfig for converse(). Drops temperature for models
21+
that reject it; leaves the field on older Claude models so we keep the
22+
existing 0.3 sampling behavior for Reporter/Haiku/legacy."""
23+
cfg = {"maxTokens": max_tokens}
24+
if model_id and _NO_SAMPLING_PARAMS_PATTERN.search(model_id):
25+
return cfg
26+
cfg["temperature"] = temperature
27+
return cfg
28+
1129

1230
class BedrockClient:
1331
def __init__(self, cfg):
@@ -82,7 +100,7 @@ def invoke(self, system_prompt, user_prompt, max_tokens=4096,
82100
kwargs = {
83101
"modelId": self._model_id,
84102
"messages": [{"role": "user", "content": user_content}],
85-
"inferenceConfig": {"maxTokens": max_tokens, "temperature": temperature},
103+
"inferenceConfig": _build_inference_config(max_tokens, temperature, self._model_id),
86104
}
87105

88106
if system_prompt:
@@ -159,10 +177,11 @@ def invoke_with_usage(self, system_prompt, user_prompt, max_tokens=4096,
159177
user_content = [{"guardContent": {"text": {"text": user_prompt}}}]
160178
else:
161179
user_content = [{"text": user_prompt}]
180+
effective_model = model_id or self._model_id
162181
kwargs = {
163-
"modelId": model_id or self._model_id,
182+
"modelId": effective_model,
164183
"messages": [{"role": "user", "content": user_content}],
165-
"inferenceConfig": {"maxTokens": max_tokens, "temperature": temperature},
184+
"inferenceConfig": _build_inference_config(max_tokens, temperature, effective_model),
166185
}
167186
if system_prompt:
168187
kwargs["system"] = [{"text": system_prompt}]
@@ -224,20 +243,21 @@ def converse_with_tools(self, system_prompt, messages, tool_specs,
224243
Two cachePoints: one after the system block, one at the tail of the
225244
last message (so the growing conversation history is cached
226245
turn-over-turn instead of re-priced as fresh input every turn).
227-
Both Opus 4.6 and Haiku 4.5 require ≥4,096 tokens before a cachePoint
228-
for it to take effect; first-turn message-tail caches silently no-op
229-
on small payloads but become useful once tool results accumulate.
246+
Opus 4.x and Haiku 4.5 require ≥4,096 tokens before a cachePoint for
247+
it to take effect; first-turn message-tail caches silently no-op on
248+
small payloads but become useful once tool results accumulate.
230249
"""
231250
if self._circuit_open:
232251
logger.warning("Circuit breaker open, skipping Bedrock tool-use call")
233252
return None
234253
try:
235254
wrapped_messages = self._wrap_first_user_for_guardrail(messages)
236255
wrapped_messages = self._append_tail_cache_point(wrapped_messages)
256+
effective_model = model_id or self._model_id
237257
kwargs = {
238-
"modelId": model_id or self._model_id,
258+
"modelId": effective_model,
239259
"messages": wrapped_messages,
240-
"inferenceConfig": {"maxTokens": max_tokens, "temperature": temperature},
260+
"inferenceConfig": _build_inference_config(max_tokens, temperature, effective_model),
241261
}
242262
if tool_specs:
243263
kwargs["toolConfig"] = {"tools": tool_specs}

src/scripts/issue_bot/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def __init__(self):
2020
self.event_before = os.getenv("EVENT_BEFORE", "")
2121
self.event_after = os.getenv("EVENT_AFTER", "")
2222

23-
self.bedrock_model_id = os.getenv("BEDROCK_MODEL_ID", "us.anthropic.claude-opus-4-6-v1")
23+
self.bedrock_model_id = os.getenv("BEDROCK_MODEL_ID", "us.anthropic.claude-opus-4-7")
2424
# Reporter is a JSON-formatting + filter step; it doesn't reason. A
2525
# cheaper model (Haiku 4.5) handles structured output cleanly at ~5x
2626
# less cost. Defaults to bedrock_model_id so the swap is opt-in via

src/scripts/tests/test_bot.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -710,6 +710,62 @@ def test_invoke_default_no_cache_prefix(self):
710710
for block in kwargs.get("system", []):
711711
assert "cachePoint" not in block
712712

713+
def test_inference_config_drops_temperature_for_opus_4_7(self):
714+
"""Opus 4.7 returns 400 if temperature is in inferenceConfig. The
715+
helper omits it for any model_id matching the opus-4-7 marker
716+
(regional 'us.' or 'global.' inference profiles)."""
717+
from issue_bot.bedrock_client import _build_inference_config
718+
cfg = _build_inference_config(8000, 0.3, "us.anthropic.claude-opus-4-7")
719+
assert "temperature" not in cfg
720+
assert cfg["maxTokens"] == 8000
721+
722+
cfg = _build_inference_config(8000, 0.3, "global.anthropic.claude-opus-4-7")
723+
assert "temperature" not in cfg
724+
725+
def test_inference_config_keeps_temperature_for_other_models(self):
726+
from issue_bot.bedrock_client import _build_inference_config
727+
cfg = _build_inference_config(4096, 0.5, "us.anthropic.claude-opus-4-6-v1")
728+
assert cfg["temperature"] == 0.5
729+
730+
cfg = _build_inference_config(4096, 0.5, "us.anthropic.claude-haiku-4-5-20251001-v1:0")
731+
assert cfg["temperature"] == 0.5
732+
733+
def test_inference_config_handles_missing_model_id(self):
734+
"""If model_id is None or empty, fall back to including temperature
735+
(preserves legacy behavior — caller is responsible for compatibility)."""
736+
from issue_bot.bedrock_client import _build_inference_config
737+
cfg = _build_inference_config(4096, 0.3, None)
738+
assert cfg["temperature"] == 0.3
739+
cfg = _build_inference_config(4096, 0.3, "")
740+
assert cfg["temperature"] == 0.3
741+
742+
def test_converse_with_tools_drops_temperature_end_to_end_for_opus_4_7(self):
743+
"""End-to-end guard: a future refactor that bypasses _build_inference_config
744+
at one of the call sites would slip a 400 ValidationException into
745+
production. Assert the wire payload to Bedrock has no temperature when
746+
the effective model is Opus 4.7."""
747+
client = self._make_client()
748+
self._mock_converse(client)
749+
client.converse_with_tools(
750+
"system",
751+
[{"role": "user", "content": [{"text": "x"}]}],
752+
tool_specs=[],
753+
model_id="us.anthropic.claude-opus-4-7",
754+
)
755+
kwargs = client._client.converse.call_args[1]
756+
assert "temperature" not in kwargs["inferenceConfig"]
757+
assert kwargs["inferenceConfig"]["maxTokens"] > 0
758+
759+
def test_invoke_drops_temperature_for_opus_4_7_default(self):
760+
"""invoke() takes no model_id arg; uses self._model_id. When Config
761+
defaults to Opus 4.7, the wire payload must drop temperature."""
762+
client = self._make_client()
763+
client._model_id = "us.anthropic.claude-opus-4-7"
764+
self._mock_converse(client)
765+
client.invoke("system", "user")
766+
kwargs = client._client.converse.call_args[1]
767+
assert "temperature" not in kwargs["inferenceConfig"]
768+
713769
def test_converse_with_tools_total_cache_points_under_limit(self):
714770
"""Bedrock rejects requests with more than 4 cachePoints. Sum across
715771
system + every message content block to catch any future addition."""

0 commit comments

Comments
 (0)