openhands-sdk 1.7.4__py3-none-any.whl → 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openhands/sdk/__init__.py +2 -0
- openhands/sdk/agent/agent.py +27 -0
- openhands/sdk/agent/base.py +88 -82
- openhands/sdk/agent/prompts/system_prompt.j2 +1 -1
- openhands/sdk/agent/utils.py +3 -0
- openhands/sdk/context/agent_context.py +45 -3
- openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +4 -0
- openhands/sdk/context/prompts/templates/system_message_suffix.j2 +9 -0
- openhands/sdk/context/skills/__init__.py +12 -0
- openhands/sdk/context/skills/skill.py +275 -296
- openhands/sdk/context/skills/types.py +4 -0
- openhands/sdk/context/skills/utils.py +442 -0
- openhands/sdk/conversation/impl/local_conversation.py +42 -14
- openhands/sdk/conversation/state.py +52 -20
- openhands/sdk/event/llm_convertible/action.py +20 -0
- openhands/sdk/git/utils.py +31 -6
- openhands/sdk/hooks/conversation_hooks.py +57 -10
- openhands/sdk/llm/llm.py +58 -74
- openhands/sdk/llm/router/base.py +12 -0
- openhands/sdk/llm/utils/telemetry.py +2 -2
- openhands/sdk/plugin/__init__.py +22 -0
- openhands/sdk/plugin/plugin.py +299 -0
- openhands/sdk/plugin/types.py +226 -0
- openhands/sdk/tool/__init__.py +7 -1
- openhands/sdk/tool/builtins/__init__.py +4 -0
- openhands/sdk/tool/tool.py +60 -9
- openhands/sdk/workspace/remote/async_remote_workspace.py +16 -0
- openhands/sdk/workspace/remote/base.py +16 -0
- {openhands_sdk-1.7.4.dist-info → openhands_sdk-1.8.0.dist-info}/METADATA +1 -1
- {openhands_sdk-1.7.4.dist-info → openhands_sdk-1.8.0.dist-info}/RECORD +32 -28
- {openhands_sdk-1.7.4.dist-info → openhands_sdk-1.8.0.dist-info}/WHEEL +0 -0
- {openhands_sdk-1.7.4.dist-info → openhands_sdk-1.8.0.dist-info}/top_level.txt +0 -0
openhands/sdk/git/utils.py
CHANGED
|
@@ -73,6 +73,28 @@ def run_git_command(args: list[str], cwd: str | Path) -> str:
|
|
|
73
73
|
) from e
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
def _repo_has_commits(repo_dir: str | Path) -> bool:
|
|
77
|
+
"""Check if a git repository has any commits.
|
|
78
|
+
|
|
79
|
+
Uses 'git rev-list --count --all' which returns "0" for empty repos
|
|
80
|
+
without failing, avoiding ERROR logs for expected conditions.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
repo_dir: Path to the git repository
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
True if the repository has at least one commit, False otherwise
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
count = run_git_command(
|
|
90
|
+
["git", "--no-pager", "rev-list", "--count", "--all"], repo_dir
|
|
91
|
+
)
|
|
92
|
+
return count.strip() != "0"
|
|
93
|
+
except GitCommandError:
|
|
94
|
+
logger.debug("Could not check commit count")
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
76
98
|
def get_valid_ref(repo_dir: str | Path) -> str | None:
|
|
77
99
|
"""Get a valid git reference to compare against.
|
|
78
100
|
|
|
@@ -90,6 +112,12 @@ def get_valid_ref(repo_dir: str | Path) -> str | None:
|
|
|
90
112
|
"""
|
|
91
113
|
refs_to_try = []
|
|
92
114
|
|
|
115
|
+
# Check if repo has any commits first. Empty repos (created with git init)
|
|
116
|
+
# won't have commits or remotes, so we can skip directly to the empty tree fallback.
|
|
117
|
+
if not _repo_has_commits(repo_dir):
|
|
118
|
+
logger.debug("Repository has no commits yet, using empty tree reference")
|
|
119
|
+
return GIT_EMPTY_TREE_HASH
|
|
120
|
+
|
|
93
121
|
# Try current branch's origin
|
|
94
122
|
try:
|
|
95
123
|
current_branch = run_git_command(
|
|
@@ -136,10 +164,6 @@ def get_valid_ref(repo_dir: str | Path) -> str | None:
|
|
|
136
164
|
except GitCommandError:
|
|
137
165
|
logger.debug("Could not get remote information")
|
|
138
166
|
|
|
139
|
-
# Add empty tree as fallback for new repositories
|
|
140
|
-
refs_to_try.append(GIT_EMPTY_TREE_HASH)
|
|
141
|
-
logger.debug(f"Added empty tree reference: {GIT_EMPTY_TREE_HASH}")
|
|
142
|
-
|
|
143
167
|
# Find the first valid reference
|
|
144
168
|
for ref in refs_to_try:
|
|
145
169
|
try:
|
|
@@ -153,8 +177,9 @@ def get_valid_ref(repo_dir: str | Path) -> str | None:
|
|
|
153
177
|
logger.debug(f"Reference not valid: {ref}")
|
|
154
178
|
continue
|
|
155
179
|
|
|
156
|
-
|
|
157
|
-
|
|
180
|
+
# Fallback to empty tree hash (always valid, no verification needed)
|
|
181
|
+
logger.debug(f"Using empty tree reference: {GIT_EMPTY_TREE_HASH}")
|
|
182
|
+
return GIT_EMPTY_TREE_HASH
|
|
158
183
|
|
|
159
184
|
|
|
160
185
|
def validate_git_repository(repo_dir: str | Path) -> Path:
|
|
@@ -6,6 +6,7 @@ from openhands.sdk.event import ActionEvent, Event, MessageEvent, ObservationEve
|
|
|
6
6
|
from openhands.sdk.hooks.config import HookConfig
|
|
7
7
|
from openhands.sdk.hooks.manager import HookManager
|
|
8
8
|
from openhands.sdk.hooks.types import HookEventType
|
|
9
|
+
from openhands.sdk.llm import TextContent
|
|
9
10
|
from openhands.sdk.logger import get_logger
|
|
10
11
|
|
|
11
12
|
|
|
@@ -41,6 +42,9 @@ class HookEventProcessor:
|
|
|
41
42
|
|
|
42
43
|
def on_event(self, event: Event) -> None:
|
|
43
44
|
"""Process an event and run appropriate hooks."""
|
|
45
|
+
# Track the event to pass to callbacks (may be modified by hooks)
|
|
46
|
+
callback_event = event
|
|
47
|
+
|
|
44
48
|
# Run PreToolUse hooks for action events
|
|
45
49
|
if isinstance(event, ActionEvent) and event.action is not None:
|
|
46
50
|
self._handle_pre_tool_use(event)
|
|
@@ -51,11 +55,11 @@ class HookEventProcessor:
|
|
|
51
55
|
|
|
52
56
|
# Run UserPromptSubmit hooks for user messages
|
|
53
57
|
if isinstance(event, MessageEvent) and event.source == "user":
|
|
54
|
-
self._handle_user_prompt_submit(event)
|
|
58
|
+
callback_event = self._handle_user_prompt_submit(event)
|
|
55
59
|
|
|
56
|
-
# Call original callback
|
|
60
|
+
# Call original callback with (possibly modified) event
|
|
57
61
|
if self.original_callback:
|
|
58
|
-
self.original_callback(
|
|
62
|
+
self.original_callback(callback_event)
|
|
59
63
|
|
|
60
64
|
def _handle_pre_tool_use(self, event: ActionEvent) -> None:
|
|
61
65
|
"""Handle PreToolUse hooks. Blocked actions are marked in conversation state."""
|
|
@@ -141,16 +145,18 @@ class HookEventProcessor:
|
|
|
141
145
|
if result.error:
|
|
142
146
|
logger.warning(f"PostToolUse hook error: {result.error}")
|
|
143
147
|
|
|
144
|
-
def _handle_user_prompt_submit(self, event: MessageEvent) ->
|
|
145
|
-
"""Handle UserPromptSubmit hooks before processing a user message.
|
|
148
|
+
def _handle_user_prompt_submit(self, event: MessageEvent) -> MessageEvent:
|
|
149
|
+
"""Handle UserPromptSubmit hooks before processing a user message.
|
|
150
|
+
|
|
151
|
+
Returns the (possibly modified) event. If hooks inject additional_context,
|
|
152
|
+
a new MessageEvent is created with the context appended to extended_content.
|
|
153
|
+
"""
|
|
146
154
|
if not self.hook_manager.has_hooks(HookEventType.USER_PROMPT_SUBMIT):
|
|
147
|
-
return
|
|
155
|
+
return event
|
|
148
156
|
|
|
149
157
|
# Extract message text
|
|
150
158
|
message = ""
|
|
151
159
|
if event.llm_message and event.llm_message.content:
|
|
152
|
-
from openhands.sdk.llm import TextContent
|
|
153
|
-
|
|
154
160
|
for content in event.llm_message.content:
|
|
155
161
|
if isinstance(content, TextContent):
|
|
156
162
|
message += content.text
|
|
@@ -175,9 +181,23 @@ class HookEventProcessor:
|
|
|
175
181
|
"after creating the Conversation."
|
|
176
182
|
)
|
|
177
183
|
|
|
178
|
-
#
|
|
184
|
+
# Inject additional_context into extended_content
|
|
179
185
|
if additional_context:
|
|
180
|
-
logger.
|
|
186
|
+
logger.debug(f"Hook injecting context: {additional_context[:100]}...")
|
|
187
|
+
new_extended_content = list(event.extended_content) + [
|
|
188
|
+
TextContent(text=additional_context)
|
|
189
|
+
]
|
|
190
|
+
# MessageEvent is frozen, so create a new one
|
|
191
|
+
event = MessageEvent(
|
|
192
|
+
source=event.source,
|
|
193
|
+
llm_message=event.llm_message,
|
|
194
|
+
llm_response_id=event.llm_response_id,
|
|
195
|
+
activated_skills=event.activated_skills,
|
|
196
|
+
extended_content=new_extended_content,
|
|
197
|
+
sender=event.sender,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return event
|
|
181
201
|
|
|
182
202
|
def is_action_blocked(self, action_id: str) -> bool:
|
|
183
203
|
"""Check if an action was blocked by a hook."""
|
|
@@ -205,6 +225,33 @@ class HookEventProcessor:
|
|
|
205
225
|
if r.error:
|
|
206
226
|
logger.warning(f"SessionEnd hook error: {r.error}")
|
|
207
227
|
|
|
228
|
+
def run_stop(self, reason: str | None = None) -> tuple[bool, str | None]:
|
|
229
|
+
"""Run Stop hooks. Returns (should_stop, feedback)."""
|
|
230
|
+
if not self.hook_manager.has_hooks(HookEventType.STOP):
|
|
231
|
+
return True, None
|
|
232
|
+
|
|
233
|
+
should_stop, results = self.hook_manager.run_stop(reason=reason)
|
|
234
|
+
|
|
235
|
+
# Log any errors
|
|
236
|
+
for r in results:
|
|
237
|
+
if r.error:
|
|
238
|
+
logger.warning(f"Stop hook error: {r.error}")
|
|
239
|
+
|
|
240
|
+
# Collect feedback if denied
|
|
241
|
+
feedback = None
|
|
242
|
+
if not should_stop:
|
|
243
|
+
reason_text = self.hook_manager.get_blocking_reason(results)
|
|
244
|
+
logger.info(f"Stop hook denied stopping: {reason_text}")
|
|
245
|
+
feedback_parts = [
|
|
246
|
+
r.additional_context for r in results if r.additional_context
|
|
247
|
+
]
|
|
248
|
+
if feedback_parts:
|
|
249
|
+
feedback = "\n".join(feedback_parts)
|
|
250
|
+
elif reason_text:
|
|
251
|
+
feedback = reason_text
|
|
252
|
+
|
|
253
|
+
return should_stop, feedback
|
|
254
|
+
|
|
208
255
|
|
|
209
256
|
def create_hook_callback(
|
|
210
257
|
hook_config: HookConfig | None = None,
|
openhands/sdk/llm/llm.py
CHANGED
|
@@ -28,8 +28,6 @@ from openhands.sdk.utils.pydantic_secrets import serialize_secret, validate_secr
|
|
|
28
28
|
if TYPE_CHECKING: # type hints only, avoid runtime import cycle
|
|
29
29
|
from openhands.sdk.tool.tool import ToolDefinition
|
|
30
30
|
|
|
31
|
-
from openhands.sdk.utils.pydantic_diff import pretty_pydantic_diff
|
|
32
|
-
|
|
33
31
|
|
|
34
32
|
with warnings.catch_warnings():
|
|
35
33
|
warnings.simplefilter("ignore")
|
|
@@ -139,7 +137,12 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
|
|
|
139
137
|
retry_min_wait: int = Field(default=8, ge=0)
|
|
140
138
|
retry_max_wait: int = Field(default=64, ge=0)
|
|
141
139
|
|
|
142
|
-
timeout: int | None = Field(
|
|
140
|
+
timeout: int | None = Field(
|
|
141
|
+
default=300,
|
|
142
|
+
ge=0,
|
|
143
|
+
description="HTTP timeout in seconds. Default is 300s (5 minutes). "
|
|
144
|
+
"Set to None to disable timeout (not recommended for production).",
|
|
145
|
+
)
|
|
143
146
|
|
|
144
147
|
max_message_chars: int = Field(
|
|
145
148
|
default=30_000,
|
|
@@ -322,19 +325,6 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
|
|
|
322
325
|
exclude=True,
|
|
323
326
|
)
|
|
324
327
|
_metrics: Metrics | None = PrivateAttr(default=None)
|
|
325
|
-
# ===== Plain class vars (NOT Fields) =====
|
|
326
|
-
# When serializing, these fields (SecretStr) will be dump to "****"
|
|
327
|
-
# When deserializing, these fields will be ignored and we will override
|
|
328
|
-
# them from the LLM instance provided at runtime.
|
|
329
|
-
OVERRIDE_ON_SERIALIZE: tuple[str, ...] = (
|
|
330
|
-
"api_key",
|
|
331
|
-
"aws_access_key_id",
|
|
332
|
-
"aws_secret_access_key",
|
|
333
|
-
# Dynamic runtime metadata for telemetry/routing that can differ across sessions
|
|
334
|
-
# and should not cause resume-time diffs. Always prefer the runtime value.
|
|
335
|
-
"litellm_extra_body",
|
|
336
|
-
)
|
|
337
|
-
|
|
338
328
|
# Runtime-only private attrs
|
|
339
329
|
_model_info: Any = PrivateAttr(default=None)
|
|
340
330
|
_tokenizer: Any = PrivateAttr(default=None)
|
|
@@ -498,9 +488,21 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
|
|
|
498
488
|
This is the method for getting responses from the model via Completion API.
|
|
499
489
|
It handles message formatting, tool calling, and response processing.
|
|
500
490
|
|
|
491
|
+
Args:
|
|
492
|
+
messages: List of conversation messages
|
|
493
|
+
tools: Optional list of tools available to the model
|
|
494
|
+
_return_metrics: Whether to return usage metrics
|
|
495
|
+
add_security_risk_prediction: Add security_risk field to tool schemas
|
|
496
|
+
on_token: Optional callback for streaming tokens
|
|
497
|
+
**kwargs: Additional arguments passed to the LLM API
|
|
498
|
+
|
|
501
499
|
Returns:
|
|
502
500
|
LLMResponse containing the model's response and metadata.
|
|
503
501
|
|
|
502
|
+
Note:
|
|
503
|
+
Summary field is always added to tool schemas for transparency and
|
|
504
|
+
explainability of agent actions.
|
|
505
|
+
|
|
504
506
|
Raises:
|
|
505
507
|
ValueError: If streaming is requested (not supported).
|
|
506
508
|
|
|
@@ -528,7 +530,7 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
|
|
|
528
530
|
if tools:
|
|
529
531
|
cc_tools = [
|
|
530
532
|
t.to_openai_tool(
|
|
531
|
-
add_security_risk_prediction=add_security_risk_prediction
|
|
533
|
+
add_security_risk_prediction=add_security_risk_prediction,
|
|
532
534
|
)
|
|
533
535
|
for t in tools
|
|
534
536
|
]
|
|
@@ -550,18 +552,20 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
|
|
|
550
552
|
# Behavior-preserving: delegate to select_chat_options
|
|
551
553
|
call_kwargs = select_chat_options(self, kwargs, has_tools=has_tools_flag)
|
|
552
554
|
|
|
553
|
-
# 4)
|
|
555
|
+
# 4) request context for telemetry (always include context_window for metrics)
|
|
554
556
|
assert self._telemetry is not None
|
|
555
|
-
|
|
557
|
+
# Always pass context_window so metrics are tracked even when logging disabled
|
|
558
|
+
telemetry_ctx: dict[str, Any] = {"context_window": self.max_input_tokens or 0}
|
|
556
559
|
if self._telemetry.log_enabled:
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
560
|
+
telemetry_ctx.update(
|
|
561
|
+
{
|
|
562
|
+
"messages": formatted_messages[:], # already simple dicts
|
|
563
|
+
"tools": tools,
|
|
564
|
+
"kwargs": {k: v for k, v in call_kwargs.items()},
|
|
565
|
+
}
|
|
566
|
+
)
|
|
563
567
|
if tools and not use_native_fc:
|
|
564
|
-
|
|
568
|
+
telemetry_ctx["raw_messages"] = original_fncall_msgs
|
|
565
569
|
|
|
566
570
|
# 5) do the call with retries
|
|
567
571
|
@self.retry_decorator(
|
|
@@ -574,7 +578,7 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
|
|
|
574
578
|
)
|
|
575
579
|
def _one_attempt(**retry_kwargs) -> ModelResponse:
|
|
576
580
|
assert self._telemetry is not None
|
|
577
|
-
self._telemetry.on_request(
|
|
581
|
+
self._telemetry.on_request(telemetry_ctx=telemetry_ctx)
|
|
578
582
|
# Merge retry-modified kwargs (like temperature) with call_kwargs
|
|
579
583
|
final_kwargs = {**call_kwargs, **retry_kwargs}
|
|
580
584
|
resp = self._transport_call(
|
|
@@ -645,6 +649,20 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
|
|
|
645
649
|
"""Alternative invocation path using OpenAI Responses API via LiteLLM.
|
|
646
650
|
|
|
647
651
|
Maps Message[] -> (instructions, input[]) and returns LLMResponse.
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
messages: List of conversation messages
|
|
655
|
+
tools: Optional list of tools available to the model
|
|
656
|
+
include: Optional list of fields to include in response
|
|
657
|
+
store: Whether to store the conversation
|
|
658
|
+
_return_metrics: Whether to return usage metrics
|
|
659
|
+
add_security_risk_prediction: Add security_risk field to tool schemas
|
|
660
|
+
on_token: Optional callback for streaming tokens (not yet supported)
|
|
661
|
+
**kwargs: Additional arguments passed to the API
|
|
662
|
+
|
|
663
|
+
Note:
|
|
664
|
+
Summary field is always added to tool schemas for transparency and
|
|
665
|
+
explainability of agent actions.
|
|
648
666
|
"""
|
|
649
667
|
# Streaming not yet supported
|
|
650
668
|
if kwargs.get("stream", False) or self.stream or on_token is not None:
|
|
@@ -658,7 +676,7 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
|
|
|
658
676
|
resp_tools = (
|
|
659
677
|
[
|
|
660
678
|
t.to_responses_tool(
|
|
661
|
-
add_security_risk_prediction=add_security_risk_prediction
|
|
679
|
+
add_security_risk_prediction=add_security_risk_prediction,
|
|
662
680
|
)
|
|
663
681
|
for t in tools
|
|
664
682
|
]
|
|
@@ -671,17 +689,19 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
|
|
|
671
689
|
self, kwargs, include=include, store=store
|
|
672
690
|
)
|
|
673
691
|
|
|
674
|
-
#
|
|
692
|
+
# Request context for telemetry (always include context_window for metrics)
|
|
675
693
|
assert self._telemetry is not None
|
|
676
|
-
|
|
694
|
+
# Always pass context_window so metrics are tracked even when logging disabled
|
|
695
|
+
telemetry_ctx: dict[str, Any] = {"context_window": self.max_input_tokens or 0}
|
|
677
696
|
if self._telemetry.log_enabled:
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
697
|
+
telemetry_ctx.update(
|
|
698
|
+
{
|
|
699
|
+
"llm_path": "responses",
|
|
700
|
+
"input": input_items[:],
|
|
701
|
+
"tools": tools,
|
|
702
|
+
"kwargs": {k: v for k, v in call_kwargs.items()},
|
|
703
|
+
}
|
|
704
|
+
)
|
|
685
705
|
|
|
686
706
|
# Perform call with retries
|
|
687
707
|
@self.retry_decorator(
|
|
@@ -694,7 +714,7 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
|
|
|
694
714
|
)
|
|
695
715
|
def _one_attempt(**retry_kwargs) -> ResponsesAPIResponse:
|
|
696
716
|
assert self._telemetry is not None
|
|
697
|
-
self._telemetry.on_request(
|
|
717
|
+
self._telemetry.on_request(telemetry_ctx=telemetry_ctx)
|
|
698
718
|
final_kwargs = {**call_kwargs, **retry_kwargs}
|
|
699
719
|
with self._litellm_modify_params_ctx(self.modify_params):
|
|
700
720
|
with warnings.catch_warnings():
|
|
@@ -1101,39 +1121,3 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
|
|
|
1101
1121
|
if v is not None:
|
|
1102
1122
|
data[field_name] = v
|
|
1103
1123
|
return cls(**data)
|
|
1104
|
-
|
|
1105
|
-
def resolve_diff_from_deserialized(self, persisted: LLM) -> LLM:
|
|
1106
|
-
"""Resolve differences between a deserialized LLM and the current instance.
|
|
1107
|
-
|
|
1108
|
-
This is due to fields like api_key being serialized to "****" in dumps,
|
|
1109
|
-
and we want to ensure that when loading from a file, we still use the
|
|
1110
|
-
runtime-provided api_key in the self instance.
|
|
1111
|
-
|
|
1112
|
-
Return a new LLM instance equivalent to `persisted` but with
|
|
1113
|
-
explicitly whitelisted fields (e.g. api_key) taken from `self`.
|
|
1114
|
-
"""
|
|
1115
|
-
if persisted.__class__ is not self.__class__:
|
|
1116
|
-
raise ValueError(
|
|
1117
|
-
f"Cannot resolve_diff_from_deserialized between {self.__class__} "
|
|
1118
|
-
f"and {persisted.__class__}"
|
|
1119
|
-
)
|
|
1120
|
-
|
|
1121
|
-
# Copy allowed fields from runtime llm into the persisted llm
|
|
1122
|
-
llm_updates = {}
|
|
1123
|
-
persisted_dump = persisted.model_dump(context={"expose_secrets": True})
|
|
1124
|
-
for field in self.OVERRIDE_ON_SERIALIZE:
|
|
1125
|
-
if field in persisted_dump.keys():
|
|
1126
|
-
llm_updates[field] = getattr(self, field)
|
|
1127
|
-
if llm_updates:
|
|
1128
|
-
reconciled = persisted.model_copy(update=llm_updates)
|
|
1129
|
-
else:
|
|
1130
|
-
reconciled = persisted
|
|
1131
|
-
|
|
1132
|
-
dump = self.model_dump(context={"expose_secrets": True})
|
|
1133
|
-
reconciled_dump = reconciled.model_dump(context={"expose_secrets": True})
|
|
1134
|
-
if dump != reconciled_dump:
|
|
1135
|
-
raise ValueError(
|
|
1136
|
-
"The LLM provided is different from the one in persisted state.\n"
|
|
1137
|
-
f"Diff: {pretty_pydantic_diff(self, reconciled)}"
|
|
1138
|
-
)
|
|
1139
|
-
return reconciled
|
openhands/sdk/llm/router/base.py
CHANGED
|
@@ -59,6 +59,18 @@ class RouterLLM(LLM):
|
|
|
59
59
|
"""
|
|
60
60
|
This method intercepts completion calls and routes them to the appropriate
|
|
61
61
|
underlying LLM based on the routing logic implemented in select_llm().
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
messages: List of conversation messages
|
|
65
|
+
tools: Optional list of tools available to the model
|
|
66
|
+
return_metrics: Whether to return usage metrics
|
|
67
|
+
add_security_risk_prediction: Add security_risk field to tool schemas
|
|
68
|
+
on_token: Optional callback for streaming tokens
|
|
69
|
+
**kwargs: Additional arguments passed to the LLM API
|
|
70
|
+
|
|
71
|
+
Note:
|
|
72
|
+
Summary field is always added to tool schemas for transparency and
|
|
73
|
+
explainability of agent actions.
|
|
62
74
|
"""
|
|
63
75
|
# Select appropriate LLM
|
|
64
76
|
selected_model = self.select_llm(messages)
|
|
@@ -73,9 +73,9 @@ class Telemetry(BaseModel):
|
|
|
73
73
|
"""
|
|
74
74
|
self._stats_update_callback = callback
|
|
75
75
|
|
|
76
|
-
def on_request(self,
|
|
76
|
+
def on_request(self, telemetry_ctx: dict | None) -> None:
|
|
77
77
|
self._req_start = time.time()
|
|
78
|
-
self._req_ctx =
|
|
78
|
+
self._req_ctx = telemetry_ctx or {}
|
|
79
79
|
|
|
80
80
|
def on_response(
|
|
81
81
|
self,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Plugin module for OpenHands SDK.
|
|
2
|
+
|
|
3
|
+
This module provides support for loading and managing plugins that bundle
|
|
4
|
+
skills, hooks, MCP configurations, agents, and commands together.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from openhands.sdk.plugin.plugin import Plugin
|
|
8
|
+
from openhands.sdk.plugin.types import (
|
|
9
|
+
AgentDefinition,
|
|
10
|
+
CommandDefinition,
|
|
11
|
+
PluginAuthor,
|
|
12
|
+
PluginManifest,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Plugin",
|
|
18
|
+
"PluginManifest",
|
|
19
|
+
"PluginAuthor",
|
|
20
|
+
"AgentDefinition",
|
|
21
|
+
"CommandDefinition",
|
|
22
|
+
]
|