base-agentkit 0.1.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.
- agentkit/__init__.py +35 -0
- agentkit/agent/__init__.py +7 -0
- agentkit/agent/agent.py +368 -0
- agentkit/agent/budgets.py +48 -0
- agentkit/agent/report.py +166 -0
- agentkit/agent/tool_runtime.py +77 -0
- agentkit/cli/__init__.py +5 -0
- agentkit/cli/main.py +108 -0
- agentkit/config/__init__.py +23 -0
- agentkit/config/loader.py +108 -0
- agentkit/config/provider_defaults.py +96 -0
- agentkit/config/schema.py +148 -0
- agentkit/constants.py +21 -0
- agentkit/errors.py +58 -0
- agentkit/llm/__init__.py +53 -0
- agentkit/llm/base.py +36 -0
- agentkit/llm/factory.py +27 -0
- agentkit/llm/providers/__init__.py +15 -0
- agentkit/llm/providers/anthropic_provider.py +371 -0
- agentkit/llm/providers/gemini_provider.py +396 -0
- agentkit/llm/providers/openai_provider.py +881 -0
- agentkit/llm/providers/qwen_provider.py +34 -0
- agentkit/llm/providers/vllm_provider.py +47 -0
- agentkit/llm/types.py +215 -0
- agentkit/llm/usage.py +72 -0
- agentkit/py.typed +0 -0
- agentkit/runlog/__init__.py +15 -0
- agentkit/runlog/events.py +67 -0
- agentkit/runlog/jsonl.py +90 -0
- agentkit/runlog/recorder.py +94 -0
- agentkit/runlog/sinks.py +15 -0
- agentkit/tools/__init__.py +16 -0
- agentkit/tools/base.py +139 -0
- agentkit/tools/library/__init__.py +8 -0
- agentkit/tools/library/_fs_common.py +330 -0
- agentkit/tools/library/create_file.py +168 -0
- agentkit/tools/library/fs_tools.py +21 -0
- agentkit/tools/library/str_replace.py +241 -0
- agentkit/tools/library/view.py +372 -0
- agentkit/tools/library/word_count.py +138 -0
- agentkit/tools/loader.py +81 -0
- agentkit/tools/registry.py +284 -0
- agentkit/tools/types.py +98 -0
- agentkit/workspace/__init__.py +6 -0
- agentkit/workspace/fs.py +288 -0
- agentkit/workspace/layout.py +33 -0
- base_agentkit-0.1.0.dist-info/METADATA +142 -0
- base_agentkit-0.1.0.dist-info/RECORD +51 -0
- base_agentkit-0.1.0.dist-info/WHEEL +4 -0
- base_agentkit-0.1.0.dist-info/entry_points.txt +3 -0
- base_agentkit-0.1.0.dist-info/licenses/LICENSE +183 -0
agentkit/llm/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Public LLM provider and type exports."""
|
|
2
|
+
|
|
3
|
+
from .base import BaseLLMProvider
|
|
4
|
+
from .factory import build_provider
|
|
5
|
+
from .providers.anthropic_provider import AnthropicProvider
|
|
6
|
+
from .providers.gemini_provider import GeminiProvider
|
|
7
|
+
from .providers.openai_provider import OpenAIProvider
|
|
8
|
+
from .providers.qwen_provider import QwenProvider
|
|
9
|
+
from .providers.vllm_provider import VLLMProvider
|
|
10
|
+
from .types import (
|
|
11
|
+
CompletionReason,
|
|
12
|
+
ConversationItem,
|
|
13
|
+
ConversationMode,
|
|
14
|
+
ConversationState,
|
|
15
|
+
GenerationOptions,
|
|
16
|
+
MessageItem,
|
|
17
|
+
ProviderKind,
|
|
18
|
+
ReasoningItem,
|
|
19
|
+
StatePatch,
|
|
20
|
+
ToolCallItem,
|
|
21
|
+
ToolResultItem,
|
|
22
|
+
TurnStatus,
|
|
23
|
+
UnifiedLLMRequest,
|
|
24
|
+
UnifiedLLMResponse,
|
|
25
|
+
UnifiedToolSpec,
|
|
26
|
+
Usage,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"AnthropicProvider",
|
|
31
|
+
"BaseLLMProvider",
|
|
32
|
+
"build_provider",
|
|
33
|
+
"CompletionReason",
|
|
34
|
+
"ConversationItem",
|
|
35
|
+
"ConversationMode",
|
|
36
|
+
"ConversationState",
|
|
37
|
+
"GenerationOptions",
|
|
38
|
+
"GeminiProvider",
|
|
39
|
+
"MessageItem",
|
|
40
|
+
"OpenAIProvider",
|
|
41
|
+
"ProviderKind",
|
|
42
|
+
"QwenProvider",
|
|
43
|
+
"ReasoningItem",
|
|
44
|
+
"StatePatch",
|
|
45
|
+
"ToolCallItem",
|
|
46
|
+
"ToolResultItem",
|
|
47
|
+
"TurnStatus",
|
|
48
|
+
"UnifiedLLMRequest",
|
|
49
|
+
"UnifiedLLMResponse",
|
|
50
|
+
"UnifiedToolSpec",
|
|
51
|
+
"Usage",
|
|
52
|
+
"VLLMProvider",
|
|
53
|
+
]
|
agentkit/llm/base.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Base abstractions for unified LLM providers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
from agentkit.llm.types import (
|
|
8
|
+
ConversationItem,
|
|
9
|
+
MessageItem,
|
|
10
|
+
UnifiedLLMRequest,
|
|
11
|
+
UnifiedLLMResponse,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseLLMProvider(ABC):
|
|
16
|
+
"""Abstract interface implemented by provider adapters."""
|
|
17
|
+
|
|
18
|
+
model: str
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def generate(self, req: UnifiedLLMRequest) -> UnifiedLLMResponse:
|
|
22
|
+
"""Execute one non-streaming model turn and return unified output."""
|
|
23
|
+
|
|
24
|
+
def render_output_text(
|
|
25
|
+
self,
|
|
26
|
+
output_items: list[ConversationItem],
|
|
27
|
+
raw_response: dict[str, object] | None,
|
|
28
|
+
) -> str:
|
|
29
|
+
"""Render provider-specific human-facing text from output items."""
|
|
30
|
+
del raw_response
|
|
31
|
+
texts = [
|
|
32
|
+
item.text
|
|
33
|
+
for item in output_items
|
|
34
|
+
if isinstance(item, MessageItem) and item.role == "assistant"
|
|
35
|
+
]
|
|
36
|
+
return "\n".join(texts).strip()
|
agentkit/llm/factory.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Provider factory for constructing configured LLM backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from agentkit.config.schema import ProviderConfig
|
|
6
|
+
from agentkit.errors import ConfigError
|
|
7
|
+
from agentkit.llm.base import BaseLLMProvider
|
|
8
|
+
from agentkit.llm.providers.anthropic_provider import AnthropicProvider
|
|
9
|
+
from agentkit.llm.providers.gemini_provider import GeminiProvider
|
|
10
|
+
from agentkit.llm.providers.openai_provider import OpenAIProvider
|
|
11
|
+
from agentkit.llm.providers.qwen_provider import QwenProvider
|
|
12
|
+
from agentkit.llm.providers.vllm_provider import VLLMProvider
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_provider(config: ProviderConfig) -> BaseLLMProvider:
|
|
16
|
+
"""Create the configured provider implementation."""
|
|
17
|
+
if config.kind == "openai":
|
|
18
|
+
return OpenAIProvider(config)
|
|
19
|
+
if config.kind == "anthropic":
|
|
20
|
+
return AnthropicProvider(config)
|
|
21
|
+
if config.kind == "gemini":
|
|
22
|
+
return GeminiProvider(config)
|
|
23
|
+
if config.kind == "vllm":
|
|
24
|
+
return VLLMProvider(config)
|
|
25
|
+
if config.kind == "qwen":
|
|
26
|
+
return QwenProvider(config)
|
|
27
|
+
raise ConfigError(f"Unsupported provider kind: {config.kind}")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Public provider implementation exports."""
|
|
2
|
+
|
|
3
|
+
from .anthropic_provider import AnthropicProvider
|
|
4
|
+
from .gemini_provider import GeminiProvider
|
|
5
|
+
from .openai_provider import OpenAIProvider
|
|
6
|
+
from .qwen_provider import QwenProvider
|
|
7
|
+
from .vllm_provider import VLLMProvider
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"AnthropicProvider",
|
|
11
|
+
"GeminiProvider",
|
|
12
|
+
"OpenAIProvider",
|
|
13
|
+
"QwenProvider",
|
|
14
|
+
"VLLMProvider",
|
|
15
|
+
]
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"""Anthropic Messages API provider adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from agentkit.config.provider_defaults import DEFAULT_ANTHROPIC_BASE_URL
|
|
10
|
+
from agentkit.config.schema import ProviderConfig
|
|
11
|
+
from agentkit.errors import ProviderError, ProviderIssue
|
|
12
|
+
from agentkit.llm.base import BaseLLMProvider
|
|
13
|
+
from agentkit.llm.types import (
|
|
14
|
+
CompletionReason,
|
|
15
|
+
ConversationItem,
|
|
16
|
+
MessageItem,
|
|
17
|
+
ReasoningItem,
|
|
18
|
+
StatePatch,
|
|
19
|
+
ToolCallItem,
|
|
20
|
+
ToolResultItem,
|
|
21
|
+
TurnStatus,
|
|
22
|
+
UnifiedLLMRequest,
|
|
23
|
+
UnifiedLLMResponse,
|
|
24
|
+
Usage,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
class AnthropicProvider(BaseLLMProvider):
|
|
28
|
+
"""Anthropic Messages API adapter."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, config: ProviderConfig) -> None:
|
|
31
|
+
self.config = config
|
|
32
|
+
self.model = config.model
|
|
33
|
+
self._session = requests.Session()
|
|
34
|
+
|
|
35
|
+
def generate(self, req: UnifiedLLMRequest) -> UnifiedLLMResponse:
|
|
36
|
+
payload = self._build_payload(req)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
response = self._session.post(
|
|
40
|
+
self._messages_endpoint,
|
|
41
|
+
headers=self._headers,
|
|
42
|
+
json=payload,
|
|
43
|
+
timeout=self.config.timeout_s,
|
|
44
|
+
)
|
|
45
|
+
except requests.Timeout as exc: # pragma: no cover - network specific
|
|
46
|
+
raise ProviderError(
|
|
47
|
+
f"Anthropic request timed out: {exc}",
|
|
48
|
+
issue=ProviderIssue(category="timeout", retryable=True),
|
|
49
|
+
) from exc
|
|
50
|
+
except requests.RequestException as exc: # pragma: no cover - network specific
|
|
51
|
+
raise ProviderError(
|
|
52
|
+
f"Anthropic request failed: {exc}",
|
|
53
|
+
issue=ProviderIssue(category="upstream", retryable=True),
|
|
54
|
+
) from exc
|
|
55
|
+
|
|
56
|
+
if response.status_code >= 400:
|
|
57
|
+
self._raise_http_error(response)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
body = response.json()
|
|
61
|
+
except ValueError as exc:
|
|
62
|
+
raise ProviderError(
|
|
63
|
+
"Anthropic response is not valid JSON.",
|
|
64
|
+
issue=ProviderIssue(category="parse", retryable=False),
|
|
65
|
+
) from exc
|
|
66
|
+
|
|
67
|
+
return self._parse_response(body)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def _messages_endpoint(self) -> str:
|
|
71
|
+
base = self.config.base_url or DEFAULT_ANTHROPIC_BASE_URL
|
|
72
|
+
if base.endswith("/v1/messages"):
|
|
73
|
+
return base
|
|
74
|
+
return f"{base.rstrip('/')}/v1/messages"
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def _headers(self) -> dict[str, str]:
|
|
78
|
+
headers = {
|
|
79
|
+
"content-type": "application/json",
|
|
80
|
+
"anthropic-version": "2023-06-01",
|
|
81
|
+
}
|
|
82
|
+
if self.config.api_key:
|
|
83
|
+
headers["x-api-key"] = self.config.api_key
|
|
84
|
+
return headers
|
|
85
|
+
|
|
86
|
+
def render_output_text(
|
|
87
|
+
self,
|
|
88
|
+
output_items: list[ConversationItem],
|
|
89
|
+
raw_response: dict[str, object] | None,
|
|
90
|
+
) -> str:
|
|
91
|
+
del raw_response
|
|
92
|
+
texts = [
|
|
93
|
+
item.text
|
|
94
|
+
for item in output_items
|
|
95
|
+
if isinstance(item, MessageItem) and item.role == "assistant" and item.text
|
|
96
|
+
]
|
|
97
|
+
return "\n".join(texts).strip()
|
|
98
|
+
|
|
99
|
+
def _build_payload(self, req: UnifiedLLMRequest) -> dict[str, Any]:
|
|
100
|
+
messages = self._compile_messages(req.state.history + req.inputs)
|
|
101
|
+
|
|
102
|
+
payload: dict[str, Any] = {
|
|
103
|
+
"model": req.model,
|
|
104
|
+
"messages": messages,
|
|
105
|
+
"max_tokens": req.options.max_output_tokens or 1024,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if req.tools:
|
|
109
|
+
payload["tools"] = [
|
|
110
|
+
{
|
|
111
|
+
"name": tool.name,
|
|
112
|
+
"description": tool.description,
|
|
113
|
+
"input_schema": tool.parameters,
|
|
114
|
+
}
|
|
115
|
+
for tool in req.tools
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
system_prompt = self._compile_system(req)
|
|
119
|
+
if system_prompt:
|
|
120
|
+
payload["system"] = system_prompt
|
|
121
|
+
|
|
122
|
+
temperature = (
|
|
123
|
+
req.options.temperature
|
|
124
|
+
if req.options.temperature is not None
|
|
125
|
+
else self.config.temperature
|
|
126
|
+
)
|
|
127
|
+
if temperature is not None:
|
|
128
|
+
payload["temperature"] = temperature
|
|
129
|
+
|
|
130
|
+
if req.options.stop_sequences:
|
|
131
|
+
payload["stop_sequences"] = list(req.options.stop_sequences)
|
|
132
|
+
|
|
133
|
+
return payload
|
|
134
|
+
|
|
135
|
+
def _compile_system(self, req: UnifiedLLMRequest) -> str:
|
|
136
|
+
return req.instructions.strip()
|
|
137
|
+
|
|
138
|
+
def _compile_messages(self, items: list[ConversationItem]) -> list[dict[str, Any]]:
|
|
139
|
+
raw_messages: list[dict[str, Any]] = []
|
|
140
|
+
for item in items:
|
|
141
|
+
message = self._item_to_message(item)
|
|
142
|
+
if message is not None:
|
|
143
|
+
raw_messages.append(message)
|
|
144
|
+
|
|
145
|
+
return self._merge_consecutive_roles(raw_messages)
|
|
146
|
+
|
|
147
|
+
def _item_to_message(self, item: ConversationItem) -> dict[str, Any] | None:
|
|
148
|
+
if isinstance(item, MessageItem):
|
|
149
|
+
return {
|
|
150
|
+
"role": item.role,
|
|
151
|
+
"content": [{"type": "text", "text": item.text}],
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if isinstance(item, ToolCallItem):
|
|
155
|
+
return {
|
|
156
|
+
"role": "assistant",
|
|
157
|
+
"content": [
|
|
158
|
+
{
|
|
159
|
+
"type": "tool_use",
|
|
160
|
+
"id": item.call_id,
|
|
161
|
+
"name": item.name,
|
|
162
|
+
"input": item.arguments,
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if isinstance(item, ToolResultItem):
|
|
168
|
+
return {
|
|
169
|
+
"role": "user",
|
|
170
|
+
"content": [
|
|
171
|
+
{
|
|
172
|
+
"type": "tool_result",
|
|
173
|
+
"tool_use_id": item.call_id,
|
|
174
|
+
"content": item.output_text,
|
|
175
|
+
"is_error": item.is_error,
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if item.replay_hint and item.raw_item:
|
|
181
|
+
block_type = str(item.raw_item.get("type") or "")
|
|
182
|
+
if block_type in {"thinking", "redacted_thinking"}:
|
|
183
|
+
return {"role": "assistant", "content": [item.raw_item]}
|
|
184
|
+
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
def _merge_consecutive_roles(
|
|
188
|
+
self, messages: list[dict[str, Any]]
|
|
189
|
+
) -> list[dict[str, Any]]:
|
|
190
|
+
if not messages:
|
|
191
|
+
return []
|
|
192
|
+
|
|
193
|
+
merged: list[dict[str, Any]] = []
|
|
194
|
+
for message in messages:
|
|
195
|
+
role = str(message.get("role") or "user")
|
|
196
|
+
content = list(message.get("content") or [])
|
|
197
|
+
if merged and merged[-1].get("role") == role:
|
|
198
|
+
merged[-1].setdefault("content", [])
|
|
199
|
+
merged[-1]["content"].extend(content)
|
|
200
|
+
else:
|
|
201
|
+
merged.append({"role": role, "content": content})
|
|
202
|
+
return merged
|
|
203
|
+
|
|
204
|
+
def _parse_response(self, body: dict[str, Any]) -> UnifiedLLMResponse:
|
|
205
|
+
output_items: list[ConversationItem] = []
|
|
206
|
+
|
|
207
|
+
for block_raw in body.get("content") or []:
|
|
208
|
+
block = self._to_dict(block_raw)
|
|
209
|
+
block_type = str(block.get("type") or "")
|
|
210
|
+
|
|
211
|
+
if block_type == "text":
|
|
212
|
+
text = str(block.get("text") or "")
|
|
213
|
+
if text:
|
|
214
|
+
output_items.append(MessageItem(role="assistant", text=text))
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
if block_type == "tool_use":
|
|
218
|
+
output_items.append(
|
|
219
|
+
ToolCallItem(
|
|
220
|
+
call_id=str(block.get("id") or ""),
|
|
221
|
+
name=str(block.get("name") or ""),
|
|
222
|
+
arguments=self._ensure_dict(block.get("input")),
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
if block_type in {"thinking", "redacted_thinking"}:
|
|
228
|
+
thinking_text = block.get("thinking")
|
|
229
|
+
if not isinstance(thinking_text, str):
|
|
230
|
+
thinking_text = block.get("text") if isinstance(block.get("text"), str) else None
|
|
231
|
+
output_items.append(
|
|
232
|
+
ReasoningItem(
|
|
233
|
+
text=thinking_text,
|
|
234
|
+
summary=None,
|
|
235
|
+
raw_item=block,
|
|
236
|
+
replay_hint=True,
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
status, reason = self._map_status(body, output_items)
|
|
241
|
+
|
|
242
|
+
return UnifiedLLMResponse(
|
|
243
|
+
response_id=str(body.get("id") or "") or None,
|
|
244
|
+
status=status,
|
|
245
|
+
reason=reason,
|
|
246
|
+
output_items=output_items,
|
|
247
|
+
output_text=self.render_output_text(output_items, body),
|
|
248
|
+
usage=self._parse_usage(body),
|
|
249
|
+
state_patch=StatePatch(),
|
|
250
|
+
provider_name="anthropic",
|
|
251
|
+
raw_response=body,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def _map_status(
|
|
255
|
+
self,
|
|
256
|
+
body: dict[str, Any],
|
|
257
|
+
output_items: list[ConversationItem],
|
|
258
|
+
) -> tuple[TurnStatus, CompletionReason]:
|
|
259
|
+
if any(isinstance(item, ToolCallItem) for item in output_items):
|
|
260
|
+
return "requires_tool", "tool_call"
|
|
261
|
+
|
|
262
|
+
stop_reason = str(body.get("stop_reason") or "")
|
|
263
|
+
|
|
264
|
+
mapping: dict[str, tuple[TurnStatus, CompletionReason]] = {
|
|
265
|
+
"end_turn": ("completed", "stop"),
|
|
266
|
+
"stop_sequence": ("completed", "stop"),
|
|
267
|
+
"max_tokens": ("incomplete", "max_tokens"),
|
|
268
|
+
"tool_use": ("requires_tool", "tool_call"),
|
|
269
|
+
"pause_turn": ("incomplete", "pause"),
|
|
270
|
+
"refusal": ("blocked", "refusal"),
|
|
271
|
+
"model_context_window_exceeded": ("incomplete", "context_window"),
|
|
272
|
+
}
|
|
273
|
+
if stop_reason in mapping:
|
|
274
|
+
return mapping[stop_reason]
|
|
275
|
+
|
|
276
|
+
if body.get("type") == "error":
|
|
277
|
+
return "failed", "error"
|
|
278
|
+
|
|
279
|
+
return "incomplete", "unknown"
|
|
280
|
+
|
|
281
|
+
def _parse_usage(self, body: dict[str, Any]) -> Usage:
|
|
282
|
+
usage = self._to_dict(body.get("usage") or {})
|
|
283
|
+
input_tokens = self._as_int(usage.get("input_tokens"))
|
|
284
|
+
output_tokens = self._as_int(usage.get("output_tokens"))
|
|
285
|
+
|
|
286
|
+
total_tokens: int | None = None
|
|
287
|
+
if input_tokens is not None and output_tokens is not None:
|
|
288
|
+
total_tokens = input_tokens + output_tokens
|
|
289
|
+
|
|
290
|
+
return Usage(
|
|
291
|
+
input_tokens=input_tokens,
|
|
292
|
+
output_tokens=output_tokens,
|
|
293
|
+
total_tokens=total_tokens,
|
|
294
|
+
cache_write_tokens=self._as_int(usage.get("cache_creation_input_tokens")),
|
|
295
|
+
cache_read_tokens=self._as_int(usage.get("cache_read_input_tokens")),
|
|
296
|
+
raw=usage or None,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def _raise_http_error(self, response: requests.Response) -> None:
|
|
300
|
+
body: dict[str, Any] | None = None
|
|
301
|
+
try:
|
|
302
|
+
parsed = response.json()
|
|
303
|
+
if isinstance(parsed, dict):
|
|
304
|
+
body = parsed
|
|
305
|
+
except ValueError:
|
|
306
|
+
body = None
|
|
307
|
+
|
|
308
|
+
status = response.status_code
|
|
309
|
+
category = "unknown"
|
|
310
|
+
retryable = False
|
|
311
|
+
provider_code: str | None = None
|
|
312
|
+
|
|
313
|
+
if status in {401, 403}:
|
|
314
|
+
category = "auth"
|
|
315
|
+
elif status == 429:
|
|
316
|
+
category = "rate_limit"
|
|
317
|
+
retryable = True
|
|
318
|
+
elif 400 <= status < 500:
|
|
319
|
+
category = "invalid_request"
|
|
320
|
+
elif status >= 500:
|
|
321
|
+
category = "upstream"
|
|
322
|
+
retryable = True
|
|
323
|
+
|
|
324
|
+
if body:
|
|
325
|
+
err = self._to_dict(body.get("error") or {})
|
|
326
|
+
provider_code = str(err.get("type") or err.get("code") or "") or None
|
|
327
|
+
err_message = str(err.get("message") or "").lower()
|
|
328
|
+
if "safety" in err_message or "policy" in err_message:
|
|
329
|
+
category = "safety"
|
|
330
|
+
|
|
331
|
+
raise ProviderError(
|
|
332
|
+
f"Anthropic request failed with status {status}.",
|
|
333
|
+
issue=ProviderIssue(
|
|
334
|
+
category=category,
|
|
335
|
+
http_status=status,
|
|
336
|
+
provider_code=provider_code,
|
|
337
|
+
retryable=retryable,
|
|
338
|
+
raw=body,
|
|
339
|
+
),
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def _to_dict(self, value: Any) -> dict[str, Any]:
|
|
343
|
+
if isinstance(value, dict):
|
|
344
|
+
return value
|
|
345
|
+
if hasattr(value, "model_dump"):
|
|
346
|
+
try:
|
|
347
|
+
dumped = value.model_dump(mode="python")
|
|
348
|
+
if isinstance(dumped, dict):
|
|
349
|
+
return dumped
|
|
350
|
+
except Exception:
|
|
351
|
+
pass
|
|
352
|
+
return {}
|
|
353
|
+
|
|
354
|
+
def _ensure_dict(self, value: Any) -> dict[str, Any]:
|
|
355
|
+
if isinstance(value, dict):
|
|
356
|
+
return value
|
|
357
|
+
return {}
|
|
358
|
+
|
|
359
|
+
def _as_int(self, value: Any) -> int | None:
|
|
360
|
+
if isinstance(value, bool):
|
|
361
|
+
return int(value)
|
|
362
|
+
if isinstance(value, int):
|
|
363
|
+
return value
|
|
364
|
+
if isinstance(value, float):
|
|
365
|
+
return int(value)
|
|
366
|
+
if isinstance(value, str):
|
|
367
|
+
try:
|
|
368
|
+
return int(value)
|
|
369
|
+
except ValueError:
|
|
370
|
+
return None
|
|
371
|
+
return None
|