Skip to content

Commit 7b98d96

Browse files
Rename metadata field to litellm_extra_body and add custom config support (#837)
Co-authored-by: openhands <[email protected]>
1 parent 6c7ad75 commit 7b98d96

File tree

8 files changed

+73
-64
lines changed

8 files changed

+73
-64
lines changed

openhands-sdk/openhands/sdk/agent/agent.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,13 @@ def step(
171171
include=None,
172172
store=False,
173173
add_security_risk_prediction=self._add_security_risk_prediction,
174-
metadata=self.llm.metadata,
174+
extra_body=self.llm.litellm_extra_body,
175175
)
176176
else:
177177
llm_response = self.llm.completion(
178178
messages=_messages,
179179
tools=list(self.tools_map.values()),
180-
extra_body={"metadata": self.llm.metadata},
180+
extra_body=self.llm.litellm_extra_body,
181181
add_security_risk_prediction=self._add_security_risk_prediction,
182182
)
183183
except FunctionCallValidationError as e:

openhands-sdk/openhands/sdk/context/condenser/llm_summarizing_condenser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def get_condensation(self, view: View) -> Condensation:
6767

6868
llm_response = self.llm.completion(
6969
messages=messages,
70-
extra_body={"metadata": self.llm.metadata},
70+
extra_body=self.llm.litellm_extra_body,
7171
)
7272
# Extract summary from the LLMResponse message
7373
summary = None

openhands-sdk/openhands/sdk/llm/llm.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,10 +246,12 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
246246
"telemetry, and spend tracking."
247247
),
248248
)
249-
metadata: dict[str, Any] = Field(
249+
litellm_extra_body: dict[str, Any] = Field(
250250
default_factory=dict,
251251
description=(
252-
"Additional metadata for the LLM instance. "
252+
"Additional key-value pairs to pass to litellm's extra_body parameter. "
253+
"This is useful for custom inference clusters that need additional "
254+
"metadata for logging, tracking, or routing purposes. "
253255
"Example structure: "
254256
"{'trace_version': '1.0.0', 'tags': ['model:gpt-4', 'agent:my-agent'], "
255257
"'session_id': 'session-123', 'trace_user_id': 'user-456'}"

openhands-sdk/openhands/sdk/llm/options/chat_options.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,12 @@ def select_chat_options(
7676
out.pop("tools", None)
7777
out.pop("tool_choice", None)
7878

79+
# Pass through litellm_extra_body if provided
80+
if llm.litellm_extra_body:
81+
out["extra_body"] = llm.litellm_extra_body
7982
# non litellm proxy special-case: keep `extra_body` off unless model requires it
80-
if "litellm_proxy" not in llm.model:
83+
# or user provided it
84+
elif "litellm_proxy" not in llm.model:
8185
out.pop("extra_body", None)
8286

8387
return out

openhands-sdk/openhands/sdk/llm/options/responses_options.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,8 @@ def select_responses_options(
4747
effort = llm.reasoning_effort or "high"
4848
out["reasoning"] = {"effort": effort, "summary": "detailed"}
4949

50+
# Pass through litellm_extra_body if provided
51+
if llm.litellm_extra_body:
52+
out["extra_body"] = llm.litellm_extra_body
53+
5054
return out

tests/sdk/context/condenser/test_llm_summarizing_condenser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def create_completion_result(content: str) -> LLMResponse:
6464
mock_llm.custom_tokenizer = None
6565
mock_llm.base_url = None
6666
mock_llm.reasoning_effort = None
67-
mock_llm.metadata = {}
67+
mock_llm.litellm_extra_body = {}
6868

6969
# Explicitly set pricing attributes required by LLM -> Telemetry wiring
7070
mock_llm.input_cost_per_token = None
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from unittest.mock import patch
2+
3+
from litellm.types.utils import ModelResponse
4+
5+
from openhands.sdk.llm import LLM, Message, TextContent
6+
7+
8+
def test_litellm_extra_body_passed_to_completion():
9+
"""Test that litellm_extra_body is correctly passed to litellm.completion()."""
10+
custom_extra_body = {
11+
"cluster_id": "prod-cluster-1",
12+
"routing_key": "high-priority",
13+
"user_tier": "premium",
14+
"custom_headers": {
15+
"X-Request-Source": "openhands-agent",
16+
},
17+
}
18+
19+
llm = LLM(model="gpt-4o", usage_id="test", litellm_extra_body=custom_extra_body)
20+
messages = [Message(role="user", content=[TextContent(text="Hello")])]
21+
22+
with patch("openhands.sdk.llm.llm.litellm_completion") as mock_completion:
23+
# Create a proper ModelResponse mock
24+
mock_response = ModelResponse(
25+
id="test-id",
26+
choices=[
27+
{
28+
"index": 0,
29+
"message": {"role": "assistant", "content": "Hello!"},
30+
"finish_reason": "stop",
31+
}
32+
],
33+
created=1234567890,
34+
model="gpt-4o",
35+
object="chat.completion",
36+
)
37+
mock_completion.return_value = mock_response
38+
39+
# Call completion
40+
llm.completion(messages=messages)
41+
42+
# Verify that litellm.completion was called with our extra_body
43+
mock_completion.assert_called_once()
44+
call_kwargs = mock_completion.call_args[1]
45+
46+
# Check that extra_body was passed correctly
47+
assert "extra_body" in call_kwargs
48+
assert call_kwargs["extra_body"] == custom_extra_body
49+
50+
# Verify specific custom fields were passed through
51+
assert call_kwargs["extra_body"]["cluster_id"] == "prod-cluster-1"
52+
assert call_kwargs["extra_body"]["routing_key"] == "high-priority"
53+
assert (
54+
call_kwargs["extra_body"]["custom_headers"]["X-Request-Source"]
55+
== "openhands-agent"
56+
)

tests/sdk/llm/test_llm_metadata.py

Lines changed: 0 additions & 57 deletions
This file was deleted.

0 commit comments

Comments
 (0)