openhands-sdk 1.7.3__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.
Files changed (180) hide show
  1. openhands/sdk/__init__.py +111 -0
  2. openhands/sdk/agent/__init__.py +8 -0
  3. openhands/sdk/agent/agent.py +650 -0
  4. openhands/sdk/agent/base.py +457 -0
  5. openhands/sdk/agent/prompts/in_context_learning_example.j2 +169 -0
  6. openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +3 -0
  7. openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +3 -0
  8. openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +1 -0
  9. openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +2 -0
  10. openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +3 -0
  11. openhands/sdk/agent/prompts/security_policy.j2 +22 -0
  12. openhands/sdk/agent/prompts/security_risk_assessment.j2 +21 -0
  13. openhands/sdk/agent/prompts/self_documentation.j2 +15 -0
  14. openhands/sdk/agent/prompts/system_prompt.j2 +132 -0
  15. openhands/sdk/agent/prompts/system_prompt_interactive.j2 +14 -0
  16. openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +40 -0
  17. openhands/sdk/agent/prompts/system_prompt_planning.j2 +40 -0
  18. openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +122 -0
  19. openhands/sdk/agent/utils.py +228 -0
  20. openhands/sdk/context/__init__.py +28 -0
  21. openhands/sdk/context/agent_context.py +264 -0
  22. openhands/sdk/context/condenser/__init__.py +18 -0
  23. openhands/sdk/context/condenser/base.py +100 -0
  24. openhands/sdk/context/condenser/llm_summarizing_condenser.py +248 -0
  25. openhands/sdk/context/condenser/no_op_condenser.py +14 -0
  26. openhands/sdk/context/condenser/pipeline_condenser.py +56 -0
  27. openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +59 -0
  28. openhands/sdk/context/condenser/utils.py +149 -0
  29. openhands/sdk/context/prompts/__init__.py +6 -0
  30. openhands/sdk/context/prompts/prompt.py +114 -0
  31. openhands/sdk/context/prompts/templates/ask_agent_template.j2 +11 -0
  32. openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +8 -0
  33. openhands/sdk/context/prompts/templates/system_message_suffix.j2 +32 -0
  34. openhands/sdk/context/skills/__init__.py +28 -0
  35. openhands/sdk/context/skills/exceptions.py +11 -0
  36. openhands/sdk/context/skills/skill.py +720 -0
  37. openhands/sdk/context/skills/trigger.py +36 -0
  38. openhands/sdk/context/skills/types.py +48 -0
  39. openhands/sdk/context/view.py +503 -0
  40. openhands/sdk/conversation/__init__.py +40 -0
  41. openhands/sdk/conversation/base.py +281 -0
  42. openhands/sdk/conversation/conversation.py +152 -0
  43. openhands/sdk/conversation/conversation_stats.py +85 -0
  44. openhands/sdk/conversation/event_store.py +157 -0
  45. openhands/sdk/conversation/events_list_base.py +17 -0
  46. openhands/sdk/conversation/exceptions.py +50 -0
  47. openhands/sdk/conversation/fifo_lock.py +133 -0
  48. openhands/sdk/conversation/impl/__init__.py +5 -0
  49. openhands/sdk/conversation/impl/local_conversation.py +665 -0
  50. openhands/sdk/conversation/impl/remote_conversation.py +956 -0
  51. openhands/sdk/conversation/persistence_const.py +9 -0
  52. openhands/sdk/conversation/response_utils.py +41 -0
  53. openhands/sdk/conversation/secret_registry.py +126 -0
  54. openhands/sdk/conversation/serialization_diff.py +0 -0
  55. openhands/sdk/conversation/state.py +392 -0
  56. openhands/sdk/conversation/stuck_detector.py +311 -0
  57. openhands/sdk/conversation/title_utils.py +191 -0
  58. openhands/sdk/conversation/types.py +45 -0
  59. openhands/sdk/conversation/visualizer/__init__.py +12 -0
  60. openhands/sdk/conversation/visualizer/base.py +67 -0
  61. openhands/sdk/conversation/visualizer/default.py +373 -0
  62. openhands/sdk/critic/__init__.py +15 -0
  63. openhands/sdk/critic/base.py +38 -0
  64. openhands/sdk/critic/impl/__init__.py +12 -0
  65. openhands/sdk/critic/impl/agent_finished.py +83 -0
  66. openhands/sdk/critic/impl/empty_patch.py +49 -0
  67. openhands/sdk/critic/impl/pass_critic.py +42 -0
  68. openhands/sdk/event/__init__.py +42 -0
  69. openhands/sdk/event/base.py +149 -0
  70. openhands/sdk/event/condenser.py +82 -0
  71. openhands/sdk/event/conversation_error.py +25 -0
  72. openhands/sdk/event/conversation_state.py +104 -0
  73. openhands/sdk/event/llm_completion_log.py +39 -0
  74. openhands/sdk/event/llm_convertible/__init__.py +20 -0
  75. openhands/sdk/event/llm_convertible/action.py +139 -0
  76. openhands/sdk/event/llm_convertible/message.py +142 -0
  77. openhands/sdk/event/llm_convertible/observation.py +141 -0
  78. openhands/sdk/event/llm_convertible/system.py +61 -0
  79. openhands/sdk/event/token.py +16 -0
  80. openhands/sdk/event/types.py +11 -0
  81. openhands/sdk/event/user_action.py +21 -0
  82. openhands/sdk/git/exceptions.py +43 -0
  83. openhands/sdk/git/git_changes.py +249 -0
  84. openhands/sdk/git/git_diff.py +129 -0
  85. openhands/sdk/git/models.py +21 -0
  86. openhands/sdk/git/utils.py +189 -0
  87. openhands/sdk/hooks/__init__.py +30 -0
  88. openhands/sdk/hooks/config.py +180 -0
  89. openhands/sdk/hooks/conversation_hooks.py +227 -0
  90. openhands/sdk/hooks/executor.py +155 -0
  91. openhands/sdk/hooks/manager.py +170 -0
  92. openhands/sdk/hooks/types.py +40 -0
  93. openhands/sdk/io/__init__.py +6 -0
  94. openhands/sdk/io/base.py +48 -0
  95. openhands/sdk/io/cache.py +85 -0
  96. openhands/sdk/io/local.py +119 -0
  97. openhands/sdk/io/memory.py +54 -0
  98. openhands/sdk/llm/__init__.py +45 -0
  99. openhands/sdk/llm/exceptions/__init__.py +45 -0
  100. openhands/sdk/llm/exceptions/classifier.py +50 -0
  101. openhands/sdk/llm/exceptions/mapping.py +54 -0
  102. openhands/sdk/llm/exceptions/types.py +101 -0
  103. openhands/sdk/llm/llm.py +1140 -0
  104. openhands/sdk/llm/llm_registry.py +122 -0
  105. openhands/sdk/llm/llm_response.py +59 -0
  106. openhands/sdk/llm/message.py +656 -0
  107. openhands/sdk/llm/mixins/fn_call_converter.py +1288 -0
  108. openhands/sdk/llm/mixins/non_native_fc.py +97 -0
  109. openhands/sdk/llm/options/__init__.py +1 -0
  110. openhands/sdk/llm/options/chat_options.py +93 -0
  111. openhands/sdk/llm/options/common.py +19 -0
  112. openhands/sdk/llm/options/responses_options.py +67 -0
  113. openhands/sdk/llm/router/__init__.py +10 -0
  114. openhands/sdk/llm/router/base.py +117 -0
  115. openhands/sdk/llm/router/impl/multimodal.py +76 -0
  116. openhands/sdk/llm/router/impl/random.py +22 -0
  117. openhands/sdk/llm/streaming.py +9 -0
  118. openhands/sdk/llm/utils/metrics.py +312 -0
  119. openhands/sdk/llm/utils/model_features.py +192 -0
  120. openhands/sdk/llm/utils/model_info.py +90 -0
  121. openhands/sdk/llm/utils/model_prompt_spec.py +98 -0
  122. openhands/sdk/llm/utils/retry_mixin.py +128 -0
  123. openhands/sdk/llm/utils/telemetry.py +362 -0
  124. openhands/sdk/llm/utils/unverified_models.py +156 -0
  125. openhands/sdk/llm/utils/verified_models.py +65 -0
  126. openhands/sdk/logger/__init__.py +22 -0
  127. openhands/sdk/logger/logger.py +195 -0
  128. openhands/sdk/logger/rolling.py +113 -0
  129. openhands/sdk/mcp/__init__.py +24 -0
  130. openhands/sdk/mcp/client.py +76 -0
  131. openhands/sdk/mcp/definition.py +106 -0
  132. openhands/sdk/mcp/exceptions.py +19 -0
  133. openhands/sdk/mcp/tool.py +270 -0
  134. openhands/sdk/mcp/utils.py +83 -0
  135. openhands/sdk/observability/__init__.py +4 -0
  136. openhands/sdk/observability/laminar.py +166 -0
  137. openhands/sdk/observability/utils.py +20 -0
  138. openhands/sdk/py.typed +0 -0
  139. openhands/sdk/secret/__init__.py +19 -0
  140. openhands/sdk/secret/secrets.py +92 -0
  141. openhands/sdk/security/__init__.py +6 -0
  142. openhands/sdk/security/analyzer.py +111 -0
  143. openhands/sdk/security/confirmation_policy.py +61 -0
  144. openhands/sdk/security/llm_analyzer.py +29 -0
  145. openhands/sdk/security/risk.py +100 -0
  146. openhands/sdk/tool/__init__.py +34 -0
  147. openhands/sdk/tool/builtins/__init__.py +34 -0
  148. openhands/sdk/tool/builtins/finish.py +106 -0
  149. openhands/sdk/tool/builtins/think.py +117 -0
  150. openhands/sdk/tool/registry.py +184 -0
  151. openhands/sdk/tool/schema.py +286 -0
  152. openhands/sdk/tool/spec.py +39 -0
  153. openhands/sdk/tool/tool.py +481 -0
  154. openhands/sdk/utils/__init__.py +22 -0
  155. openhands/sdk/utils/async_executor.py +115 -0
  156. openhands/sdk/utils/async_utils.py +39 -0
  157. openhands/sdk/utils/cipher.py +68 -0
  158. openhands/sdk/utils/command.py +90 -0
  159. openhands/sdk/utils/deprecation.py +166 -0
  160. openhands/sdk/utils/github.py +44 -0
  161. openhands/sdk/utils/json.py +48 -0
  162. openhands/sdk/utils/models.py +570 -0
  163. openhands/sdk/utils/paging.py +63 -0
  164. openhands/sdk/utils/pydantic_diff.py +85 -0
  165. openhands/sdk/utils/pydantic_secrets.py +64 -0
  166. openhands/sdk/utils/truncate.py +117 -0
  167. openhands/sdk/utils/visualize.py +58 -0
  168. openhands/sdk/workspace/__init__.py +17 -0
  169. openhands/sdk/workspace/base.py +158 -0
  170. openhands/sdk/workspace/local.py +189 -0
  171. openhands/sdk/workspace/models.py +35 -0
  172. openhands/sdk/workspace/remote/__init__.py +8 -0
  173. openhands/sdk/workspace/remote/async_remote_workspace.py +149 -0
  174. openhands/sdk/workspace/remote/base.py +164 -0
  175. openhands/sdk/workspace/remote/remote_workspace_mixin.py +323 -0
  176. openhands/sdk/workspace/workspace.py +49 -0
  177. openhands_sdk-1.7.3.dist-info/METADATA +17 -0
  178. openhands_sdk-1.7.3.dist-info/RECORD +180 -0
  179. openhands_sdk-1.7.3.dist-info/WHEEL +5 -0
  180. openhands_sdk-1.7.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,85 @@
1
+ from typing import Any
2
+
3
+ from cachetools import LRUCache
4
+
5
+ from openhands.sdk.logger import get_logger
6
+
7
+
8
+ logger = get_logger(__name__)
9
+
10
+
11
+ class MemoryLRUCache(LRUCache):
12
+ """LRU cache with both entry count and memory size limits.
13
+
14
+ This cache enforces two limits:
15
+ 1. Maximum number of entries (maxsize)
16
+ 2. Maximum memory usage in bytes (max_memory)
17
+
18
+ When either limit is exceeded, the least recently used items are evicted.
19
+
20
+ Note: Memory tracking is based on string length for simplicity and accuracy.
21
+ For non-string values, sys.getsizeof is used as a rough approximation.
22
+ """
23
+
24
+ def __init__(self, max_memory: int, max_size: int, *args, **kwargs):
25
+ # Ensure minimum maxsize of 1 to avoid LRUCache issues
26
+ maxsize = max(1, max_size)
27
+ super().__init__(maxsize=maxsize, *args, **kwargs)
28
+ self.max_memory = max_memory
29
+ self.current_memory = 0
30
+
31
+ def _get_size(self, value: Any) -> int:
32
+ """Calculate size of value for memory tracking.
33
+
34
+ For strings (the common case in FileStore), we use len() which gives
35
+ accurate character count. For other types, we use sys.getsizeof() as
36
+ a rough approximation.
37
+ """
38
+ if isinstance(value, str):
39
+ # For strings, len() gives character count which is what we care about
40
+ # This is much more accurate than sys.getsizeof for our use case
41
+ return len(value)
42
+ elif isinstance(value, bytes):
43
+ return len(value)
44
+ else:
45
+ # For other types, fall back to sys.getsizeof
46
+ # This is mainly for edge cases and won't be accurate for nested
47
+ # structures, but it's better than nothing
48
+ try:
49
+ import sys
50
+
51
+ return sys.getsizeof(value)
52
+ except Exception:
53
+ return 0
54
+
55
+ def __setitem__(self, key: Any, value: Any) -> None:
56
+ new_size = self._get_size(value)
57
+
58
+ # Don't cache items that are larger than max_memory
59
+ # This prevents cache thrashing where one huge item evicts everything
60
+ if new_size > self.max_memory:
61
+ logger.debug(
62
+ f"Item too large for cache ({new_size} bytes > "
63
+ f"{self.max_memory} bytes), skipping cache"
64
+ )
65
+ return
66
+
67
+ # Update memory accounting if key exists
68
+ if key in self:
69
+ old_value = self[key]
70
+ self.current_memory -= self._get_size(old_value)
71
+
72
+ self.current_memory += new_size
73
+
74
+ # Evict items until we're under memory limit
75
+ while self.current_memory > self.max_memory and len(self) > 0:
76
+ self.popitem()
77
+
78
+ super().__setitem__(key, value)
79
+
80
+ def __delitem__(self, key: Any) -> None:
81
+ if key in self:
82
+ old_value = self[key]
83
+ self.current_memory -= self._get_size(old_value)
84
+
85
+ super().__delitem__(key)
@@ -0,0 +1,119 @@
1
+ import os
2
+ import shutil
3
+
4
+ from openhands.sdk.io.cache import MemoryLRUCache
5
+ from openhands.sdk.logger import get_logger
6
+ from openhands.sdk.observability.laminar import observe
7
+
8
+ from .base import FileStore
9
+
10
+
11
+ logger = get_logger(__name__)
12
+
13
+
14
+ class LocalFileStore(FileStore):
15
+ root: str
16
+ cache: MemoryLRUCache
17
+
18
+ def __init__(
19
+ self,
20
+ root: str,
21
+ cache_limit_size: int = 500,
22
+ cache_memory_size: int = 20 * 1024 * 1024,
23
+ ) -> None:
24
+ """Initialize a LocalFileStore with caching.
25
+
26
+ Args:
27
+ root: Root directory for file storage.
28
+ cache_limit_size: Maximum number of cached entries (default: 500).
29
+ cache_memory_size: Maximum cache memory in bytes (default: 20MB).
30
+
31
+ Note:
32
+ The cache assumes exclusive access to files. External modifications
33
+ to files will not be detected and may result in stale cache reads.
34
+ """
35
+ if root.startswith("~"):
36
+ root = os.path.expanduser(root)
37
+ root = os.path.abspath(os.path.normpath(root))
38
+ self.root = root
39
+ os.makedirs(self.root, exist_ok=True)
40
+ self.cache = MemoryLRUCache(cache_memory_size, cache_limit_size)
41
+
42
+ def get_full_path(self, path: str) -> str:
43
+ # strip leading slash to keep relative under root
44
+ if path.startswith("/"):
45
+ path = path[1:]
46
+ # normalize path separators to handle both Unix (/) and Windows (\) styles
47
+ normalized_path = path.replace("\\", "/")
48
+ full = os.path.abspath(
49
+ os.path.normpath(os.path.join(self.root, normalized_path))
50
+ )
51
+ # ensure sandboxing
52
+ if os.path.commonpath([self.root, full]) != self.root:
53
+ raise ValueError(f"path escapes filestore root: {path}")
54
+
55
+ return full
56
+
57
+ @observe(name="LocalFileStore.write", span_type="TOOL")
58
+ def write(self, path: str, contents: str | bytes) -> None:
59
+ full_path = self.get_full_path(path)
60
+ os.makedirs(os.path.dirname(full_path), exist_ok=True)
61
+ if isinstance(contents, str):
62
+ with open(full_path, "w", encoding="utf-8") as f:
63
+ f.write(contents)
64
+ self.cache[full_path] = contents
65
+ else:
66
+ with open(full_path, "wb") as f:
67
+ f.write(contents)
68
+ # Don't cache binary content - LocalFileStore is meant for JSON data
69
+ # If binary data is written and then read, it will error on read
70
+
71
+ def read(self, path: str) -> str:
72
+ full_path = self.get_full_path(path)
73
+
74
+ if full_path in self.cache:
75
+ return self.cache[full_path]
76
+
77
+ if not os.path.exists(full_path):
78
+ raise FileNotFoundError(path)
79
+
80
+ with open(full_path, encoding="utf-8") as f:
81
+ result = f.read()
82
+
83
+ self.cache[full_path] = result
84
+ return result
85
+
86
+ @observe(name="LocalFileStore.list", span_type="TOOL")
87
+ def list(self, path: str) -> list[str]:
88
+ full_path = self.get_full_path(path)
89
+ if not os.path.exists(full_path):
90
+ return []
91
+
92
+ # If path is a file, return the file itself (S3-consistent behavior)
93
+ if os.path.isfile(full_path):
94
+ return [path]
95
+
96
+ # Otherwise it's a directory, return its contents
97
+ files = [os.path.join(path, f) for f in os.listdir(full_path)]
98
+ files = [f + "/" if os.path.isdir(self.get_full_path(f)) else f for f in files]
99
+ return files
100
+
101
+ @observe(name="LocalFileStore.delete", span_type="TOOL")
102
+ def delete(self, path: str) -> None:
103
+ try:
104
+ full_path = self.get_full_path(path)
105
+ if not os.path.exists(full_path):
106
+ logger.debug(f"Local path does not exist: {full_path}")
107
+ return
108
+
109
+ if os.path.isfile(full_path):
110
+ os.remove(full_path)
111
+ del self.cache[full_path]
112
+ logger.debug(f"Removed local file: {full_path}")
113
+ elif os.path.isdir(full_path):
114
+ shutil.rmtree(full_path)
115
+ self.cache.clear()
116
+ logger.debug(f"Removed local directory: {full_path}")
117
+
118
+ except Exception as e:
119
+ logger.error(f"Error clearing local file store: {str(e)}")
@@ -0,0 +1,54 @@
1
+ import os
2
+
3
+ from openhands.sdk.io.base import FileStore
4
+ from openhands.sdk.logger import get_logger
5
+
6
+
7
+ logger = get_logger(__name__)
8
+
9
+
10
+ class InMemoryFileStore(FileStore):
11
+ files: dict[str, str]
12
+
13
+ def __init__(self, files: dict[str, str] | None = None) -> None:
14
+ self.files = {}
15
+ if files is not None:
16
+ self.files = files
17
+
18
+ def write(self, path: str, contents: str | bytes) -> None:
19
+ if isinstance(contents, bytes):
20
+ contents = contents.decode("utf-8")
21
+ self.files[path] = contents
22
+
23
+ def read(self, path: str) -> str:
24
+ if path not in self.files:
25
+ raise FileNotFoundError(path)
26
+ return self.files[path]
27
+
28
+ def list(self, path: str) -> list[str]:
29
+ files = []
30
+ for file in self.files:
31
+ if not file.startswith(path):
32
+ continue
33
+ suffix = file.removeprefix(path)
34
+ parts = suffix.split("/")
35
+ if parts[0] == "":
36
+ parts.pop(0)
37
+ if len(parts) == 1:
38
+ files.append(file)
39
+ else:
40
+ dir_path = os.path.join(path, parts[0])
41
+ if not dir_path.endswith("/"):
42
+ dir_path += "/"
43
+ if dir_path not in files:
44
+ files.append(dir_path)
45
+ return files
46
+
47
+ def delete(self, path: str) -> None:
48
+ try:
49
+ keys_to_delete = [key for key in self.files.keys() if key.startswith(path)]
50
+ for key in keys_to_delete:
51
+ del self.files[key]
52
+ logger.debug(f"Cleared in-memory file store: {path}")
53
+ except Exception as e:
54
+ logger.error(f"Error clearing in-memory file store: {str(e)}")
@@ -0,0 +1,45 @@
1
+ from openhands.sdk.llm.llm import LLM
2
+ from openhands.sdk.llm.llm_registry import LLMRegistry, RegistryEvent
3
+ from openhands.sdk.llm.llm_response import LLMResponse
4
+ from openhands.sdk.llm.message import (
5
+ ImageContent,
6
+ Message,
7
+ MessageToolCall,
8
+ ReasoningItemModel,
9
+ RedactedThinkingBlock,
10
+ TextContent,
11
+ ThinkingBlock,
12
+ content_to_str,
13
+ )
14
+ from openhands.sdk.llm.router import RouterLLM
15
+ from openhands.sdk.llm.streaming import LLMStreamChunk, TokenCallbackType
16
+ from openhands.sdk.llm.utils.metrics import Metrics, MetricsSnapshot
17
+ from openhands.sdk.llm.utils.unverified_models import (
18
+ UNVERIFIED_MODELS_EXCLUDING_BEDROCK,
19
+ get_unverified_models,
20
+ )
21
+ from openhands.sdk.llm.utils.verified_models import VERIFIED_MODELS
22
+
23
+
24
+ __all__ = [
25
+ "LLMResponse",
26
+ "LLM",
27
+ "LLMRegistry",
28
+ "RouterLLM",
29
+ "RegistryEvent",
30
+ "Message",
31
+ "MessageToolCall",
32
+ "TextContent",
33
+ "ImageContent",
34
+ "ThinkingBlock",
35
+ "RedactedThinkingBlock",
36
+ "ReasoningItemModel",
37
+ "content_to_str",
38
+ "LLMStreamChunk",
39
+ "TokenCallbackType",
40
+ "Metrics",
41
+ "MetricsSnapshot",
42
+ "VERIFIED_MODELS",
43
+ "UNVERIFIED_MODELS_EXCLUDING_BEDROCK",
44
+ "get_unverified_models",
45
+ ]
@@ -0,0 +1,45 @@
1
+ from .classifier import is_context_window_exceeded, looks_like_auth_error
2
+ from .mapping import map_provider_exception
3
+ from .types import (
4
+ FunctionCallConversionError,
5
+ FunctionCallNotExistsError,
6
+ FunctionCallValidationError,
7
+ LLMAuthenticationError,
8
+ LLMBadRequestError,
9
+ LLMContextWindowExceedError,
10
+ LLMError,
11
+ LLMMalformedActionError,
12
+ LLMNoActionError,
13
+ LLMNoResponseError,
14
+ LLMRateLimitError,
15
+ LLMResponseError,
16
+ LLMServiceUnavailableError,
17
+ LLMTimeoutError,
18
+ OperationCancelled,
19
+ UserCancelledError,
20
+ )
21
+
22
+
23
+ __all__ = [
24
+ # Types
25
+ "LLMError",
26
+ "LLMMalformedActionError",
27
+ "LLMNoActionError",
28
+ "LLMResponseError",
29
+ "FunctionCallConversionError",
30
+ "FunctionCallValidationError",
31
+ "FunctionCallNotExistsError",
32
+ "LLMNoResponseError",
33
+ "LLMContextWindowExceedError",
34
+ "LLMAuthenticationError",
35
+ "LLMRateLimitError",
36
+ "LLMTimeoutError",
37
+ "LLMServiceUnavailableError",
38
+ "LLMBadRequestError",
39
+ "UserCancelledError",
40
+ "OperationCancelled",
41
+ # Helpers
42
+ "is_context_window_exceeded",
43
+ "looks_like_auth_error",
44
+ "map_provider_exception",
45
+ ]
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from litellm.exceptions import BadRequestError, ContextWindowExceededError, OpenAIError
4
+
5
+ from .types import LLMContextWindowExceedError
6
+
7
+
8
+ # Minimal, provider-agnostic context-window detection
9
+ LONG_PROMPT_PATTERNS: list[str] = [
10
+ "contextwindowexceedederror",
11
+ "prompt is too long",
12
+ "input length and `max_tokens` exceed context limit",
13
+ "please reduce the length of",
14
+ "the request exceeds the available context size",
15
+ "context length exceeded",
16
+ "input exceeds the context window",
17
+ ]
18
+
19
+
20
+ def is_context_window_exceeded(exception: Exception) -> bool:
21
+ if isinstance(exception, (ContextWindowExceededError, LLMContextWindowExceedError)):
22
+ return True
23
+
24
+ if not isinstance(exception, (BadRequestError, OpenAIError)):
25
+ return False
26
+
27
+ s = str(exception).lower()
28
+ return any(p in s for p in LONG_PROMPT_PATTERNS)
29
+
30
+
31
+ AUTH_PATTERNS: list[str] = [
32
+ "invalid api key",
33
+ "unauthorized",
34
+ "missing api key",
35
+ "invalid authentication",
36
+ "access denied",
37
+ ]
38
+
39
+
40
+ def looks_like_auth_error(exception: Exception) -> bool:
41
+ if not isinstance(exception, (BadRequestError, OpenAIError)):
42
+ return False
43
+ s = str(exception).lower()
44
+ if any(p in s for p in AUTH_PATTERNS):
45
+ return True
46
+ # Some providers include explicit status codes in message text
47
+ for code in ("status 401", "status 403"):
48
+ if code in s:
49
+ return True
50
+ return False
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from litellm.exceptions import (
4
+ APIConnectionError,
5
+ BadRequestError,
6
+ InternalServerError,
7
+ RateLimitError,
8
+ ServiceUnavailableError,
9
+ Timeout as LiteLLMTimeout,
10
+ )
11
+
12
+ from .classifier import is_context_window_exceeded, looks_like_auth_error
13
+ from .types import (
14
+ LLMAuthenticationError,
15
+ LLMBadRequestError,
16
+ LLMContextWindowExceedError,
17
+ LLMRateLimitError,
18
+ LLMServiceUnavailableError,
19
+ LLMTimeoutError,
20
+ )
21
+
22
+
23
+ def map_provider_exception(exception: Exception) -> Exception:
24
+ """
25
+ Map provider/LiteLLM exceptions to SDK-typed exceptions.
26
+
27
+ Returns original exception if no mapping applies.
28
+ """
29
+ # Context window exceeded first (highest priority)
30
+ if is_context_window_exceeded(exception):
31
+ return LLMContextWindowExceedError(str(exception))
32
+
33
+ # Auth-like errors often appear as BadRequest/OpenAIError with specific text
34
+ if looks_like_auth_error(exception):
35
+ return LLMAuthenticationError(str(exception))
36
+
37
+ if isinstance(exception, RateLimitError):
38
+ return LLMRateLimitError(str(exception))
39
+
40
+ if isinstance(exception, LiteLLMTimeout):
41
+ return LLMTimeoutError(str(exception))
42
+
43
+ # Connectivity and service-side availability issues → service unavailable
44
+ if isinstance(
45
+ exception, (APIConnectionError, ServiceUnavailableError, InternalServerError)
46
+ ):
47
+ return LLMServiceUnavailableError(str(exception))
48
+
49
+ # Generic client-side 4xx errors
50
+ if isinstance(exception, BadRequestError):
51
+ return LLMBadRequestError(str(exception))
52
+
53
+ # Unknown: let caller re-raise original
54
+ return exception
@@ -0,0 +1,101 @@
1
+ class LLMError(Exception):
2
+ message: str
3
+
4
+ def __init__(self, message: str) -> None:
5
+ super().__init__(message)
6
+ self.message = message
7
+
8
+ def __str__(self) -> str:
9
+ return self.message
10
+
11
+
12
+ # General response parsing/validation errors
13
+ class LLMMalformedActionError(LLMError):
14
+ def __init__(self, message: str = "Malformed response") -> None:
15
+ super().__init__(message)
16
+
17
+
18
+ class LLMNoActionError(LLMError):
19
+ def __init__(self, message: str = "Agent must return an action") -> None:
20
+ super().__init__(message)
21
+
22
+
23
+ class LLMResponseError(LLMError):
24
+ def __init__(
25
+ self, message: str = "Failed to retrieve action from LLM response"
26
+ ) -> None:
27
+ super().__init__(message)
28
+
29
+
30
+ # Function-calling conversion/validation
31
+ class FunctionCallConversionError(LLMError):
32
+ def __init__(self, message: str) -> None:
33
+ super().__init__(message)
34
+
35
+
36
+ class FunctionCallValidationError(LLMError):
37
+ def __init__(self, message: str) -> None:
38
+ super().__init__(message)
39
+
40
+
41
+ class FunctionCallNotExistsError(LLMError):
42
+ def __init__(self, message: str) -> None:
43
+ super().__init__(message)
44
+
45
+
46
+ # Provider/transport related
47
+ class LLMNoResponseError(LLMError):
48
+ def __init__(
49
+ self,
50
+ message: str = (
51
+ "LLM did not return a response. This is only seen in Gemini models so far."
52
+ ),
53
+ ) -> None:
54
+ super().__init__(message)
55
+
56
+
57
+ class LLMContextWindowExceedError(LLMError):
58
+ def __init__(
59
+ self,
60
+ message: str = (
61
+ "Conversation history longer than LLM context window limit. "
62
+ "Consider enabling a condenser or shortening inputs."
63
+ ),
64
+ ) -> None:
65
+ super().__init__(message)
66
+
67
+
68
+ class LLMAuthenticationError(LLMError):
69
+ def __init__(self, message: str = "Invalid or missing API credentials") -> None:
70
+ super().__init__(message)
71
+
72
+
73
+ class LLMRateLimitError(LLMError):
74
+ def __init__(self, message: str = "Rate limit exceeded") -> None:
75
+ super().__init__(message)
76
+
77
+
78
+ class LLMTimeoutError(LLMError):
79
+ def __init__(self, message: str = "LLM request timed out") -> None:
80
+ super().__init__(message)
81
+
82
+
83
+ class LLMServiceUnavailableError(LLMError):
84
+ def __init__(self, message: str = "LLM service unavailable") -> None:
85
+ super().__init__(message)
86
+
87
+
88
+ class LLMBadRequestError(LLMError):
89
+ def __init__(self, message: str = "Bad request to LLM provider") -> None:
90
+ super().__init__(message)
91
+
92
+
93
+ # Other
94
+ class UserCancelledError(Exception):
95
+ def __init__(self, message: str = "User cancelled the request") -> None:
96
+ super().__init__(message)
97
+
98
+
99
+ class OperationCancelled(Exception):
100
+ def __init__(self, message: str = "Operation was cancelled") -> None:
101
+ super().__init__(message)