From b557f76bcd134e75f552e6a6b0e82c1fbccfd045 Mon Sep 17 00:00:00 2001 From: Simon Rosenberg Date: Wed, 7 Jan 2026 17:06:04 +0100 Subject: [PATCH 1/8] Fix cache tag truncation with ports and latest suffix --- .../openhands/agent_server/docker/build.py | 29 +++++++++++++--- tests/agent_server/test_docker_build.py | 33 +++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/openhands-agent-server/openhands/agent_server/docker/build.py b/openhands-agent-server/openhands/agent_server/docker/build.py index 2389690ab5..1e3cc4049e 100755 --- a/openhands-agent-server/openhands/agent_server/docker/build.py +++ b/openhands-agent-server/openhands/agent_server/docker/build.py @@ -226,7 +226,9 @@ def _base_slug(image: str, max_len: int = 64) -> str: # Parse components from the slug form if "_tag_" in base_slug: - left, tag = base_slug.split("_tag_", 1) + # Use rsplit to handle registries with ports (e.g., localhost:5000) so we + # only treat the last ":" as the tag separator. + left, tag = base_slug.rsplit("_tag_", 1) else: left, tag = base_slug, "" @@ -234,7 +236,8 @@ def _base_slug(image: str, max_len: int = 64) -> str: repo = parts[-1] if parts else left # last path segment is the repo # Reconstruct a compact, identifiable core: "[_tag_]" - ident = repo + (f"_tag_{tag}" if tag else "") + tag_piece = f"_tag_{tag}" if tag else "" + ident = repo + tag_piece # Fit within budget, reserving space for the digest suffix visible_budget = max_len - len(suffix) @@ -242,8 +245,26 @@ def _base_slug(image: str, max_len: int = 64) -> str: f"max_len too small to fit digest suffix with length {len(suffix)}" ) - kept = ident[:visible_budget] - return kept + suffix + if len(ident) > visible_budget: + if tag_piece and len(tag_piece) < visible_budget: + # Keep the full tag portion and trim the repo prefix if needed. + head_budget = visible_budget - len(tag_piece) + ident = repo[:head_budget] + tag_piece + elif tag_piece: + # Tag portion alone is longer than the budget; keep the leading part + # so sentinel substrings like "tag_latest" remain intact. + ident = tag_piece[:visible_budget] + else: + ident = ident[:visible_budget] + + truncated = ident + suffix + logger.debug( + "[base-slug] Truncated base image '%s' to slug '%s' (max_len=%d)", + image, + truncated, + max_len, + ) + return truncated def _git_info() -> tuple[str, str]: diff --git a/tests/agent_server/test_docker_build.py b/tests/agent_server/test_docker_build.py index 2415493b64..b48ead839f 100644 --- a/tests/agent_server/test_docker_build.py +++ b/tests/agent_server/test_docker_build.py @@ -212,6 +212,39 @@ def test_base_slug_truncation_no_tag(): assert "very-long-repository-name" in result +def test_base_slug_preserves_latest_tag_suffix(): + """Ensure tag_latest suffix is not mangled when truncating long slugs.""" + from openhands.agent_server.docker.build import _base_slug + + image = ( + "docker.io/swebench/sweb.eval.x86_64.astropy_1776_astropy-8872:" + "tag_latest-0a797356ebce" + ) + + result = _base_slug(image, max_len=64) + + assert len(result) <= 64 + assert "tag_latest" in result + # Keep cache keys stable with digest suffix + assert result[-13:-12] == "-" + assert all(c in "0123456789abcdef" for c in result[-12:]) + + +def test_base_slug_preserves_tag_with_registry_port(): + """Handle registries with ports without losing the tag segment.""" + from openhands.agent_server.docker.build import _base_slug + + image = ( + "localhost:5001/swebench/sweb.eval.x86_64.astropy_1776_astropy-8872:" + "tag_latest-0a797356ebce" + ) + + result = _base_slug(image, max_len=64) + + assert len(result) <= 64 + assert "tag_latest" in result + + def test_base_slug_custom_max_len(): """Test base_slug with custom max_len parameter.""" from openhands.agent_server.docker.build import _base_slug From ac4652eadeedb1c3d97aa7cb9775be375061cceb Mon Sep 17 00:00:00 2001 From: Simon Rosenberg Date: Wed, 7 Jan 2026 20:26:48 +0100 Subject: [PATCH 2/8] Clarify port example in cache-tag comment --- openhands-agent-server/openhands/agent_server/docker/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openhands-agent-server/openhands/agent_server/docker/build.py b/openhands-agent-server/openhands/agent_server/docker/build.py index 1e3cc4049e..54e12a7626 100755 --- a/openhands-agent-server/openhands/agent_server/docker/build.py +++ b/openhands-agent-server/openhands/agent_server/docker/build.py @@ -226,7 +226,7 @@ def _base_slug(image: str, max_len: int = 64) -> str: # Parse components from the slug form if "_tag_" in base_slug: - # Use rsplit to handle registries with ports (e.g., localhost:5000) so we + # Use rsplit to handle registries with ports (e.g., ghcr.io:443) so we # only treat the last ":" as the tag separator. left, tag = base_slug.rsplit("_tag_", 1) else: From 9b05e0a4d7217b975f2784cc249737912f955cf3 Mon Sep 17 00:00:00 2001 From: Simon Rosenberg Date: Wed, 7 Jan 2026 20:31:06 +0100 Subject: [PATCH 3/8] Clarify port/tag rsplit rationale --- openhands-agent-server/openhands/agent_server/docker/build.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openhands-agent-server/openhands/agent_server/docker/build.py b/openhands-agent-server/openhands/agent_server/docker/build.py index 54e12a7626..f6312f013c 100755 --- a/openhands-agent-server/openhands/agent_server/docker/build.py +++ b/openhands-agent-server/openhands/agent_server/docker/build.py @@ -226,8 +226,8 @@ def _base_slug(image: str, max_len: int = 64) -> str: # Parse components from the slug form if "_tag_" in base_slug: - # Use rsplit to handle registries with ports (e.g., ghcr.io:443) so we - # only treat the last ":" as the tag separator. + # Ports also become "_tag_" (e.g., "ghcr.io:443/repo:tag" -> "ghcr.io_tag_443_s_repo_tag_tag"). + # Use rsplit so we split on the real image tag (the last _tag_), not the port. left, tag = base_slug.rsplit("_tag_", 1) else: left, tag = base_slug, "" From edf1fcd6cd3f7568e7aa8e8936bd3c62bfa4e17e Mon Sep 17 00:00:00 2001 From: Simon Rosenberg Date: Wed, 7 Jan 2026 20:46:48 +0100 Subject: [PATCH 4/8] Fix lint/pyright issues for cache-tag PR --- .github/run-eval/resolve_model_config.py | 3 ++- .../openhands/agent_server/docker/build.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/run-eval/resolve_model_config.py b/.github/run-eval/resolve_model_config.py index 2631757925..9b8de399ab 100755 --- a/.github/run-eval/resolve_model_config.py +++ b/.github/run-eval/resolve_model_config.py @@ -12,6 +12,7 @@ import json import os import sys +from typing import NoReturn # Model configurations dictionary @@ -53,7 +54,7 @@ } -def error_exit(msg: str, exit_code: int = 1) -> None: +def error_exit(msg: str, exit_code: int = 1) -> NoReturn: """Print error message and exit.""" print(f"ERROR: {msg}", file=sys.stderr) sys.exit(exit_code) diff --git a/openhands-agent-server/openhands/agent_server/docker/build.py b/openhands-agent-server/openhands/agent_server/docker/build.py index f6312f013c..fec199c129 100755 --- a/openhands-agent-server/openhands/agent_server/docker/build.py +++ b/openhands-agent-server/openhands/agent_server/docker/build.py @@ -226,8 +226,9 @@ def _base_slug(image: str, max_len: int = 64) -> str: # Parse components from the slug form if "_tag_" in base_slug: - # Ports also become "_tag_" (e.g., "ghcr.io:443/repo:tag" -> "ghcr.io_tag_443_s_repo_tag_tag"). - # Use rsplit so we split on the real image tag (the last _tag_), not the port. + # Ports also become "_tag_" (e.g., "ghcr.io:443/repo:tag" + # -> "ghcr.io_tag_443_s_repo_tag_tag"). Use rsplit so we split on the real + # image tag (the last _tag_), not the port. left, tag = base_slug.rsplit("_tag_", 1) else: left, tag = base_slug, "" From 172c322931fb0c790ba1333dcf4fa993e6d38ced Mon Sep 17 00:00:00 2001 From: Simon Rosenberg Date: Wed, 7 Jan 2026 20:50:50 +0100 Subject: [PATCH 5/8] Revert "Fix lint/pyright issues for cache-tag PR" This reverts commit edf1fcd6cd3f7568e7aa8e8936bd3c62bfa4e17e. --- .github/run-eval/resolve_model_config.py | 3 +-- .../openhands/agent_server/docker/build.py | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/run-eval/resolve_model_config.py b/.github/run-eval/resolve_model_config.py index 9b8de399ab..2631757925 100755 --- a/.github/run-eval/resolve_model_config.py +++ b/.github/run-eval/resolve_model_config.py @@ -12,7 +12,6 @@ import json import os import sys -from typing import NoReturn # Model configurations dictionary @@ -54,7 +53,7 @@ } -def error_exit(msg: str, exit_code: int = 1) -> NoReturn: +def error_exit(msg: str, exit_code: int = 1) -> None: """Print error message and exit.""" print(f"ERROR: {msg}", file=sys.stderr) sys.exit(exit_code) diff --git a/openhands-agent-server/openhands/agent_server/docker/build.py b/openhands-agent-server/openhands/agent_server/docker/build.py index fec199c129..f6312f013c 100755 --- a/openhands-agent-server/openhands/agent_server/docker/build.py +++ b/openhands-agent-server/openhands/agent_server/docker/build.py @@ -226,9 +226,8 @@ def _base_slug(image: str, max_len: int = 64) -> str: # Parse components from the slug form if "_tag_" in base_slug: - # Ports also become "_tag_" (e.g., "ghcr.io:443/repo:tag" - # -> "ghcr.io_tag_443_s_repo_tag_tag"). Use rsplit so we split on the real - # image tag (the last _tag_), not the port. + # Ports also become "_tag_" (e.g., "ghcr.io:443/repo:tag" -> "ghcr.io_tag_443_s_repo_tag_tag"). + # Use rsplit so we split on the real image tag (the last _tag_), not the port. left, tag = base_slug.rsplit("_tag_", 1) else: left, tag = base_slug, "" From 5e6786b97619c40a23e11c754553462e28952788 Mon Sep 17 00:00:00 2001 From: Simon Rosenberg Date: Wed, 7 Jan 2026 20:55:49 +0100 Subject: [PATCH 6/8] Tidy cache-tag comment; keep resolve_model_config behavior --- .github/run-eval/resolve_model_config.py | 1 + .../openhands/agent_server/docker/build.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/run-eval/resolve_model_config.py b/.github/run-eval/resolve_model_config.py index 2631757925..9dd23e8334 100755 --- a/.github/run-eval/resolve_model_config.py +++ b/.github/run-eval/resolve_model_config.py @@ -64,6 +64,7 @@ def get_required_env(key: str) -> str: value = os.environ.get(key) if not value: error_exit(f"{key} not set") + raise SystemExit(1) # for type checkers; error_exit already exits return value diff --git a/openhands-agent-server/openhands/agent_server/docker/build.py b/openhands-agent-server/openhands/agent_server/docker/build.py index f6312f013c..fec199c129 100755 --- a/openhands-agent-server/openhands/agent_server/docker/build.py +++ b/openhands-agent-server/openhands/agent_server/docker/build.py @@ -226,8 +226,9 @@ def _base_slug(image: str, max_len: int = 64) -> str: # Parse components from the slug form if "_tag_" in base_slug: - # Ports also become "_tag_" (e.g., "ghcr.io:443/repo:tag" -> "ghcr.io_tag_443_s_repo_tag_tag"). - # Use rsplit so we split on the real image tag (the last _tag_), not the port. + # Ports also become "_tag_" (e.g., "ghcr.io:443/repo:tag" + # -> "ghcr.io_tag_443_s_repo_tag_tag"). Use rsplit so we split on the real + # image tag (the last _tag_), not the port. left, tag = base_slug.rsplit("_tag_", 1) else: left, tag = base_slug, "" From 044fb47509c33ef4fdefb70aabaf99c8b65d524c Mon Sep 17 00:00:00 2001 From: Simon Rosenberg Date: Wed, 7 Jan 2026 20:59:16 +0100 Subject: [PATCH 7/8] revert changfe --- .github/run-eval/resolve_model_config.py | 39 +++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/.github/run-eval/resolve_model_config.py b/.github/run-eval/resolve_model_config.py index 9dd23e8334..816ed66a39 100755 --- a/.github/run-eval/resolve_model_config.py +++ b/.github/run-eval/resolve_model_config.py @@ -50,6 +50,44 @@ "display_name": "Kimi K2 Thinking", "llm_config": {"model": "litellm_proxy/moonshot/kimi-k2-thinking"}, }, + "claude-4.5-opus": { + "id": "claude-4.5-opus", + "display_name": "Claude 4.5 Opus", + "llm_config": { + "model": "litellm_proxy/anthropic/claude-opus-4-5-20251101", + "temperature": 0.0, + }, + }, + "gemini-3-pro": { + "id": "gemini-3-pro", + "display_name": "Gemini 3 Pro", + "llm_config": {"model": "litellm_proxy/gemini/gemini-3-pro-preview"}, + }, + "gemini-3-flash": { + "id": "gemini-3-flash", + "display_name": "Gemini 3 Flash", + "llm_config": {"model": "litellm_proxy/gemini/gemini-3-flash-preview"}, + }, + "gpt-5.2": { + "id": "gpt-5.2", + "display_name": "GPT-5.2", + "llm_config": {"model": "litellm_proxy/openai/gpt-5.2-2025-12-11"}, + }, + "minimax-m2": { + "id": "minimax-m2", + "display_name": "MiniMax M2", + "llm_config": {"model": "litellm_proxy/minimax/minimax-m2"}, + }, + "deepseek-v3.2-reasoner": { + "id": "deepseek-v3.2-reasoner", + "display_name": "DeepSeek V3.2 Reasoner", + "llm_config": {"model": "litellm_proxy/deepseek/deepseek-v3.2"}, + }, + "qwen-3-coder": { + "id": "qwen-3-coder", + "display_name": "Qwen 3 Coder", + "llm_config": {"model": "litellm_proxy/qwen/qwen3-coder"}, + }, } @@ -64,7 +102,6 @@ def get_required_env(key: str) -> str: value = os.environ.get(key) if not value: error_exit(f"{key} not set") - raise SystemExit(1) # for type checkers; error_exit already exits return value From 1505cf227a2740ed0d7b549c5139235c2ee927d2 Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 13 Jan 2026 20:19:19 +0000 Subject: [PATCH 8/8] Simplify nested if-else in _base_slug truncation logic Flatten the elif/else branches into a single else clause using (tag_piece or ident)[:visible_budget] which handles both cases: - tag_piece exists but too long: truncate tag_piece - no tag_piece: truncate ident (which equals repo) Co-authored-by: openhands --- .../openhands/agent_server/docker/build.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/openhands-agent-server/openhands/agent_server/docker/build.py b/openhands-agent-server/openhands/agent_server/docker/build.py index fec199c129..4df53fed97 100755 --- a/openhands-agent-server/openhands/agent_server/docker/build.py +++ b/openhands-agent-server/openhands/agent_server/docker/build.py @@ -248,15 +248,11 @@ def _base_slug(image: str, max_len: int = 64) -> str: if len(ident) > visible_budget: if tag_piece and len(tag_piece) < visible_budget: - # Keep the full tag portion and trim the repo prefix if needed. - head_budget = visible_budget - len(tag_piece) - ident = repo[:head_budget] + tag_piece - elif tag_piece: - # Tag portion alone is longer than the budget; keep the leading part - # so sentinel substrings like "tag_latest" remain intact. - ident = tag_piece[:visible_budget] + # Full tag fits: trim repo to make room + ident = repo[: visible_budget - len(tag_piece)] + tag_piece else: - ident = ident[:visible_budget] + # Either no tag or tag too long: truncate from the start + ident = (tag_piece or ident)[:visible_budget] truncated = ident + suffix logger.debug(