codeframe-ai 0.9.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.
Files changed (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,414 @@
1
+ """Anthropic LLM provider implementation.
2
+
3
+ Provides Claude model access via the Anthropic API.
4
+ """
5
+
6
+ import asyncio
7
+ import os
8
+ from typing import TYPE_CHECKING, AsyncIterator, Iterator, Optional
9
+
10
+ from codeframe.adapters.llm.base import (
11
+ LLMProvider,
12
+ LLMResponse,
13
+ ModelSelector,
14
+ Purpose,
15
+ StreamChunk,
16
+ Tool,
17
+ ToolCall,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from codeframe.core.credentials import CredentialManager
22
+
23
+
24
+ class AnthropicProvider(LLMProvider):
25
+ """Anthropic Claude provider.
26
+
27
+ Uses the Anthropic Python SDK to make API calls.
28
+ Supports tool use and streaming.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ api_key: Optional[str] = None,
34
+ model_selector: Optional[ModelSelector] = None,
35
+ credential_manager: Optional["CredentialManager"] = None,
36
+ ):
37
+ """Initialize the Anthropic provider.
38
+
39
+ Args:
40
+ api_key: Anthropic API key (defaults to ANTHROPIC_API_KEY env var)
41
+ model_selector: Custom model selector
42
+ credential_manager: Optional credential manager for secure key retrieval
43
+
44
+ Raises:
45
+ ValueError: If no API key is available
46
+ """
47
+ super().__init__(model_selector)
48
+
49
+ # Try to get API key from multiple sources in order:
50
+ # 1. Direct api_key parameter
51
+ # 2. CredentialManager (if provided)
52
+ # 3. Environment variable
53
+ self.api_key = api_key
54
+
55
+ if not self.api_key and credential_manager:
56
+ from codeframe.core.credentials import CredentialProvider
57
+ self.api_key = credential_manager.get_credential(CredentialProvider.LLM_ANTHROPIC)
58
+
59
+ if not self.api_key:
60
+ self.api_key = os.getenv("ANTHROPIC_API_KEY")
61
+
62
+ if not self.api_key:
63
+ raise ValueError(
64
+ "ANTHROPIC_API_KEY not set. "
65
+ "Set the environment variable, pass api_key parameter, "
66
+ "or configure via 'codeframe auth setup --provider anthropic'."
67
+ )
68
+ self._client = None
69
+ self._async_client = None
70
+
71
+ @property
72
+ def client(self):
73
+ """Lazy-load the Anthropic client."""
74
+ if self._client is None:
75
+ from anthropic import Anthropic
76
+
77
+ self._client = Anthropic(api_key=self.api_key)
78
+ return self._client
79
+
80
+ def complete(
81
+ self,
82
+ messages: list[dict],
83
+ purpose: Purpose = Purpose.EXECUTION,
84
+ tools: Optional[list[Tool]] = None,
85
+ max_tokens: int = 4096,
86
+ temperature: float = 0.0,
87
+ system: Optional[str] = None,
88
+ ) -> LLMResponse:
89
+ """Generate a completion using Claude.
90
+
91
+ Args:
92
+ messages: Conversation messages
93
+ purpose: Purpose of call (for model selection)
94
+ tools: Available tools for the model to use
95
+ max_tokens: Maximum tokens to generate
96
+ temperature: Sampling temperature
97
+ system: System prompt
98
+
99
+ Returns:
100
+ LLMResponse with content and/or tool calls
101
+ """
102
+ model = self.get_model(purpose)
103
+
104
+ # Build request kwargs
105
+ kwargs = {
106
+ "model": model,
107
+ "max_tokens": max_tokens,
108
+ "messages": self._convert_messages(messages),
109
+ }
110
+
111
+ if temperature > 0:
112
+ kwargs["temperature"] = temperature
113
+
114
+ if system:
115
+ kwargs["system"] = system
116
+
117
+ if tools:
118
+ kwargs["tools"] = self._convert_tools(tools)
119
+
120
+ # Make the API call
121
+ response = self.client.messages.create(**kwargs)
122
+
123
+ # Parse response
124
+ return self._parse_response(response)
125
+
126
+ async def async_complete(
127
+ self,
128
+ messages: list[dict],
129
+ purpose: Purpose = Purpose.EXECUTION,
130
+ tools: Optional[list[Tool]] = None,
131
+ max_tokens: int = 4096,
132
+ temperature: float = 0.0,
133
+ system: Optional[str] = None,
134
+ ) -> LLMResponse:
135
+ """True async completion via AsyncAnthropic.
136
+
137
+ Raises LLMAuthError / LLMRateLimitError / LLMConnectionError on failure.
138
+ """
139
+ from anthropic import AsyncAnthropic
140
+ from anthropic import (
141
+ AuthenticationError,
142
+ RateLimitError,
143
+ APIConnectionError,
144
+ )
145
+ from codeframe.adapters.llm.base import (
146
+ LLMAuthError,
147
+ LLMRateLimitError,
148
+ LLMConnectionError,
149
+ )
150
+
151
+ if self._async_client is None:
152
+ self._async_client = AsyncAnthropic(api_key=self.api_key)
153
+
154
+ model = self.get_model(purpose)
155
+ kwargs: dict = {
156
+ "model": model,
157
+ "max_tokens": max_tokens,
158
+ "messages": self._convert_messages(messages),
159
+ }
160
+ if temperature > 0:
161
+ kwargs["temperature"] = temperature
162
+ if system:
163
+ kwargs["system"] = system
164
+ if tools:
165
+ kwargs["tools"] = self._convert_tools(tools)
166
+
167
+ try:
168
+ response = await self._async_client.messages.create(**kwargs)
169
+ return self._parse_response(response)
170
+ except AuthenticationError as exc:
171
+ raise LLMAuthError(str(exc)) from exc
172
+ except RateLimitError as exc:
173
+ raise LLMRateLimitError(str(exc)) from exc
174
+ except APIConnectionError as exc:
175
+ raise LLMConnectionError(str(exc)) from exc
176
+
177
+ def supports(self, capability: str) -> bool:
178
+ """Return True for capabilities this provider supports."""
179
+ return capability == "extended_thinking"
180
+
181
+ async def async_stream(
182
+ self,
183
+ messages: list[dict],
184
+ system: str,
185
+ tools: list[dict],
186
+ model: str,
187
+ max_tokens: int,
188
+ interrupt_event: Optional[asyncio.Event] = None,
189
+ extended_thinking: bool = False,
190
+ ) -> AsyncIterator[StreamChunk]:
191
+ """Stream using Anthropic AsyncAnthropic SDK, yielding StreamChunk objects.
192
+
193
+ Translates Anthropic SDK events into the normalized StreamChunk format.
194
+ Tool inputs are collected and emitted in the final message_stop chunk
195
+ via tool_inputs_by_id, which is more reliable than streaming input deltas.
196
+
197
+ When ``extended_thinking=True``, requests interleaved thinking via the
198
+ Anthropic betas API. The flag is silently ignored on SDK versions that
199
+ do not support it.
200
+ """
201
+ from anthropic import AsyncAnthropic
202
+
203
+ if self._async_client is None:
204
+ self._async_client = AsyncAnthropic(api_key=self.api_key)
205
+
206
+ # Convert messages to Anthropic API format (handles tool_calls/tool_results)
207
+ converted = self._convert_messages(messages)
208
+
209
+ kwargs: dict = {
210
+ "model": model,
211
+ "system": system,
212
+ "messages": converted,
213
+ "tools": tools,
214
+ "max_tokens": max_tokens,
215
+ }
216
+
217
+ if extended_thinking:
218
+ kwargs["betas"] = ["interleaved-thinking-2025-05-14"]
219
+
220
+ active_tool_id: Optional[str] = None
221
+
222
+ # When extended_thinking is set, the beta header may be unsupported on
223
+ # older SDK versions. Retry without it rather than hard-failing.
224
+ try:
225
+ stream_ctx = self._async_client.messages.stream(**kwargs)
226
+ except Exception: # pragma: no cover
227
+ if extended_thinking:
228
+ kwargs.pop("betas", None)
229
+ stream_ctx = self._async_client.messages.stream(**kwargs)
230
+ else:
231
+ raise
232
+
233
+ async with stream_ctx as stream:
234
+ async for sdk_event in stream:
235
+ if interrupt_event and interrupt_event.is_set():
236
+ return
237
+
238
+ event_type = sdk_event.type
239
+
240
+ if event_type == "content_block_start":
241
+ block = sdk_event.content_block
242
+ if block.type == "tool_use":
243
+ active_tool_id = block.id
244
+ yield StreamChunk(
245
+ type="tool_use_start",
246
+ tool_id=block.id,
247
+ tool_name=block.name,
248
+ tool_input=getattr(block, "input", {}),
249
+ )
250
+
251
+ elif event_type == "content_block_delta":
252
+ delta = sdk_event.delta
253
+ if delta.type == "text_delta":
254
+ yield StreamChunk(type="text_delta", text=delta.text)
255
+ elif delta.type == "thinking_delta":
256
+ yield StreamChunk(type="thinking_delta", text=delta.thinking)
257
+ # input_json_delta: final inputs are rebuilt from message_stop
258
+
259
+ elif event_type == "content_block_stop":
260
+ if active_tool_id is not None:
261
+ yield StreamChunk(type="tool_use_stop")
262
+ active_tool_id = None
263
+
264
+ elif event_type == "message_stop":
265
+ # Flush any open tool block
266
+ if active_tool_id is not None:
267
+ yield StreamChunk(type="tool_use_stop")
268
+ active_tool_id = None
269
+
270
+ final_msg = await stream.get_final_message()
271
+ stop_reason = final_msg.stop_reason or "end_turn"
272
+
273
+ # Build tool_inputs_by_id from final content blocks
274
+ tool_inputs_by_id: dict = {}
275
+ if hasattr(final_msg, "content"):
276
+ for block in final_msg.content:
277
+ if getattr(block, "type", None) == "tool_use" and hasattr(block, "id"):
278
+ tool_inputs_by_id[block.id] = getattr(block, "input", {})
279
+
280
+ yield StreamChunk(
281
+ type="message_stop",
282
+ stop_reason=stop_reason,
283
+ input_tokens=final_msg.usage.input_tokens,
284
+ output_tokens=final_msg.usage.output_tokens,
285
+ tool_inputs_by_id=tool_inputs_by_id,
286
+ )
287
+
288
+ def stream(
289
+ self,
290
+ messages: list[dict],
291
+ purpose: Purpose = Purpose.EXECUTION,
292
+ max_tokens: int = 4096,
293
+ temperature: float = 0.0,
294
+ system: Optional[str] = None,
295
+ ) -> Iterator[str]:
296
+ """Stream a completion token by token.
297
+
298
+ Args:
299
+ messages: Conversation messages
300
+ purpose: Purpose of call (for model selection)
301
+ max_tokens: Maximum tokens to generate
302
+ temperature: Sampling temperature
303
+ system: System prompt
304
+
305
+ Yields:
306
+ Text chunks as they are generated
307
+ """
308
+ model = self.get_model(purpose)
309
+
310
+ kwargs = {
311
+ "model": model,
312
+ "max_tokens": max_tokens,
313
+ "messages": self._convert_messages(messages),
314
+ }
315
+
316
+ if temperature > 0:
317
+ kwargs["temperature"] = temperature
318
+
319
+ if system:
320
+ kwargs["system"] = system
321
+
322
+ with self.client.messages.stream(**kwargs) as stream:
323
+ for text in stream.text_stream:
324
+ yield text
325
+
326
+ def _convert_messages(self, messages: list[dict]) -> list[dict]:
327
+ """Convert messages to Anthropic format.
328
+
329
+ Handles tool results by converting them to the expected format.
330
+ """
331
+ converted = []
332
+ for msg in messages:
333
+ if "tool_results" in msg and msg["tool_results"]:
334
+ # Convert tool results to Anthropic format
335
+ # Mirror tool_calls logic: tool_result blocks first, then text if present
336
+ content = []
337
+ for tr in msg["tool_results"]:
338
+ content.append(
339
+ {
340
+ "type": "tool_result",
341
+ "tool_use_id": tr["tool_call_id"],
342
+ "content": tr["content"],
343
+ "is_error": tr.get("is_error", False),
344
+ }
345
+ )
346
+ # Preserve any user text content alongside tool results
347
+ if msg.get("content"):
348
+ msg_content = msg["content"]
349
+ if isinstance(msg_content, str):
350
+ content.append({"type": "text", "text": msg_content})
351
+ elif isinstance(msg_content, list):
352
+ # Handle list of content blocks
353
+ for block in msg_content:
354
+ if isinstance(block, str):
355
+ content.append({"type": "text", "text": block})
356
+ elif isinstance(block, dict) and block.get("type") == "text":
357
+ content.append(block)
358
+ converted.append({"role": "user", "content": content})
359
+ elif "tool_calls" in msg and msg["tool_calls"]:
360
+ # Convert assistant message with tool calls
361
+ content = []
362
+ if msg.get("content"):
363
+ content.append({"type": "text", "text": msg["content"]})
364
+ for tc in msg["tool_calls"]:
365
+ content.append(
366
+ {
367
+ "type": "tool_use",
368
+ "id": tc["id"],
369
+ "name": tc["name"],
370
+ "input": tc["input"],
371
+ }
372
+ )
373
+ converted.append({"role": "assistant", "content": content})
374
+ else:
375
+ # Simple text message
376
+ converted.append({"role": msg["role"], "content": msg["content"]})
377
+ return converted
378
+
379
+ def _convert_tools(self, tools: list[Tool]) -> list[dict]:
380
+ """Convert Tool objects to Anthropic format."""
381
+ return [
382
+ {
383
+ "name": tool.name,
384
+ "description": tool.description,
385
+ "input_schema": tool.input_schema,
386
+ }
387
+ for tool in tools
388
+ ]
389
+
390
+ def _parse_response(self, response) -> LLMResponse:
391
+ """Parse Anthropic response into LLMResponse."""
392
+ content = ""
393
+ tool_calls = []
394
+
395
+ for block in response.content:
396
+ if block.type == "text":
397
+ content += block.text
398
+ elif block.type == "tool_use":
399
+ tool_calls.append(
400
+ ToolCall(
401
+ id=block.id,
402
+ name=block.name,
403
+ input=block.input,
404
+ )
405
+ )
406
+
407
+ return LLMResponse(
408
+ content=content,
409
+ tool_calls=tool_calls,
410
+ stop_reason=response.stop_reason,
411
+ model=response.model,
412
+ input_tokens=response.usage.input_tokens,
413
+ output_tokens=response.usage.output_tokens,
414
+ )