"""Model layer tests: spec parsing, adapters, async runner end-to-end. Run: .venv/bin/python tests/test_model.py """ import asyncio import json import sys import threading from http.server import BaseHTTPRequestHandler, HTTPServer from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) from evalharness.data.sample import Sample, ToolInfo # noqa: E402 from evalharness.model import MockAdapter, OpenAICompatible, resolve_adapter, run_eval # noqa: E402 from evalharness.model.adapter import parse_model_spec # noqa: E402 from evalharness.model.runner import _make_adapter # noqa: E402 def test_parse_spec(): got = parse_model_spec('openai/http://gpu03:8000/v1?qwen3-8b') assert got == {'adapter': 'openai', 'api_base': 'http://gpu03:8000/v1', 'model': 'qwen3-8b'} assert parse_model_spec('mock') == {'adapter': 'mock', 'api_base': '', 'model': ''} def test_make_adapter_mock_modes(): a = _make_adapter('mock:boxed') assert isinstance(a, MockAdapter) and a.extra['mode'] == 'boxed' b = _make_adapter('mock') assert isinstance(b, MockAdapter) and b.extra.get('mode', 'echo') == 'echo' c = _make_adapter('openai/http://127.0.0.1:9/v1?m') assert isinstance(c, OpenAICompatible) and c.api_base == 'http://127.0.0.1:9/v1' and c.model == 'm' def test_mock_tool_mode(): out = asyncio.run(MockAdapter().generate([Sample(input='x').input and __import__('evalharness.data.sample', fromlist=['ChatMessage']).ChatMessage(role='user', content='x')])) assert out.text == 'x' def _fake_server(): class Fake(BaseHTTPRequestHandler): def do_POST(self): body = json.loads(self.rfile.read(int(self.headers['Content-Length']))) msg = body['messages'][-1]['content'] tool_calls = [] if body.get('tools'): tool_calls = [{'id': 'c1', 'function': {'name': 't1', 'arguments': '{"x": 1}'}}] resp = {'choices': [{'message': {'content': f'echo:{msg}', 'tool_calls': tool_calls}, 'finish_reason': 'tool_calls' if tool_calls else 'stop'}], 'usage': {'prompt_tokens': 3, 'completion_tokens': 5, 'total_tokens': 8}, 'model': body['model']} data = json.dumps(resp).encode() self.send_response(200) self.send_header('Content-Type', 'application/json') self.send_header('Content-Length', str(len(data))) self.end_headers() self.wfile.write(data) def log_message(self, *a): pass srv = HTTPServer(('127.0.0.1', 0), Fake) threading.Thread(target=srv.serve_forever, daemon=True).start() return srv def test_openai_adapter_roundtrip(): srv = _fake_server() port = srv.server_address[1] try: rep = asyncio.run(run_eval([Sample(input='hello', target='echo:hello')], f'openai/http://127.0.0.1:{port}/v1?fake-m', progress=False)) r = rep.samples[0] assert 'echo:hello' in r.raw_prediction assert r.usage['total_tokens'] == 8 finally: srv.shutdown() def test_tools_pass_through(): srv = _fake_server() port = srv.server_address[1] try: s = Sample(input='call it', tools=[ToolInfo(name='f1', parameters={'type': 'object'})]) rep = asyncio.run(run_eval([s], f'openai/http://127.0.0.1:{port}/v1?fake-m', progress=False)) assert '"t1"' in rep.samples[0].raw_prediction # tool call serialized into prediction finally: srv.shutdown() def test_oracle_mock_pipeline(): from evalharness import get_dataset ds = get_dataset('gsm8k') rep = asyncio.run(run_eval(ds, 'mock:boxed', limit=50, concurrency=8, progress=False)) assert rep.metrics['acc'] == 1.0 assert rep.dataset == 'gsm8k' assert rep.metric_groups['run_info']['gen_total_tokens'] == 100 if __name__ == '__main__': fails = 0 for name, fn in sorted({k: v for k, v in globals().items() if k.startswith('test_') and callable(v)}.items()): try: fn() print(f'PASS {name}') except AssertionError as e: fails += 1 print(f'FAIL {name}: {e}') except Exception as e: fails += 1 print(f'ERROR {name}: {type(e).__name__}: {e}') sys.exit(1 if fails else 0)