Fix DeepSWE Pier routing and Terminal-Bench context/apt setup.

Keep generation max_tokens from being used as terminus-2 context, rewrite Debian/Ubuntu .sources to the Tsinghua mirror, and skip injecting truncation_tokens into DeepSWE extra_params.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
sora 2026-09-08 07:32:07 +00:00
parent 0c2698d0d7
commit ad61a2c44e
8 changed files with 379 additions and 39 deletions

View File

@ -600,6 +600,7 @@ def build_task_config(
thinking_max_tokens_scale: float = 1.0, thinking_max_tokens_scale: float = 1.0,
max_tokens_add: int = 0, max_tokens_add: int = 0,
thinking_budget_tokens: int = None, thinking_budget_tokens: int = None,
truncation_tokens: int = None,
) -> TaskConfig: ) -> TaskConfig:
if run_idx > 0: if run_idx > 0:
work_dir = Path(output_dir) / dataset_name / f'seed_{seed}_run_{run_idx}' work_dir = Path(output_dir) / dataset_name / f'seed_{seed}_run_{run_idx}'
@ -641,6 +642,11 @@ def build_task_config(
print(f' max_tokens add: {original_max_tokens} -> {new_max_tokens}') print(f' max_tokens add: {original_max_tokens} -> {new_max_tokens}')
dataset_args = deepcopy(ds_cfg.get('dataset_args', {})) dataset_args = deepcopy(ds_cfg.get('dataset_args', {}))
dataset_args.setdefault('shuffle', True) dataset_args.setdefault('shuffle', True)
# Terminal-Bench uses truncation_tokens as a context-limit fallback.
# DeepSWE rejects unknown extra_params, so never inject it there.
if truncation_tokens is not None and dataset_name != 'deep_swe':
extra_params = dataset_args.setdefault('extra_params', {})
extra_params.setdefault('truncation_tokens', truncation_tokens)
if dataset_name in MATH_DATASETS: if dataset_name in MATH_DATASETS:
dataset_args['prompt_template'] = MATH_PROMPT_TEMPLATE dataset_args['prompt_template'] = MATH_PROMPT_TEMPLATE
@ -668,6 +674,7 @@ def build_task_config(
generation_config=generation_config, generation_config=generation_config,
dataset_args=dataset_args_dict, dataset_args=dataset_args_dict,
agent_config=agent_config, agent_config=agent_config,
ignore_errors=bool(ds_cfg.get('ignore_errors', False)),
eval_batch_size=batch_size, eval_batch_size=batch_size,
sandbox=SandboxTaskConfig( sandbox=SandboxTaskConfig(
enabled=True, enabled=True,
@ -1001,6 +1008,7 @@ def main():
thinking_max_tokens_scale=args.thinking_max_tokens_scale, thinking_max_tokens_scale=args.thinking_max_tokens_scale,
max_tokens_add=args.max_tokens_add, max_tokens_add=args.max_tokens_add,
thinking_budget_tokens=args.thinking_budget_tokens, thinking_budget_tokens=args.thinking_budget_tokens,
truncation_tokens=args.truncation_tokens,
) )
try: try:
run_and_summarize(task_cfg, write_summary_flag, str(model_output_dir), args.model, run_and_summarize(task_cfg, write_summary_flag, str(model_output_dir), args.model,

View File

@ -204,6 +204,8 @@ swe_bench_pro:
stream: true stream: true
max_tokens: 32768 max_tokens: 32768
terminal_bench_v2_1: terminal_bench_v2_1:
# 单题 Harbor 异常(如 AgentTimeoutError不中止整项超时题不计入分数
ignore_errors: true
generation_config: generation_config:
temperature: 0.0 temperature: 0.0
top_p: 1.0 top_p: 1.0
@ -211,8 +213,14 @@ terminal_bench_v2_1:
max_tokens: 8192 max_tokens: 8192
dataset_args: dataset_args:
extra_params: extra_params:
timeout_multiplier: 2.0 # 任务默认 agent 时限约 900s倍率 6 → 约 1 小时。绝对秒数用 agent_timeout_sec勿与 agent_timeout_multiplier 同设)
max_turns: 500 timeout_multiplier: 6.0
max_turns: 200
# 上下窗口context_limit > generation_config.max_tokens > --truncation-tokens > 100000
# 必须显式设 context_limit否则 max_tokens=8192 会被当成上下文terminus-2 几乎每轮摘要
context_limit: 65536
enable_summarize: true
proactive_summarization_threshold: 8000
aa_lcr: aa_lcr:
generation_config: generation_config:
temperature: 1.0 temperature: 1.0
@ -233,6 +241,8 @@ deep_swe:
max_tokens: 32768 max_tokens: 32768
dataset_args: dataset_args:
extra_params: extra_params:
# 官方 Claude 等原生 provider 改 pier_route: native
pier_route: openai_compat
pier_model_prefix: openai pier_model_prefix: openai
pier_agent_kwargs: pier_agent_kwargs:
model_class: litellm model_class: litellm

View File

@ -257,6 +257,7 @@ deep_swe:
max_completion_tokens: 64000 max_completion_tokens: 64000
dataset_args: dataset_args:
extra_params: extra_params:
pier_route: openai_compat
pier_model_prefix: openai pier_model_prefix: openai
pier_agent_kwargs: pier_agent_kwargs:
model_class: litellm model_class: litellm
@ -320,7 +321,10 @@ terminal_bench_v2_1:
dataset_args: dataset_args:
extra_params: extra_params:
timeout_multiplier: 2.0 timeout_multiplier: 2.0
max_turns: 500 max_turns: 200
context_limit: 65536
enable_summarize: true
proactive_summarization_threshold: 8000
browsecomp: browsecomp:
generation_config: generation_config:
temperature: 1.0 temperature: 1.0

View File

@ -1,6 +1,8 @@
import uuid import uuid
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union from typing import Any, Dict, List, Optional, Tuple, Union
from urllib.parse import urlparse, urlunparse
from evalscope.api.benchmark import AgentAdapter, BenchmarkMeta from evalscope.api.benchmark import AgentAdapter, BenchmarkMeta
from evalscope.api.dataset import DatasetDict, Sample, build_dataset_from_records from evalscope.api.dataset import DatasetDict, Sample, build_dataset_from_records
@ -54,16 +56,117 @@ COMMON_EXTRA_PARAMS = {
}, },
'pier_model_prefix': { 'pier_model_prefix': {
'type': 'str', 'type': 'str',
'description': 'LiteLLM provider prefix prepended when TaskConfig.model has no slash (e.g. openai).', 'description': (
'LiteLLM provider prepended in openai_compat mode (default openai). '
'Applied even when the served model id already contains a slash.'
),
'value': 'openai', 'value': 'openai',
}, },
'pier_model_name': { 'pier_model_name': {
'type': 'str', 'type': 'str',
'description': 'Optional full Pier model name provider/model. Overrides TaskConfig.model when set.', 'description': 'Optional served model id for Pier. Overrides TaskConfig.model, then still normalized by pier_route.',
'value': '', 'value': '',
}, },
'pier_route': {
'type': 'str',
'description': (
"openai_compat (default): always use openai/<served-id> for LiteLLM. "
"native: pass the model string through (e.g. anthropic/claude-opus-4-8)."
),
'value': 'openai_compat',
},
'extra_allowed_hosts': {
'type': 'list',
'description': (
'Optional extra Pier agent allowlist hosts. The hostname/IP from TaskConfig.api_url '
'is always added automatically; use this only for additional destinations.'
),
'value': [],
},
} }
_LOOPBACK_HOSTS = {'127.0.0.1', 'localhost', '::1'}
_HOST_GATEWAY = 'host.docker.internal'
_CHAT_COMPLETIONS_SUFFIXES = ('/chat/completions', '/completions')
_HOST_GATEWAY_COMPOSE = """\
services:
main:
extra_hosts:
- "host.docker.internal:host-gateway"
"""
@dataclass(frozen=True)
class PierApiEndpoint:
"""Normalized OpenAI-compatible endpoint derived from TaskConfig.api_url."""
base_url: str
allowlist_host: str
uses_host_gateway: bool
def resolve_pier_api_endpoint(api_url: str) -> Optional[PierApiEndpoint]:
"""Turn an EvalScope api_url into a sandbox-reachable LiteLLM base URL.
Strips ``/chat/completions``, keeps ``/v1``, rewrites loopback hosts to
``host.docker.internal`` so Pier's Docker agent can reach the host or a
remote OpenAI-compatible server. Returns None when api_url is empty.
"""
raw = (api_url or '').strip()
if not raw:
return None
parsed = urlparse(raw if '://' in raw else f'http://{raw}')
host = (parsed.hostname or '').strip('[]')
if not host:
return None
uses_host_gateway = host.lower() in _LOOPBACK_HOSTS
if uses_host_gateway:
host = _HOST_GATEWAY
path = (parsed.path or '').rstrip('/')
for suffix in _CHAT_COMPLETIONS_SUFFIXES:
if path.endswith(suffix):
path = path[: -len(suffix)].rstrip('/')
break
if not path:
path = '/v1'
netloc = f'{host}:{parsed.port}' if parsed.port else host
scheme = parsed.scheme or 'http'
base_url = urlunparse((scheme, netloc, path, '', '', '')).rstrip('/')
return PierApiEndpoint(base_url=base_url, allowlist_host=host, uses_host_gateway=uses_host_gateway)
PIER_ROUTE_OPENAI_COMPAT = 'openai_compat'
PIER_ROUTE_NATIVE = 'native'
def resolve_pier_model_name(
served_name: str,
*,
route: str = PIER_ROUTE_OPENAI_COMPAT,
prefix: str = 'openai',
) -> str:
"""Map a served model id to the LiteLLM/Pier ``--model`` string.
``openai_compat`` (default) always uses ``{prefix}/<served-id>``. A slash in
the served id is part of the gateway model name, not a LiteLLM provider
(e.g. ``DeepSeek/DeepSeek-V4-Flash-0731`` -> ``openai/DeepSeek/DeepSeek-V4-Flash-0731``).
``native`` leaves official provider routes unchanged (``anthropic/...``).
"""
name = (served_name or '').strip()
if not name:
return name
route_key = (route or PIER_ROUTE_OPENAI_COMPAT).strip().lower()
if route_key == PIER_ROUTE_NATIVE:
return name
provider = (prefix or 'openai').strip().strip('/') or 'openai'
marker = f'{provider}/'
if name.startswith(marker) or name.lower().startswith(marker.lower()):
return name
return f'{provider}/{name}'
class DeepSWEAdapter(AgentAdapter): class DeepSWEAdapter(AgentAdapter):
"""EvalScope adapter for DeepSWE through Pier Python API jobs.""" """EvalScope adapter for DeepSWE through Pier Python API jobs."""
@ -78,6 +181,8 @@ class DeepSWEAdapter(AgentAdapter):
self.pier_agent_kwargs = dict(extra_params.get('pier_agent_kwargs') or {}) self.pier_agent_kwargs = dict(extra_params.get('pier_agent_kwargs') or {})
self.pier_model_prefix = str(extra_params.get('pier_model_prefix') or 'openai') self.pier_model_prefix = str(extra_params.get('pier_model_prefix') or 'openai')
self.pier_model_name = str(extra_params.get('pier_model_name') or '') self.pier_model_name = str(extra_params.get('pier_model_name') or '')
self.pier_route = str(extra_params.get('pier_route') or PIER_ROUTE_OPENAI_COMPAT)
self.extra_allowed_hosts = self._as_list(extra_params.get('extra_allowed_hosts') or [])
@staticmethod @staticmethod
def _as_list(value: Union[str, List[Any], Tuple[Any, ...]]) -> List[str]: def _as_list(value: Union[str, List[Any], Tuple[Any, ...]]) -> List[str]:
@ -145,6 +250,10 @@ class DeepSWEAdapter(AgentAdapter):
task_id = str(sample.metadata['task_id']) task_id = str(sample.metadata['task_id'])
pier_kwargs = dict(self.pier_agent_kwargs) pier_kwargs = dict(self.pier_agent_kwargs)
pier_kwargs.setdefault('model_class', 'litellm') pier_kwargs.setdefault('model_class', 'litellm')
endpoint = self._pier_api_endpoint()
environment_kwargs: Dict[str, Any] = {'type': 'docker'}
if endpoint is not None and endpoint.uses_host_gateway:
environment_kwargs['extra_docker_compose'] = [self._host_gateway_compose_path()]
config = JobConfig( config = JobConfig(
job_name=f'{task_id[:48].rstrip("_-")}__{uuid.uuid4().hex[:8]}', job_name=f'{task_id[:48].rstrip("_-")}__{uuid.uuid4().hex[:8]}',
@ -161,10 +270,11 @@ class DeepSWEAdapter(AgentAdapter):
name='mini-swe-agent', name='mini-swe-agent',
model_name=self._pier_model_name(model), model_name=self._pier_model_name(model),
kwargs=pier_kwargs, kwargs=pier_kwargs,
env=self._pier_agent_env(), env=self._pier_agent_env(endpoint),
extra_allowed_hosts=self._pier_allowed_hosts(endpoint),
) )
], ],
environment=EnvironmentConfig(type='docker'), environment=EnvironmentConfig(**environment_kwargs),
verifier=VerifierConfig(env={}), verifier=VerifierConfig(env={}),
tasks=[TaskConfig(path=Path(sample.metadata['task_path']))], tasks=[TaskConfig(path=Path(sample.metadata['task_path']))],
) )
@ -181,10 +291,11 @@ class DeepSWEAdapter(AgentAdapter):
def _pier_model_name(self, model: Model) -> str: def _pier_model_name(self, model: Model) -> str:
name = self.pier_model_name or (model.name if model else '') name = self.pier_model_name or (model.name if model else '')
name = str(name).strip() return resolve_pier_model_name(
if name and '/' not in name: str(name),
name = f'{self.pier_model_prefix}/{name}' route=self.pier_route,
return name prefix=self.pier_model_prefix,
)
@staticmethod @staticmethod
def _plain_secret(value: Any) -> str: def _plain_secret(value: Any) -> str:
@ -194,18 +305,39 @@ class DeepSWEAdapter(AgentAdapter):
value = value.get_secret_value() value = value.get_secret_value()
return str(value).strip() return str(value).strip()
def _pier_agent_env(self) -> Dict[str, str]: def _pier_api_endpoint(self) -> Optional[PierApiEndpoint]:
tc = self._task_config
if tc is None:
return None
return resolve_pier_api_endpoint(self._plain_secret(getattr(tc, 'api_url', None)))
def _pier_allowed_hosts(self, endpoint: Optional[PierApiEndpoint] = None) -> List[str]:
endpoint = self._pier_api_endpoint() if endpoint is None else endpoint
hosts: List[str] = []
if endpoint is not None:
hosts.append(endpoint.allowlist_host)
hosts.extend(self.extra_allowed_hosts)
return list(dict.fromkeys(host for host in hosts if host))
def _host_gateway_compose_path(self) -> Path:
path = Path(self.output_dir) / 'deep_swe_host_gateway.compose.yaml'
if not path.exists():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(_HOST_GATEWAY_COMPOSE, encoding='utf-8')
return path
def _pier_agent_env(self, endpoint: Optional[PierApiEndpoint] = None) -> Dict[str, str]:
env: Dict[str, str] = { env: Dict[str, str] = {
'LITELLM_LOCAL_MODEL_COST_MAP': 'true', 'LITELLM_LOCAL_MODEL_COST_MAP': 'true',
} }
tc = self._task_config tc = self._task_config
if tc is None: if tc is None:
return env return env
api_url = self._plain_secret(getattr(tc, 'api_url', None)) endpoint = self._pier_api_endpoint() if endpoint is None else endpoint
api_key = self._plain_secret(getattr(tc, 'api_key', None)) api_key = self._plain_secret(getattr(tc, 'api_key', None))
if api_url: if endpoint is not None:
env['OPENAI_API_BASE'] = api_url env['OPENAI_API_BASE'] = endpoint.base_url
env['OPENAI_BASE_URL'] = api_url env['OPENAI_BASE_URL'] = endpoint.base_url
if api_key and api_key != 'EMPTY': if api_key and api_key != 'EMPTY':
env['OPENAI_API_KEY'] = api_key env['OPENAI_API_KEY'] = api_key
env['MSWEA_API_KEY'] = api_key env['MSWEA_API_KEY'] = api_key
@ -266,8 +398,10 @@ integrates it through Pier and runs each benchmark sample as one Pier Python API
- Dataset defaults to ModelScope `evalscope/deep-swe` - Dataset defaults to ModelScope `evalscope/deep-swe`
- DeepSWE runs through Pier's Docker environment in EvalScope - DeepSWE runs through Pier's Docker environment in EvalScope
- Use `pier_agent_kwargs={'model_class': 'litellm'}` for OpenAI-compatible providers that do not support Responses API - Use `pier_agent_kwargs={'model_class': 'litellm'}` for OpenAI-compatible providers that do not support Responses API
- If `TaskConfig.model` has no slash, EvalScope prefixes `pier_model_prefix` (default `openai/`) for Pier/LiteLLM - `pier_route=openai_compat` (default) always sends LiteLLM `openai/<served-id>`, including served ids that already contain a slash
- `pier_route=native` passes the model string through for official providers such as `anthropic/...`
- API base/key are forwarded as `OPENAI_API_BASE` / `OPENAI_API_KEY` from TaskConfig - API base/key are forwarded as `OPENAI_API_BASE` / `OPENAI_API_KEY` from TaskConfig
- The hostname/IP in `TaskConfig.api_url` is added to Pier `extra_allowed_hosts` automatically; loopback URLs are rewritten to `host.docker.internal`
- Optional offline Pier agent install: run `bash/images_load/preload_deep_swe_agent_offline.sh` so sandbox builds COPY local uv/wheels instead of curling GitHub - Optional offline Pier agent install: run `bash/images_load/preload_deep_swe_agent_offline.sh` so sandbox builds COPY local uv/wheels instead of curling GitHub
""", """,
dataset_id=DEFAULT_MODELSCOPE_DATASET_ID, dataset_id=DEFAULT_MODELSCOPE_DATASET_ID,

View File

@ -47,31 +47,35 @@ except Exception:
pass pass
# 把容器内的 apt 源换成清华镜像,避免 tmux/asciinema 安装时 apt-get update 慢/超时。 # 把容器内的 apt 源换成清华镜像,避免 tmux/asciinema 安装时 apt-get update 慢/超时。
# 可以通过环境变量 HARBOR_APT_MIRROR 切换镜像地址。 # 必须同时改 classic .list 和 Ubuntu 24.04 / Debian 12 的 deb822 .sources
# 否则 log-summary 这类镜像仍走 deb.debian.orgPackages 索引可能下几百秒。
# 默认用 http任务镜像常缺 CA且实测比 https 更快。可用 HARBOR_APT_MIRROR 覆盖。
try: try:
from harbor.agents.terminus_2.tmux_session import TmuxSession as _TmuxSession from harbor.agents.terminus_2.tmux_session import TmuxSession as _TmuxSession
_APT_MIRROR = os.environ.get('HARBOR_APT_MIRROR', 'https://mirrors.tuna.tsinghua.edu.cn') _APT_MIRROR = os.environ.get('HARBOR_APT_MIRROR', 'http://mirrors.tuna.tsinghua.edu.cn')
_ORIG_GET_COMBINED_INSTALL_COMMAND = _TmuxSession._get_combined_install_command _ORIG_GET_COMBINED_INSTALL_COMMAND = _TmuxSession._get_combined_install_command
def _patched_get_combined_install_command(self, system_info, tools): def _patched_get_combined_install_command(self, system_info, tools):
package_manager = system_info.get('package_manager') if isinstance(system_info, dict) else None package_manager = system_info.get('package_manager') if isinstance(system_info, dict) else None
if package_manager == 'apt-get': if package_manager == 'apt-get':
packages = ' '.join(tools) packages = ' '.join(tools)
sed_expr = (
f"s|https\\?://archive.ubuntu.com/ubuntu|{_APT_MIRROR}/ubuntu|g; "
f"s|https\\?://security.ubuntu.com/ubuntu|{_APT_MIRROR}/ubuntu|g; "
f"s|https\\?://ports.ubuntu.com/ubuntu-ports|{_APT_MIRROR}/ubuntu-ports|g; "
f"s|https\\?://security.debian.org/debian-security|{_APT_MIRROR}/debian-security|g; "
f"s|https\\?://deb.debian.org/debian-security|{_APT_MIRROR}/debian-security|g; "
f"s|https\\?://deb.debian.org/debian|{_APT_MIRROR}/debian|g"
)
return ( return (
f"sed -i 's|http://archive.ubuntu.com/ubuntu/|{_APT_MIRROR}/ubuntu/|g; " 'for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.list '
f"s|https://archive.ubuntu.com/ubuntu/|{_APT_MIRROR}/ubuntu/|g; " '/etc/apt/sources.list.d/*.sources; do '
f"s|http://security.ubuntu.com/ubuntu/|{_APT_MIRROR}/ubuntu/|g; " '[ -f "$f" ] || continue; '
f"s|https://security.ubuntu.com/ubuntu/|{_APT_MIRROR}/ubuntu/|g; " f"sed -i '{sed_expr}' \"$f\"; "
f"s|http://ports.ubuntu.com/ubuntu-ports/|{_APT_MIRROR}/ubuntu-ports/|g; " 'done; '
f"s|https://ports.ubuntu.com/ubuntu-ports/|{_APT_MIRROR}/ubuntu-ports/|g; " 'DEBIAN_FRONTEND=noninteractive apt-get update && '
f"s|http://deb.debian.org/debian|{_APT_MIRROR}/debian|g; " f'DEBIAN_FRONTEND=noninteractive apt-get install -y {packages}'
f"s|https://deb.debian.org/debian|{_APT_MIRROR}/debian|g; "
f"s|http://security.debian.org/debian-security|{_APT_MIRROR}/debian-security|g; "
f"s|https://security.debian.org/debian-security|{_APT_MIRROR}/debian-security|g' "
f"/etc/apt/sources.list /etc/apt/sources.list.d/*.list 2>/dev/null; "
f"DEBIAN_FRONTEND=noninteractive apt-get update && "
f"DEBIAN_FRONTEND=noninteractive apt-get install -y {packages}"
) )
return _ORIG_GET_COMBINED_INSTALL_COMMAND(self, system_info, tools) return _ORIG_GET_COMBINED_INSTALL_COMMAND(self, system_info, tools)
@ -132,6 +136,35 @@ COMMON_EXTRA_PARAMS = {
'description': 'Maximum number of turns for the agent to complete the task.', 'description': 'Maximum number of turns for the agent to complete the task.',
'value': 200, 'value': 200,
}, },
'context_limit': {
'type': 'int',
'description': (
'Terminus-2 context window for summarization. Priority: context_limit > '
'generation_config.max_tokens > truncation_tokens > 100000.'
),
'value': None,
},
'enable_summarize': {
'type': 'bool',
'description': 'Enable terminus-2 context summarization.',
'value': True,
},
'proactive_summarization_threshold': {
'type': 'int',
'description': (
'Summarize when remaining context tokens fall below this value. '
'Set 0 to disable proactive summarization.'
),
'value': 8000,
},
'truncation_tokens': {
'type': 'int',
'description': (
'Fallback context window from --truncation-tokens. Used only when '
'context_limit and generation_config.max_tokens are unset.'
),
'value': None,
},
'environment_kwargs': { 'environment_kwargs': {
'type': 'dict', 'type': 'dict',
'description': 'Extra kwargs passed to Harbor EnvironmentConfig. ' 'description': 'Extra kwargs passed to Harbor EnvironmentConfig. '
@ -192,6 +225,20 @@ class _TerminalBenchBase(AgentAdapter):
) )
self.max_turns = self.extra_params.get('max_turns', 200) self.max_turns = self.extra_params.get('max_turns', 200)
self.environment_kwargs = self.extra_params.get('environment_kwargs', {}) self.environment_kwargs = self.extra_params.get('environment_kwargs', {})
self.context_limit = self.extra_params.get('context_limit')
self.enable_summarize = self.extra_params.get('enable_summarize', True)
self.proactive_summarization_threshold = self.extra_params.get(
'proactive_summarization_threshold', 8000
)
def _resolve_context_limit(self, model: Model) -> int:
from .utils import resolve_terminus_context_limit
return resolve_terminus_context_limit(
context_limit=self.context_limit,
max_tokens=getattr(getattr(model, 'config', None), 'max_tokens', None),
truncation_tokens=self.extra_params.get('truncation_tokens'),
)
def load(self): def load(self):
_validate_environment_requirements(self.environment_type) _validate_environment_requirements(self.environment_type)
@ -242,8 +289,8 @@ class _TerminalBenchBase(AgentAdapter):
agent_kwargs.update( agent_kwargs.update(
{ {
'parser_name': 'json', 'parser_name': 'json',
'enable_summarize': True, 'enable_summarize': bool(self.enable_summarize),
'proactive_summarization_threshold': 8000, 'proactive_summarization_threshold': int(self.proactive_summarization_threshold),
'collect_rollout_details': False, 'collect_rollout_details': False,
} }
) )
@ -268,7 +315,9 @@ class _TerminalBenchBase(AgentAdapter):
) )
try: try:
harbor_llm = HarborLLM(model=model) if self.agent_name == 'terminus-2' else None harbor_llm = None
if self.agent_name == 'terminus-2':
harbor_llm = HarborLLM(model=model, context_limit=self._resolve_context_limit(model))
async def _run_trial(): async def _run_trial():
trial = await Trial.create(trial_config) trial = await Trial.create(trial_config)

View File

@ -1,5 +1,5 @@
import asyncio import asyncio
from typing import List from typing import Any, List, Optional
from harbor.llms.base import BaseLLM, LLMResponse, UsageInfo from harbor.llms.base import BaseLLM, LLMResponse, UsageInfo
from pydantic import BaseModel, ConfigDict, PrivateAttr from pydantic import BaseModel, ConfigDict, PrivateAttr
@ -9,17 +9,53 @@ from evalscope.api.messages.perf_metrics import PerformanceMetrics
from evalscope.api.model.model import Model from evalscope.api.model.model import Model
from evalscope.models.utils.openai import openai_chat_choices from evalscope.models.utils.openai import openai_chat_choices
HARBOR_DEFAULT_CONTEXT_LIMIT = 100_000
def _positive_int(value: Any) -> Optional[int]:
if value in (None, ''):
return None
try:
parsed = int(value)
except (TypeError, ValueError):
return None
return parsed if parsed > 0 else None
def resolve_terminus_context_limit(
*,
context_limit: Any = None,
max_tokens: Any = None,
truncation_tokens: Any = None,
default: int = HARBOR_DEFAULT_CONTEXT_LIMIT,
) -> int:
"""Context window for terminus-2 summarization.
Priority: extra_params.context_limit > generation_config.max_tokens >
truncation-tokens > Harbor default 100000.
"""
for value in (context_limit, max_tokens, truncation_tokens):
parsed = _positive_int(value)
if parsed is not None:
return parsed
return default
class HarborLLM(BaseModel, BaseLLM): class HarborLLM(BaseModel, BaseLLM):
"""A mock LLM that simulates sandboxed code execution.""" """A mock LLM that simulates sandboxed code execution."""
model_config = ConfigDict(arbitrary_types_allowed=True) model_config = ConfigDict(arbitrary_types_allowed=True)
_model: Model = PrivateAttr() _model: Model = PrivateAttr()
_context_limit: int = PrivateAttr()
_perf_metrics: List[PerformanceMetrics] = PrivateAttr(default_factory=list) _perf_metrics: List[PerformanceMetrics] = PrivateAttr(default_factory=list)
def __init__(self, model: Model, **kwargs): def __init__(self, model: Model, context_limit: Optional[int] = None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self._model = model self._model = model
self._context_limit = resolve_terminus_context_limit(
context_limit=context_limit,
max_tokens=getattr(getattr(model, 'config', None), 'max_tokens', None),
)
@property @property
def model(self): def model(self):
@ -58,7 +94,7 @@ class HarborLLM(BaseModel, BaseLLM):
) )
def get_model_context_limit(self): def get_model_context_limit(self):
return self._model.config.max_tokens or 100_000 return self._context_limit
def get_model_output_limit(self): def get_model_output_limit(self):
return self._model.config.max_tokens or 16384 return self._model.config.max_tokens or 16384

View File

@ -11,7 +11,12 @@ import pytest
from evalscope.api.dataset import Sample from evalscope.api.dataset import Sample
from evalscope.api.evaluator import TaskState from evalscope.api.evaluator import TaskState
from evalscope.api.registry import get_benchmark from evalscope.api.registry import get_benchmark
from evalscope.benchmarks.deep_swe.deep_swe_adapter import DEFAULT_MODELSCOPE_DATASET_ID, DeepSWEAdapter from evalscope.benchmarks.deep_swe.deep_swe_adapter import (
DEFAULT_MODELSCOPE_DATASET_ID,
DeepSWEAdapter,
resolve_pier_api_endpoint,
resolve_pier_model_name,
)
from evalscope.benchmarks.deep_swe.utils import artifact_path, build_score_metadata, parse_timestamp from evalscope.benchmarks.deep_swe.utils import artifact_path, build_score_metadata, parse_timestamp
from evalscope.config import TaskConfig from evalscope.config import TaskConfig
@ -297,6 +302,62 @@ def install_fake_pier(monkeypatch: Any, captured: Dict[str, Any], result: Dict[s
monkeypatch.setattr('evalscope.benchmarks.deep_swe.deep_swe_adapter.check_import', lambda *args, **kwargs: True) monkeypatch.setattr('evalscope.benchmarks.deep_swe.deep_swe_adapter.check_import', lambda *args, **kwargs: True)
def test_resolve_pier_api_endpoint_from_local_intranet_and_gateway_urls() -> None:
local = resolve_pier_api_endpoint('http://127.0.0.1:8004/v1')
assert local is not None
assert local.base_url == 'http://host.docker.internal:8004/v1'
assert local.allowlist_host == 'host.docker.internal'
assert local.uses_host_gateway is True
intranet = resolve_pier_api_endpoint('http://10.1.2.3:8000/v1')
assert intranet is not None
assert intranet.base_url == 'http://10.1.2.3:8000/v1'
assert intranet.allowlist_host == '10.1.2.3'
assert intranet.uses_host_gateway is False
gateway = resolve_pier_api_endpoint('https://api.vectron.meta-stone.com/v1/chat/completions')
assert gateway is not None
assert gateway.base_url == 'https://api.vectron.meta-stone.com/v1'
assert gateway.allowlist_host == 'api.vectron.meta-stone.com'
assert gateway.uses_host_gateway is False
def test_resolve_pier_model_name_openai_compat_and_native() -> None:
assert resolve_pier_model_name('DeepSeek/DeepSeek-V4-Flash-0731') == (
'openai/DeepSeek/DeepSeek-V4-Flash-0731'
)
assert resolve_pier_model_name('Qwen3-VL-8B-Instruct') == 'openai/Qwen3-VL-8B-Instruct'
assert resolve_pier_model_name('openai/gpt-5.5') == 'openai/gpt-5.5'
assert resolve_pier_model_name(
'anthropic/claude-opus-4-8',
route='native',
) == 'anthropic/claude-opus-4-8'
def test_pier_model_name_uses_openai_compat_by_default(tmp_path: Path) -> None:
class SlashModel:
name = 'DeepSeek/DeepSeek-V4-Flash-0731'
adapter = make_adapter(tmp_path)
assert adapter._pier_model_name(SlashModel()) == 'openai/DeepSeek/DeepSeek-V4-Flash-0731'
native = make_adapter(tmp_path, pier_route='native')
assert native._pier_model_name(SlashModel()) == 'DeepSeek/DeepSeek-V4-Flash-0731'
def test_pier_allowed_hosts_come_from_api_url(tmp_path: Path) -> None:
cfg = TaskConfig(
datasets=['deep_swe'],
api_url='https://api.vectron.meta-stone.com/v1/chat/completions',
api_key='sk-test',
work_dir=str(tmp_path / 'outputs'),
dataset_args={'deep_swe': {'extra_params': {'extra_allowed_hosts': ['10.9.9.9']}}},
)
adapter = get_benchmark('deep_swe', cfg)
assert isinstance(adapter, DeepSWEAdapter)
assert adapter._pier_allowed_hosts() == ['api.vectron.meta-stone.com', '10.9.9.9']
def test_run_pier_job_uses_adhoc_task_source(monkeypatch: Any, tmp_path: Path) -> None: def test_run_pier_job_uses_adhoc_task_source(monkeypatch: Any, tmp_path: Path) -> None:
captured: Dict[str, Any] = {} captured: Dict[str, Any] = {}
result = { result = {
@ -313,6 +374,7 @@ def test_run_pier_job_uses_adhoc_task_source(monkeypatch: Any, tmp_path: Path) -
task_path = tmp_path / 'tasks' / 'task-a' task_path = tmp_path / 'tasks' / 'task-a'
task_path.mkdir(parents=True) task_path.mkdir(parents=True)
adapter = make_adapter(tmp_path, pier_agent_kwargs={'model_class': 'litellm'}) adapter = make_adapter(tmp_path, pier_agent_kwargs={'model_class': 'litellm'})
adapter._task_config.api_url = 'http://10.1.2.3:8000/v1'
sample = Sample(input='', metadata={'task_id': 'task-a', 'task_path': str(task_path)}) sample = Sample(input='', metadata={'task_id': 'task-a', 'task_path': str(task_path)})
result_dict = adapter._run_pier_job(MockModel(), sample) result_dict = adapter._run_pier_job(MockModel(), sample)
@ -321,7 +383,10 @@ def test_run_pier_job_uses_adhoc_task_source(monkeypatch: Any, tmp_path: Path) -
assert captured['config'].tasks[0].path == task_path assert captured['config'].tasks[0].path == task_path
assert getattr(captured['config'].tasks[0], 'source', None) is None assert getattr(captured['config'].tasks[0], 'source', None) is None
assert captured['config'].agents[0].kwargs == {'model_class': 'litellm'} assert captured['config'].agents[0].kwargs == {'model_class': 'litellm'}
assert captured['config'].agents[0].model_name == 'openai/mock-model'
assert captured['config'].agents[0].extra_allowed_hosts == ['10.1.2.3']
assert captured['config'].environment.type == 'docker' assert captured['config'].environment.type == 'docker'
assert getattr(captured['config'].environment, 'extra_docker_compose', None) in (None, [])
@pytest.mark.skipif( @pytest.mark.skipif(

View File

@ -8,6 +8,7 @@ import pytest
from evalscope.api.messages.perf_metrics import PerformanceMetrics from evalscope.api.messages.perf_metrics import PerformanceMetrics
from evalscope.api.metric import Score from evalscope.api.metric import Score
from evalscope.benchmarks.terminal_bench.terminal_bench_adapter import _phase_timeout_options, _TerminalBenchBase from evalscope.benchmarks.terminal_bench.terminal_bench_adapter import _phase_timeout_options, _TerminalBenchBase
from evalscope.benchmarks.terminal_bench.utils import HARBOR_DEFAULT_CONTEXT_LIMIT, resolve_terminus_context_limit
TRIAL_URI = 'file:///tmp/terminal-bench-trial' TRIAL_URI = 'file:///tmp/terminal-bench-trial'
@ -145,3 +146,36 @@ def test_terminal_bench_trace_preserves_request_perf_metrics(tmp_path) -> None:
assert messages[0].perf_metrics.latency == 1.0 assert messages[0].perf_metrics.latency == 1.0
assert messages[0].perf_metrics.input_tokens == 3 assert messages[0].perf_metrics.input_tokens == 3
@pytest.mark.parametrize(
('kwargs', 'expected'),
[
({'context_limit': 65536, 'max_tokens': 8192, 'truncation_tokens': 131072}, 65536),
({'max_tokens': 8192, 'truncation_tokens': 131072}, 8192),
({'truncation_tokens': 65536}, 65536),
({}, HARBOR_DEFAULT_CONTEXT_LIMIT),
({'context_limit': 0, 'max_tokens': 8192, 'truncation_tokens': 65536}, 8192),
({'context_limit': '', 'max_tokens': None, 'truncation_tokens': 65536}, 65536),
],
)
def test_terminus_context_limit_priority(kwargs, expected) -> None:
assert resolve_terminus_context_limit(**kwargs) == expected
def test_adapter_resolves_context_limit_from_extra_params() -> None:
adapter = object.__new__(_TerminalBenchBase)
adapter.context_limit = 65536
adapter.extra_params = {'truncation_tokens': 131072}
model = SimpleNamespace(config=SimpleNamespace(max_tokens=8192))
assert adapter._resolve_context_limit(model) == 65536
adapter.context_limit = None
assert adapter._resolve_context_limit(model) == 8192
model.config.max_tokens = None
assert adapter._resolve_context_limit(model) == 131072
adapter.extra_params = {}
assert adapter._resolve_context_limit(model) == HARBOR_DEFAULT_CONTEXT_LIMIT