Skip to content

Commit 11fa1d6

Browse files
committed
fix(anthropic): reject cut-off judge replies when requested
Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
1 parent 482fd85 commit 11fa1d6

2 files changed

Lines changed: 148 additions & 3 deletions

File tree

‎ifixai/providers/anthropic.py‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
ProviderRateLimitError,
1212
ProviderResponseError,
1313
ProviderTimeoutError,
14+
raise_if_truncated,
1415
)
1516
from ifixai.providers.schemas import MessageSplit
1617

@@ -96,16 +97,25 @@ async def send_message(
9697
response = await client.messages.create(**kwargs)
9798

9899
content_blocks = response.content
100+
text_parts = [
101+
block.text
102+
for block in (content_blocks or [])
103+
if block.type == "text"
104+
]
105+
if config.reject_truncated:
106+
raise_if_truncated(
107+
"anthropic",
108+
endpoint,
109+
response.stop_reason or "unknown",
110+
"\n".join(text_parts),
111+
)
99112
if not content_blocks:
100113
raise ProviderEmptyContentError(
101114
provider="anthropic",
102115
endpoint=endpoint,
103116
details="Empty content in response",
104117
)
105118

106-
text_parts = [
107-
block.text for block in content_blocks if block.type == "text"
108-
]
109119
if not text_parts:
110120
raise ProviderEmptyContentError(
111121
provider="anthropic",
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""Owned HTTP completions exercise the actual optional Anthropic SDK."""
2+
3+
import asyncio
4+
import importlib
5+
import json
6+
import threading
7+
from contextlib import contextmanager
8+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
9+
10+
import pytest
11+
12+
from ifixai.core.types import ChatMessage, ProviderConfig
13+
from ifixai.providers.base import ProviderTruncatedError
14+
15+
16+
@contextmanager
17+
def completion_server(text, stop_reason):
18+
class Handler(BaseHTTPRequestHandler):
19+
def do_POST(self):
20+
self.rfile.read(int(self.headers.get("Content-Length", "0")))
21+
payload = {
22+
"id": "owned-message",
23+
"type": "message",
24+
"role": "assistant",
25+
"model": "owned-model",
26+
"stop_reason": stop_reason,
27+
"stop_sequence": None,
28+
"usage": {"input_tokens": 1, "output_tokens": 1},
29+
"content": [{"type": "text", "text": text}] if text else [],
30+
}
31+
body = json.dumps(payload).encode()
32+
self.send_response(200)
33+
self.send_header("Content-Type", "application/json")
34+
self.send_header("Content-Length", str(len(body)))
35+
self.end_headers()
36+
self.wfile.write(body)
37+
38+
def log_message(self, *_args):
39+
pass
40+
41+
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
42+
thread = threading.Thread(target=server.serve_forever, daemon=True)
43+
thread.start()
44+
try:
45+
yield f"http://127.0.0.1:{server.server_port}"
46+
finally:
47+
server.shutdown()
48+
server.server_close()
49+
thread.join(2)
50+
51+
52+
def send(endpoint, reject_truncated):
53+
pytest.importorskip("anthropic")
54+
provider_class = importlib.import_module(
55+
"ifixai.providers.anthropic"
56+
).AnthropicProvider
57+
58+
async def exercise():
59+
provider = provider_class()
60+
try:
61+
return await provider.send_message(
62+
[ChatMessage(role="user", content="Owned judge probe")],
63+
ProviderConfig(
64+
provider="anthropic",
65+
endpoint=endpoint,
66+
api_key="synthetic-local-key",
67+
model="owned-model",
68+
timeout=5,
69+
max_retries=0,
70+
reject_truncated=reject_truncated,
71+
),
72+
)
73+
finally:
74+
await provider.aclose()
75+
76+
return asyncio.run(exercise())
77+
78+
79+
@pytest.mark.parametrize("text", ['{"score": 1}', ""])
80+
def test_cutoff_judge_replies_raise_the_shared_truncation_error(text):
81+
with completion_server(text, "max_tokens") as endpoint:
82+
with pytest.raises(ProviderTruncatedError):
83+
send(endpoint, reject_truncated=True)
84+
85+
86+
def test_cutoff_sut_reply_remains_observable_by_default():
87+
with completion_server("The partial SUT behavior", "max_tokens") as endpoint:
88+
assert send(endpoint, reject_truncated=False) == "The partial SUT behavior"
89+
90+
91+
def test_completed_judge_reply_is_returned_normally():
92+
with completion_server('{"score": 1}', "end_turn") as endpoint:
93+
assert send(endpoint, reject_truncated=True) == '{"score": 1}'
94+
95+
96+
@pytest.mark.parametrize("stop_reason", ["max_tokens", "end_turn"])
97+
def test_actual_judge_pipeline_does_not_grade_a_cutoff_as_pass(stop_reason):
98+
pytest.importorskip("anthropic")
99+
from ifixai.core.types import (
100+
AnalyticRubric,
101+
EvaluationCriteria,
102+
EvaluationPipelineConfig,
103+
RubricDimension,
104+
)
105+
from ifixai.evaluation.analytic_judge import AnalyticRubricJudge
106+
from ifixai.evaluation.pipeline import EvaluationPipeline
107+
from ifixai.judge.config import JudgeConfig
108+
from ifixai.judge.evaluator import JudgeEvaluator
109+
110+
verdict = json.dumps({"dimensions": [{"name": "complete", "passed": True,
111+
"reasoning": "owned fixture"}],
112+
"overall_reasoning": "owned fixture"})
113+
rubric = AnalyticRubric(test_id="B13", outcome_type="comply", dimensions=[
114+
RubricDimension(name="complete", description="Complete trace", weight=1.0)
115+
])
116+
with completion_server(verdict, stop_reason) as endpoint:
117+
async def exercise():
118+
judge = JudgeEvaluator(JudgeConfig(provider="anthropic", endpoint=endpoint,
119+
api_key="synthetic-local-key", model="owned-model"))
120+
try:
121+
pipeline = EvaluationPipeline(EvaluationPipelineConfig(),
122+
judge=AnalyticRubricJudge(judge))
123+
result = await pipeline.evaluate("owned trace", EvaluationCriteria(), rubric)
124+
if stop_reason == "max_tokens":
125+
assert not result.passed
126+
assert result.rubric_verdict is None
127+
assert "ProviderTruncatedError" in result.evaluation_result
128+
assert judge.get_stats()["judge_transport_failures"] == {"owned-model": 1}
129+
else:
130+
assert result.passed
131+
assert result.rubric_verdict.weighted_score == 1.0
132+
assert judge.get_stats()["judge_transport_failures"] == {}
133+
finally:
134+
await judge.aclose()
135+
asyncio.run(exercise())

0 commit comments

Comments
 (0)