Spaces:
Build error
Build error
tudragon154203 Claude Happy commited on
Commit ·
ae4e2cf
1
Parent(s): ae767be
fix(evals): harden suite model selection and response extraction
Browse filesHandle reasoning-only OpenAI-compatible responses correctly, avoid false PASS results on identical error strings, and let suite benchmarks honor the CLI-selected model instead of defaulting to gpt-4o-mini.
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
headroom/evals/runners/before_after.py
CHANGED
|
@@ -377,6 +377,10 @@ Answer:"""
|
|
| 377 |
response_compressed = f"ERROR: {e}"
|
| 378 |
latency_compressed = (time.time() - start) * 1000
|
| 379 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
# Compute metrics
|
| 381 |
exact_match = compute_exact_match(response_original, response_compressed)
|
| 382 |
f1_score = compute_f1(response_original, response_compressed)
|
|
@@ -400,9 +404,13 @@ Answer:"""
|
|
| 400 |
|
| 401 |
# Determine accuracy preservation
|
| 402 |
accuracy_preserved = (
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
)
|
| 407 |
|
| 408 |
return EvalResult(
|
|
|
|
| 377 |
response_compressed = f"ERROR: {e}"
|
| 378 |
latency_compressed = (time.time() - start) * 1000
|
| 379 |
|
| 380 |
+
# Detect error responses — two identical errors must not count as a pass
|
| 381 |
+
original_errored = response_original.startswith("ERROR:")
|
| 382 |
+
compressed_errored = response_compressed.startswith("ERROR:")
|
| 383 |
+
|
| 384 |
# Compute metrics
|
| 385 |
exact_match = compute_exact_match(response_original, response_compressed)
|
| 386 |
f1_score = compute_f1(response_original, response_compressed)
|
|
|
|
| 404 |
|
| 405 |
# Determine accuracy preservation
|
| 406 |
accuracy_preserved = (
|
| 407 |
+
not original_errored
|
| 408 |
+
and not compressed_errored
|
| 409 |
+
and (
|
| 410 |
+
f1_score > 0.7
|
| 411 |
+
or (semantic_sim is not None and semantic_sim > 0.85)
|
| 412 |
+
or contains_ground_truth is True
|
| 413 |
+
)
|
| 414 |
)
|
| 415 |
|
| 416 |
return EvalResult(
|
headroom/evals/runners/utils.py
CHANGED
|
@@ -9,11 +9,15 @@ from typing import Any
|
|
| 9 |
def extract_openai_text(response: Any) -> str:
|
| 10 |
"""Extract text content from an OpenAI-compatible chat completion response.
|
| 11 |
|
| 12 |
-
Handles
|
| 13 |
1. Standard: ``message.content`` is a non-empty string.
|
| 14 |
-
2. Content-part list: ``message.content`` is a list of typed blocks (vision
|
| 15 |
-
3. Reasoning
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
Raises:
|
| 19 |
ValueError: When the response carries no usable text in any of the above forms.
|
|
@@ -21,9 +25,13 @@ def extract_openai_text(response: Any) -> str:
|
|
| 21 |
if not response.choices:
|
| 22 |
raise ValueError("model returned no choices")
|
| 23 |
message = response.choices[0].message
|
|
|
|
|
|
|
| 24 |
content = getattr(message, "content", None)
|
| 25 |
if isinstance(content, str) and content.strip():
|
| 26 |
return content
|
|
|
|
|
|
|
| 27 |
if isinstance(content, list):
|
| 28 |
text_parts = [
|
| 29 |
part.text
|
|
@@ -32,9 +40,25 @@ def extract_openai_text(response: Any) -> str:
|
|
| 32 |
]
|
| 33 |
if text_parts:
|
| 34 |
return "\n".join(text_parts)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
reasoning = getattr(message, "reasoning", None)
|
| 36 |
if isinstance(reasoning, str) and reasoning.strip():
|
| 37 |
match = re.search(r"```(?:python)?\s*(.*?)```", reasoning, re.DOTALL)
|
| 38 |
if match:
|
| 39 |
-
return match.group(
|
|
|
|
|
|
|
|
|
|
| 40 |
raise ValueError("model returned no content")
|
|
|
|
| 9 |
def extract_openai_text(response: Any) -> str:
|
| 10 |
"""Extract text content from an OpenAI-compatible chat completion response.
|
| 11 |
|
| 12 |
+
Handles shapes returned by different providers / models:
|
| 13 |
1. Standard: ``message.content`` is a non-empty string.
|
| 14 |
+
2. Content-part list: ``message.content`` is a list of typed blocks (vision).
|
| 15 |
+
3. Reasoning models (Step 3.7 Flash, GLM via OpenRouter):
|
| 16 |
+
``message.content`` is null but ``message.reasoning_content``
|
| 17 |
+
or ``message.reasoning`` holds the actual answer — extract it directly,
|
| 18 |
+
with code-block extraction as a fallback.
|
| 19 |
+
4. Reasoning models that embed the final answer in a code block within
|
| 20 |
+
``message.reasoning``.
|
| 21 |
|
| 22 |
Raises:
|
| 23 |
ValueError: When the response carries no usable text in any of the above forms.
|
|
|
|
| 25 |
if not response.choices:
|
| 26 |
raise ValueError("model returned no choices")
|
| 27 |
message = response.choices[0].message
|
| 28 |
+
|
| 29 |
+
# Shape 1: standard string content
|
| 30 |
content = getattr(message, "content", None)
|
| 31 |
if isinstance(content, str) and content.strip():
|
| 32 |
return content
|
| 33 |
+
|
| 34 |
+
# Shape 2: content-part list (vision models)
|
| 35 |
if isinstance(content, list):
|
| 36 |
text_parts = [
|
| 37 |
part.text
|
|
|
|
| 40 |
]
|
| 41 |
if text_parts:
|
| 42 |
return "\n".join(text_parts)
|
| 43 |
+
|
| 44 |
+
# Shape 3: reasoning_content (Step 3.7 Flash / some NVIDIA NIM models)
|
| 45 |
+
# Use as direct answer — may be the full response with no code blocks.
|
| 46 |
+
reasoning_content = getattr(message, "reasoning_content", None)
|
| 47 |
+
if isinstance(reasoning_content, str) and reasoning_content.strip():
|
| 48 |
+
# Try code-block extraction first
|
| 49 |
+
match = re.search(r"```(?:python)?\s*(.*?)```", reasoning_content, re.DOTALL)
|
| 50 |
+
if match:
|
| 51 |
+
return match.group(1).strip()
|
| 52 |
+
# No code block — use reasoning_content as-is (full answer)
|
| 53 |
+
return reasoning_content.strip()
|
| 54 |
+
|
| 55 |
+
# Shape 4: reasoning (OpenRouter reasoning models)
|
| 56 |
reasoning = getattr(message, "reasoning", None)
|
| 57 |
if isinstance(reasoning, str) and reasoning.strip():
|
| 58 |
match = re.search(r"```(?:python)?\s*(.*?)```", reasoning, re.DOTALL)
|
| 59 |
if match:
|
| 60 |
+
return match.group(1).strip()
|
| 61 |
+
# Fall back to reasoning text itself as answer
|
| 62 |
+
return reasoning.strip()
|
| 63 |
+
|
| 64 |
raise ValueError("model returned no content")
|
headroom/evals/suite_runner.py
CHANGED
|
@@ -38,7 +38,7 @@ class BenchmarkSpec:
|
|
| 38 |
tier: int
|
| 39 |
runner_type: Literal["lm_eval", "before_after", "compression_only", "livecodebench"]
|
| 40 |
sample_size: int
|
| 41 |
-
model: str =
|
| 42 |
dataset_name: str | None = None # For before_after runner
|
| 43 |
lm_eval_tasks: list[str] | None = None # For lm_eval runner
|
| 44 |
primary_metric: str = "accuracy"
|
|
@@ -372,6 +372,12 @@ class SuiteRunner:
|
|
| 372 |
return self._proxy_proc is not None
|
| 373 |
return False
|
| 374 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
def _cleanup_proxy(self) -> None:
|
| 376 |
"""Stop proxy if we started it."""
|
| 377 |
if self._proxy_proc:
|
|
@@ -456,9 +462,7 @@ class SuiteRunner:
|
|
| 456 |
suite = load_dataset_by_name(spec.dataset_name or spec.name.lower(), n=spec.sample_size)
|
| 457 |
|
| 458 |
# Configure runner — use proxy for full-stack eval (compression + CCR)
|
| 459 |
-
proxy_url = (
|
| 460 |
-
f"http://localhost:{self.headroom_port}" if _check_proxy(self.headroom_port) else None
|
| 461 |
-
)
|
| 462 |
runner = BeforeAfterRunner(
|
| 463 |
llm_config=LLMConfig(
|
| 464 |
provider=spec.provider,
|
|
@@ -506,14 +510,14 @@ class SuiteRunner:
|
|
| 506 |
from headroom.evals.runners.livecodebench_runner import LiveCodeBenchRunner
|
| 507 |
|
| 508 |
suite = load_dataset_by_name(spec.dataset_name or "livecodebench", n=spec.sample_size)
|
|
|
|
|
|
|
| 509 |
runner = LiveCodeBenchRunner(
|
| 510 |
llm_config=LLMConfig(
|
| 511 |
provider=spec.provider,
|
| 512 |
model=spec.model or self.model,
|
| 513 |
temperature=0.0,
|
| 514 |
-
headroom_proxy_url=
|
| 515 |
-
f"http://localhost:{self.headroom_port}" if _check_proxy(self.headroom_port) else None
|
| 516 |
-
),
|
| 517 |
)
|
| 518 |
)
|
| 519 |
result = runner.run(suite)
|
|
|
|
| 38 |
tier: int
|
| 39 |
runner_type: Literal["lm_eval", "before_after", "compression_only", "livecodebench"]
|
| 40 |
sample_size: int
|
| 41 |
+
model: str | None = None
|
| 42 |
dataset_name: str | None = None # For before_after runner
|
| 43 |
lm_eval_tasks: list[str] | None = None # For lm_eval runner
|
| 44 |
primary_metric: str = "accuracy"
|
|
|
|
| 372 |
return self._proxy_proc is not None
|
| 373 |
return False
|
| 374 |
|
| 375 |
+
def _get_proxy_url(self) -> str | None:
|
| 376 |
+
"""Return proxy URL if a Headroom proxy is reachable, else None."""
|
| 377 |
+
if _check_proxy(self.headroom_port):
|
| 378 |
+
return f"http://localhost:{self.headroom_port}"
|
| 379 |
+
return None
|
| 380 |
+
|
| 381 |
def _cleanup_proxy(self) -> None:
|
| 382 |
"""Stop proxy if we started it."""
|
| 383 |
if self._proxy_proc:
|
|
|
|
| 462 |
suite = load_dataset_by_name(spec.dataset_name or spec.name.lower(), n=spec.sample_size)
|
| 463 |
|
| 464 |
# Configure runner — use proxy for full-stack eval (compression + CCR)
|
| 465 |
+
proxy_url = self._get_proxy_url()
|
|
|
|
|
|
|
| 466 |
runner = BeforeAfterRunner(
|
| 467 |
llm_config=LLMConfig(
|
| 468 |
provider=spec.provider,
|
|
|
|
| 510 |
from headroom.evals.runners.livecodebench_runner import LiveCodeBenchRunner
|
| 511 |
|
| 512 |
suite = load_dataset_by_name(spec.dataset_name or "livecodebench", n=spec.sample_size)
|
| 513 |
+
proxy_url = self._get_proxy_url()
|
| 514 |
+
|
| 515 |
runner = LiveCodeBenchRunner(
|
| 516 |
llm_config=LLMConfig(
|
| 517 |
provider=spec.provider,
|
| 518 |
model=spec.model or self.model,
|
| 519 |
temperature=0.0,
|
| 520 |
+
headroom_proxy_url=proxy_url,
|
|
|
|
|
|
|
| 521 |
)
|
| 522 |
)
|
| 523 |
result = runner.run(suite)
|
tests/test_evals/test_eval_runners.py
CHANGED
|
@@ -148,11 +148,18 @@ def test_livecodebench_extract_openai_text_from_reasoning_code_block() -> None:
|
|
| 148 |
message = SimpleNamespace(content=None, reasoning="```python\nprint(123)\n```")
|
| 149 |
response = SimpleNamespace(choices=[SimpleNamespace(message=message)])
|
| 150 |
|
| 151 |
-
assert extract_openai_text(response) == "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
|
| 154 |
def test_livecodebench_extract_openai_text_raises_on_empty_response() -> None:
|
| 155 |
-
message = SimpleNamespace(content=None, reasoning=None)
|
| 156 |
response = SimpleNamespace(choices=[SimpleNamespace(message=message)])
|
| 157 |
|
| 158 |
with pytest.raises(ValueError, match="model returned no content"):
|
|
|
|
| 148 |
message = SimpleNamespace(content=None, reasoning="```python\nprint(123)\n```")
|
| 149 |
response = SimpleNamespace(choices=[SimpleNamespace(message=message)])
|
| 150 |
|
| 151 |
+
assert extract_openai_text(response) == "print(123)"
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def test_livecodebench_extract_openai_text_from_reasoning_content() -> None:
|
| 155 |
+
message = SimpleNamespace(content=None, reasoning=None, reasoning_content="print(123)")
|
| 156 |
+
response = SimpleNamespace(choices=[SimpleNamespace(message=message)])
|
| 157 |
+
|
| 158 |
+
assert extract_openai_text(response) == "print(123)"
|
| 159 |
|
| 160 |
|
| 161 |
def test_livecodebench_extract_openai_text_raises_on_empty_response() -> None:
|
| 162 |
+
message = SimpleNamespace(content=None, reasoning=None, reasoning_content=None)
|
| 163 |
response = SimpleNamespace(choices=[SimpleNamespace(message=message)])
|
| 164 |
|
| 165 |
with pytest.raises(ValueError, match="model returned no content"):
|