Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/run-eval.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ on:
sdk_ref:
description: SDK commit/ref to evaluate (must be a semantic version like v1.0.0 unless 'Allow unreleased branches' is checked)
required: true
default: v1.27.0
default: v1.27.1




Expand Down
2 changes: 1 addition & 1 deletion openhands-agent-server/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "openhands-agent-server"
version = "1.27.0"
version = "1.27.1"
description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"

requires-python = ">=3.12"
Expand Down
5 changes: 3 additions & 2 deletions openhands-sdk/openhands/sdk/llm/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2100,8 +2100,9 @@ def _apply_prompt_caching(self, messages: list[Message]) -> None:
# Single block: mark it for caching
sys_content[0].cache_prompt = True

# Anthropic and Gemini both use these cache_control markers. LiteLLM
# performs the provider-specific cache setup for Gemini downstream.
# Second breakpoint: mark the last user/tool message so the cached prefix
# extends every turn. Anthropic-only; Gemini is excluded from
# PROMPT_CACHE_MODELS because its cache can't extend this way.
for message in reversed(messages):
if message.role in ("user", "tool"):
message.content[
Expand Down
7 changes: 3 additions & 4 deletions openhands-sdk/openhands/sdk/llm/utils/model_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,9 @@ def _supports_reasoning_effort(model: str | None) -> bool:
"claude-opus-4-7",
"claude-opus-4-8",
"claude-sonnet-4-6",
# Gemini uses the same cache_control marker format. LiteLLM handles
# Vertex/Gemini context-cache creation when these markers are present.
"gemini-2.5",
"gemini-3",
# Do NOT add Gemini: explicit cache_control markers freeze its cache at the
# static prefix and disable Google's implicit caching on the growing body
# (~6-14x cost). Gemini uses implicit prefix caching instead.
]

# Models that support a top-level prompt_cache_retention parameter
Expand Down
2 changes: 1 addition & 1 deletion openhands-sdk/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "openhands-sdk"
version = "1.27.0"
version = "1.27.1"
description = "OpenHands SDK - Core functionality for building AI agents"

requires-python = ">=3.12"
Expand Down
2 changes: 1 addition & 1 deletion openhands-tools/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "openhands-tools"
version = "1.27.0"
version = "1.27.1"
description = "OpenHands Tools - Runtime tools for AI agents"

requires-python = ">=3.12"
Expand Down
2 changes: 1 addition & 1 deletion openhands-workspace/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "openhands-workspace"
version = "1.27.0"
version = "1.27.1"
description = "OpenHands Workspace - Docker and container-based workspace implementations"

requires-python = ">=3.12"
Expand Down
9 changes: 5 additions & 4 deletions tests/sdk/llm/test_model_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,11 @@ def test_extended_thinking_support(model, expected_extended_thinking):
("anthropic.claude-3-5-sonnet-20241022", True),
("anthropic.claude-3-haiku-20240307", True),
("anthropic.claude-3-opus-20240229", True),
# Gemini explicit context caching through LiteLLM.
("gemini-2.5-pro", True),
("gemini-3.1-pro-preview", True),
("litellm_proxy/gemini-3.1-pro-preview", True),
# Gemini must NOT use explicit cache_control markers: they freeze the
# cache at the static prefix and disable Google's implicit caching.
("gemini-2.5-pro", False),
("gemini-3.1-pro-preview", False),
("litellm_proxy/gemini-3.1-pro-preview", False),
("gpt-4o", False), # OpenAI doesn't support explicit prompt caching
("gemini-1.5-pro", False),
("unknown-model", False),
Expand Down
35 changes: 24 additions & 11 deletions tests/sdk/llm/test_prompt_caching_cross_conversation.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,24 @@ def on_event(event):
assert messages[1].content[-1].cache_prompt is True


def test_gemini_prompt_caching_marks_formatted_messages():
"""Gemini models should emit cache_control markers when caching is enabled."""
def test_gemini_prompt_caching_emits_no_markers():
"""REGRESSION: Gemini must not emit explicit cache_control markers.

Explicit markers freeze Gemini's cache at the static prefix and disable
Google's implicit caching on the growing body (~6-14x cost). No markers keeps
Gemini on the implicit-caching path, where the cached prefix grows.
"""
llm = LLM(
model="litellm_proxy/gemini-3.1-pro-preview",
usage_id="test",
caching_prompt=True,
)

# Explicit-breakpoint caching must be inactive for Gemini.
assert llm.is_caching_prompt_active() is False

# System (index 0) and last user message (index 3) are non-adjacent — the
# case that froze the LiteLLM/Vertex cache at the static prefix.
messages = [
Message(
role="system",
Expand All @@ -159,19 +170,21 @@ def test_gemini_prompt_caching_marks_formatted_messages():
TextContent(text="Dynamic context"),
],
),
Message(
role="user",
content=[TextContent(text="Hello")],
),
Message(role="user", content=[TextContent(text="First question")]),
Message(role="assistant", content=[TextContent(text="First answer")]),
Message(role="user", content=[TextContent(text="Second question")]),
]

formatted_messages = llm.format_messages_for_llm(messages)

system_content = formatted_messages[0]["content"]
user_content = formatted_messages[1]["content"]
assert system_content[0]["cache_control"] == {"type": "ephemeral"}
assert "cache_control" not in system_content[1]
assert user_content[-1]["cache_control"] == {"type": "ephemeral"}
# No cache_control marker anywhere in the formatted payload.
for message in formatted_messages:
assert "cache_control" not in message
content = message.get("content")
if isinstance(content, list):
for block in content:
if isinstance(block, dict):
assert "cache_control" not in block


@pytest.mark.parametrize(
Expand Down
7 changes: 5 additions & 2 deletions tests/sdk/llm/test_responses_parsing_and_kwargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,8 +338,9 @@ def test_responses_retries_without_caching_on_prompt_cache_too_small(mock_respon

# Pick a model that supports prompt caching so is_caching_prompt_active()
# is True and the retry branch is reachable on the responses() path.
# (Gemini no longer uses explicit caching, so use an Anthropic model here.)
llm = LLM(
model="gemini-3-flash",
model="claude-sonnet-4-20250514",
api_key=SecretStr("test_key"),
usage_id="test-llm",
caching_prompt=True,
Expand Down Expand Up @@ -417,8 +418,10 @@ async def test_aresponses_retries_without_caching_on_prompt_cache_too_small(
)
mock_aresponses.side_effect = [cache_error, success_resp]

# Anthropic model so is_caching_prompt_active() is True (Gemini no longer
# uses explicit caching); mirrors the sync test above.
llm = LLM(
model="gemini-3-flash",
model="claude-sonnet-4-20250514",
api_key=SecretStr("test_key"),
usage_id="test-llm",
caching_prompt=True,
Expand Down
10 changes: 5 additions & 5 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading