crabcode 0.1.6__py3-none-win_amd64.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 (205) hide show
  1. crabcode-0.1.6.dist-info/METADATA +63 -0
  2. crabcode-0.1.6.dist-info/RECORD +205 -0
  3. crabcode-0.1.6.dist-info/WHEEL +4 -0
  4. crabcode-0.1.6.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +7 -0
  6. crabcode_cli/__main__.py +4 -0
  7. crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
  8. crabcode_cli/_native/runtime.json +6 -0
  9. crabcode_cli/app.py +776 -0
  10. crabcode_cli/banner.py +39 -0
  11. crabcode_cli/computer_use.py +286 -0
  12. crabcode_cli/model_manager.py +649 -0
  13. crabcode_cli/model_picker.py +168 -0
  14. crabcode_cli/pipe.py +148 -0
  15. crabcode_cli/render/__init__.py +0 -0
  16. crabcode_cli/render/messages.py +19 -0
  17. crabcode_cli/repl.py +4841 -0
  18. crabcode_cli/session_picker.py +239 -0
  19. crabcode_core/__init__.py +7 -0
  20. crabcode_core/_version.py +3 -0
  21. crabcode_core/_windows_process.py +68 -0
  22. crabcode_core/agent_manager.py +1731 -0
  23. crabcode_core/api/__init__.py +20 -0
  24. crabcode_core/api/anthropic_adapter.py +648 -0
  25. crabcode_core/api/azure_adapter.py +69 -0
  26. crabcode_core/api/base.py +233 -0
  27. crabcode_core/api/codex_adapter.py +1327 -0
  28. crabcode_core/api/error_logging.py +132 -0
  29. crabcode_core/api/gemini_adapter.py +26 -0
  30. crabcode_core/api/model_info.py +257 -0
  31. crabcode_core/api/network.py +62 -0
  32. crabcode_core/api/ollama_adapter.py +27 -0
  33. crabcode_core/api/openai_adapter.py +323 -0
  34. crabcode_core/api/registry.py +58 -0
  35. crabcode_core/command_line.py +18 -0
  36. crabcode_core/compact/__init__.py +17 -0
  37. crabcode_core/compact/compact.py +514 -0
  38. crabcode_core/compact/context_tokens.py +165 -0
  39. crabcode_core/computer_use_mode.py +38 -0
  40. crabcode_core/computer_use_observation.py +144 -0
  41. crabcode_core/config/__init__.py +3 -0
  42. crabcode_core/config/claudemd.py +61 -0
  43. crabcode_core/config/manager.py +237 -0
  44. crabcode_core/config/model_catalog.py +256 -0
  45. crabcode_core/events.py +4896 -0
  46. crabcode_core/file_lock.py +81 -0
  47. crabcode_core/filesystem.py +22 -0
  48. crabcode_core/goal.py +149 -0
  49. crabcode_core/hooks/__init__.py +5 -0
  50. crabcode_core/hooks/manager.py +290 -0
  51. crabcode_core/image_io.py +111 -0
  52. crabcode_core/io_worker.py +211 -0
  53. crabcode_core/logging_utils.py +168 -0
  54. crabcode_core/lsp/__init__.py +6 -0
  55. crabcode_core/lsp/client.py +786 -0
  56. crabcode_core/lsp/diagnostics.py +150 -0
  57. crabcode_core/lsp/manager.py +583 -0
  58. crabcode_core/mcp/__init__.py +4 -0
  59. crabcode_core/mcp/client.py +169 -0
  60. crabcode_core/mcp/config.py +57 -0
  61. crabcode_core/path_validation.py +34 -0
  62. crabcode_core/paths.py +24 -0
  63. crabcode_core/peer/__init__.py +10 -0
  64. crabcode_core/peer/runtime.py +518 -0
  65. crabcode_core/permissions/__init__.py +7 -0
  66. crabcode_core/permissions/ai_reviewer.py +184 -0
  67. crabcode_core/permissions/manager.py +187 -0
  68. crabcode_core/plan/__init__.py +5 -0
  69. crabcode_core/plan/executor.py +270 -0
  70. crabcode_core/plan/types.py +169 -0
  71. crabcode_core/prompts/__init__.py +0 -0
  72. crabcode_core/prompts/blocks.py +10 -0
  73. crabcode_core/prompts/context.py +229 -0
  74. crabcode_core/prompts/library.py +92 -0
  75. crabcode_core/prompts/profile.py +79 -0
  76. crabcode_core/prompts/system.py +356 -0
  77. crabcode_core/prompts/templates.py +104 -0
  78. crabcode_core/query/__init__.py +0 -0
  79. crabcode_core/query/computer_use_history.py +83 -0
  80. crabcode_core/query/loop.py +2446 -0
  81. crabcode_core/query/retry.py +116 -0
  82. crabcode_core/schedule/__init__.py +13 -0
  83. crabcode_core/schedule/manager.py +631 -0
  84. crabcode_core/schedule/models.py +204 -0
  85. crabcode_core/schedule/store.py +558 -0
  86. crabcode_core/schedule/timing.py +287 -0
  87. crabcode_core/session/__init__.py +12 -0
  88. crabcode_core/session/export.py +141 -0
  89. crabcode_core/session/meta_db.py +684 -0
  90. crabcode_core/session/storage.py +2032 -0
  91. crabcode_core/session/title_gen.py +52 -0
  92. crabcode_core/skills/__init__.py +6 -0
  93. crabcode_core/skills/context.py +23 -0
  94. crabcode_core/skills/loader.py +214 -0
  95. crabcode_core/skills/matcher.py +203 -0
  96. crabcode_core/snapshot/__init__.py +21 -0
  97. crabcode_core/snapshot/snapshot.py +436 -0
  98. crabcode_core/snapshot/tracker.py +359 -0
  99. crabcode_core/subprocess_utils.py +260 -0
  100. crabcode_core/team/__init__.py +27 -0
  101. crabcode_core/team/bridge.py +66 -0
  102. crabcode_core/team/inbox.py +237 -0
  103. crabcode_core/team/manager.py +1660 -0
  104. crabcode_core/team/message_bus.py +821 -0
  105. crabcode_core/team/models.py +191 -0
  106. crabcode_core/team/recovery.py +192 -0
  107. crabcode_core/text_io.py +52 -0
  108. crabcode_core/tools/__init__.py +112 -0
  109. crabcode_core/tools/_input_helpers.py +39 -0
  110. crabcode_core/tools/_input_schema.py +130 -0
  111. crabcode_core/tools/agent.py +413 -0
  112. crabcode_core/tools/apply_patch.py +587 -0
  113. crabcode_core/tools/ask_user.py +122 -0
  114. crabcode_core/tools/bash.py +154 -0
  115. crabcode_core/tools/browser.py +678 -0
  116. crabcode_core/tools/checklist.py +241 -0
  117. crabcode_core/tools/checkpoint.py +78 -0
  118. crabcode_core/tools/computer_use.py +599 -0
  119. crabcode_core/tools/diff_utils.py +123 -0
  120. crabcode_core/tools/file_edit.py +240 -0
  121. crabcode_core/tools/file_read.py +187 -0
  122. crabcode_core/tools/file_write.py +154 -0
  123. crabcode_core/tools/glob.py +123 -0
  124. crabcode_core/tools/goal.py +134 -0
  125. crabcode_core/tools/grep.py +247 -0
  126. crabcode_core/tools/image.py +173 -0
  127. crabcode_core/tools/image_generate.py +115 -0
  128. crabcode_core/tools/lint.py +388 -0
  129. crabcode_core/tools/loading.py +187 -0
  130. crabcode_core/tools/memory.py +456 -0
  131. crabcode_core/tools/monitor.py +677 -0
  132. crabcode_core/tools/peer.py +103 -0
  133. crabcode_core/tools/revert.py +90 -0
  134. crabcode_core/tools/schedule.py +683 -0
  135. crabcode_core/tools/skill.py +108 -0
  136. crabcode_core/tools/switch_mode.py +213 -0
  137. crabcode_core/tools/team.py +329 -0
  138. crabcode_core/tools/tool_search.py +64 -0
  139. crabcode_core/tools/web_search.py +444 -0
  140. crabcode_core/turn_details.py +64 -0
  141. crabcode_core/types/__init__.py +95 -0
  142. crabcode_core/types/config.py +434 -0
  143. crabcode_core/types/event.py +334 -0
  144. crabcode_core/types/message.py +263 -0
  145. crabcode_core/types/tool.py +208 -0
  146. crabcode_core/usage.py +289 -0
  147. crabcode_core/utf8_sanitize.py +29 -0
  148. crabcode_debugger/__init__.py +5 -0
  149. crabcode_debugger/adapters.py +332 -0
  150. crabcode_debugger/dap.py +304 -0
  151. crabcode_debugger/memory.py +1806 -0
  152. crabcode_debugger/process.py +707 -0
  153. crabcode_debugger/runtime.py +58 -0
  154. crabcode_debugger/sessions.py +387 -0
  155. crabcode_debugger/tool.py +731 -0
  156. crabcode_gateway/__init__.py +18 -0
  157. crabcode_gateway/acp/__init__.py +13 -0
  158. crabcode_gateway/acp/agent.py +1581 -0
  159. crabcode_gateway/acp/session.py +114 -0
  160. crabcode_gateway/acp/transport.py +39 -0
  161. crabcode_gateway/acp/types.py +90 -0
  162. crabcode_gateway/adapter.py +30 -0
  163. crabcode_gateway/auth.py +139 -0
  164. crabcode_gateway/computer_use.py +364 -0
  165. crabcode_gateway/document_engine.py +637 -0
  166. crabcode_gateway/document_worker.py +301 -0
  167. crabcode_gateway/event_bus.py +655 -0
  168. crabcode_gateway/grpc/__init__.py +0 -0
  169. crabcode_gateway/grpc/crabcode_pb2.py +70 -0
  170. crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
  171. crabcode_gateway/grpc/proto/crabcode.proto +160 -0
  172. crabcode_gateway/grpc/server.py +765 -0
  173. crabcode_gateway/middleware.py +344 -0
  174. crabcode_gateway/protocol.py +17 -0
  175. crabcode_gateway/routes/__init__.py +0 -0
  176. crabcode_gateway/routes/agent.py +392 -0
  177. crabcode_gateway/routes/auth.py +150 -0
  178. crabcode_gateway/routes/computer_use.py +101 -0
  179. crabcode_gateway/routes/config.py +1698 -0
  180. crabcode_gateway/routes/document.py +1810 -0
  181. crabcode_gateway/routes/event.py +3801 -0
  182. crabcode_gateway/routes/git_review.py +232 -0
  183. crabcode_gateway/routes/health.py +14 -0
  184. crabcode_gateway/routes/peer.py +78 -0
  185. crabcode_gateway/routes/permission.py +121 -0
  186. crabcode_gateway/routes/schedule.py +300 -0
  187. crabcode_gateway/routes/session.py +1496 -0
  188. crabcode_gateway/routes/snapshot.py +211 -0
  189. crabcode_gateway/routes/tasks.py +318 -0
  190. crabcode_gateway/routes/team.py +392 -0
  191. crabcode_gateway/routes/usage.py +49 -0
  192. crabcode_gateway/routes/workspace.py +299 -0
  193. crabcode_gateway/safe_file.py +63 -0
  194. crabcode_gateway/schemas.py +1820 -0
  195. crabcode_gateway/server.py +514 -0
  196. crabcode_gateway/session_registry.py +36 -0
  197. crabcode_gateway/task_registry.py +708 -0
  198. crabcode_gateway/turn_details.py +60 -0
  199. crabcode_search/__init__.py +11 -0
  200. crabcode_search/background.py +368 -0
  201. crabcode_search/chunker.py +517 -0
  202. crabcode_search/embedder.py +497 -0
  203. crabcode_search/indexer.py +215 -0
  204. crabcode_search/store.py +222 -0
  205. crabcode_search/tool.py +268 -0
@@ -0,0 +1,20 @@
1
+ """API adapter layer - abstracts multiple LLM backends."""
2
+
3
+ from crabcode_core.api.base import APIAdapter, StreamChunk, ModelConfig
4
+ from crabcode_core.api.model_info import DEFAULT_CONTEXT_WINDOW, lookup_context_window
5
+ from crabcode_core.api.registry import create_adapter
6
+ from crabcode_core.api.ollama_adapter import OllamaAdapter
7
+ from crabcode_core.api.gemini_adapter import GeminiAdapter
8
+ from crabcode_core.api.azure_adapter import AzureOpenAIAdapter
9
+
10
+ __all__ = [
11
+ "APIAdapter",
12
+ "StreamChunk",
13
+ "ModelConfig",
14
+ "create_adapter",
15
+ "DEFAULT_CONTEXT_WINDOW",
16
+ "lookup_context_window",
17
+ "OllamaAdapter",
18
+ "GeminiAdapter",
19
+ "AzureOpenAIAdapter",
20
+ ]
@@ -0,0 +1,648 @@
1
+ """Anthropic API adapter — primary backend, closest to original behavior."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from typing import Any, AsyncGenerator
8
+
9
+ import anthropic
10
+ import httpx
11
+
12
+ from crabcode_core.api.base import (
13
+ APIAdapter,
14
+ ModelConfig,
15
+ StreamChunk,
16
+ usage_int_field,
17
+ )
18
+ from crabcode_core.logging_utils import get_logger
19
+ from crabcode_core.api.error_logging import response_request_id
20
+ from crabcode_core.paths import get_config_home
21
+ from crabcode_core.types.config import ApiConfig
22
+ from crabcode_core.utf8_sanitize import safe_utf8_json_tree, safe_utf8_str
23
+ from crabcode_core.types.message import (
24
+ ImageBlock,
25
+ Message,
26
+ MessageRole,
27
+ TextBlock,
28
+ ThinkingBlock,
29
+ ToolResultBlock,
30
+ ToolUseBlock,
31
+ )
32
+
33
+ logger = get_logger(__name__)
34
+
35
+
36
+ ANTHROPIC_VERSION = "2023-06-01"
37
+
38
+
39
+ def _anthropic_usage(
40
+ raw: Any,
41
+ *,
42
+ include_input: bool,
43
+ include_output: bool,
44
+ ) -> dict[str, int]:
45
+ usage: dict[str, int] = {}
46
+ if include_input:
47
+ input_tokens, has_input = usage_int_field(raw, "input_tokens")
48
+ cache_read, has_cache_read = usage_int_field(raw, "cache_read_input_tokens")
49
+ cache_write, has_cache_write = usage_int_field(raw, "cache_creation_input_tokens")
50
+ if has_input:
51
+ usage["input_tokens"] = input_tokens
52
+ usage["total_input_tokens"] = input_tokens + cache_read + cache_write
53
+ if has_cache_read:
54
+ usage["cache_read_tokens"] = cache_read
55
+ if has_cache_write:
56
+ usage["cache_write_tokens"] = cache_write
57
+ if include_output:
58
+ output_tokens, has_output = usage_int_field(raw, "output_tokens")
59
+ if has_output:
60
+ usage["output_tokens"] = output_tokens
61
+ return usage
62
+
63
+
64
+ def _messages_to_api(messages: list[Message]) -> list[dict[str, Any]]:
65
+ """Convert internal messages to Anthropic API format."""
66
+ result: list[dict[str, Any]] = []
67
+ for msg in messages:
68
+ if msg.role == MessageRole.SYSTEM:
69
+ continue
70
+
71
+ if isinstance(msg.content, str):
72
+ result.append({"role": msg.role.value, "content": msg.content})
73
+ continue
74
+
75
+ blocks: list[dict[str, Any]] = []
76
+ for block in msg.content:
77
+ if isinstance(block, TextBlock):
78
+ blocks.append({"type": "text", "text": block.text})
79
+ elif isinstance(block, ToolUseBlock):
80
+ blocks.append({
81
+ "type": "tool_use",
82
+ "id": block.id,
83
+ "name": block.name,
84
+ "input": block.input,
85
+ })
86
+ elif isinstance(block, ToolResultBlock):
87
+ blocks.append({
88
+ "type": "tool_result",
89
+ "tool_use_id": block.tool_use_id,
90
+ "content": block.content,
91
+ **({"is_error": True} if block.is_error else {}),
92
+ })
93
+ elif isinstance(block, ThinkingBlock):
94
+ blocks.append({
95
+ "type": "thinking",
96
+ "thinking": block.thinking,
97
+ })
98
+ elif isinstance(block, ImageBlock):
99
+ blocks.append({
100
+ "type": "image",
101
+ "source": block.source,
102
+ })
103
+
104
+ if blocks:
105
+ result.append({"role": msg.role.value, "content": blocks})
106
+
107
+ return safe_utf8_json_tree(result)
108
+
109
+
110
+ def _tools_to_api(tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
111
+ """Convert tool schemas to Anthropic API format."""
112
+ result = []
113
+ for tool in tools:
114
+ api_tool: dict[str, Any] = {
115
+ "name": tool["name"],
116
+ "description": tool.get("description", ""),
117
+ "input_schema": tool.get("input_schema", {"type": "object", "properties": {}}),
118
+ }
119
+ result.append(api_tool)
120
+ return safe_utf8_json_tree(result)
121
+
122
+
123
+ class AnthropicAdapter(APIAdapter):
124
+ """Adapter for Anthropic's Messages API (direct, first-party)."""
125
+
126
+ emits_response_item_events = True
127
+
128
+ def __init__(self, config: ApiConfig):
129
+ self.config = config
130
+ self._cached_context_window: int | None = None
131
+ api_key = None
132
+ if config.api_key_env:
133
+ api_key = os.environ.get(config.api_key_env)
134
+ if not api_key:
135
+ api_key = os.environ.get("ANTHROPIC_API_KEY")
136
+
137
+ kwargs: dict[str, Any] = {}
138
+ if api_key:
139
+ kwargs["api_key"] = api_key
140
+ if config.base_url:
141
+ kwargs["base_url"] = config.base_url
142
+ if config.http_headers:
143
+ kwargs["default_headers"] = config.http_headers
144
+
145
+ from crabcode_core.api.network import sdk_options
146
+ self.client = anthropic.AsyncAnthropic(**kwargs, **sdk_options(config))
147
+ self._api_key = api_key
148
+
149
+ def _messages_url(self) -> str | None:
150
+ """Return a Messages API URL for Anthropic-compatible custom endpoints."""
151
+ if not self.config.base_url:
152
+ return "https://api.anthropic.com/v1/messages"
153
+ base = self.config.base_url.rstrip("/")
154
+ if base.endswith("/v1"):
155
+ return f"{base}/messages"
156
+ return f"{base}/v1/messages"
157
+
158
+ def _manual_headers(self) -> dict[str, str]:
159
+ headers: dict[str, str] = {
160
+ "accept": "text/event-stream",
161
+ "content-type": "application/json",
162
+ "anthropic-version": ANTHROPIC_VERSION,
163
+ }
164
+ headers.update(self.config.http_headers or {})
165
+ lower_header_names = {k.lower() for k in headers}
166
+ if self._api_key and "x-api-key" not in lower_header_names:
167
+ headers["x-api-key"] = self._api_key
168
+ return headers
169
+
170
+ async def _stream_message_httpx(
171
+ self,
172
+ params: dict[str, Any],
173
+ ) -> AsyncGenerator[StreamChunk, None]:
174
+ """Stream from Anthropic-compatible routers without SDK helper headers.
175
+
176
+ Some Anthropic-compatible proxies reject the Python SDK's stream helper
177
+ headers and return a JSON error body with HTTP 200. Reading SSE directly
178
+ keeps the wire format Anthropic-compatible while avoiding those headers.
179
+ """
180
+ url = self._messages_url()
181
+ if not url:
182
+ return
183
+
184
+ payload = dict(params)
185
+ payload["stream"] = True
186
+
187
+ timeout = httpx.Timeout(
188
+ float(self.config.timeout),
189
+ connect=min(float(self.config.timeout), 10.0),
190
+ read=float(self.config.timeout),
191
+ write=float(self.config.timeout),
192
+ )
193
+ from crabcode_core.api.network import http_options
194
+ async with httpx.AsyncClient(timeout=timeout, **http_options(self.config)) as client:
195
+ async with client.stream(
196
+ "POST",
197
+ url,
198
+ headers=self._manual_headers(),
199
+ json=payload,
200
+ ) as response:
201
+ content_type = response.headers.get("content-type", "")
202
+ if response.status_code >= 400:
203
+ body = await response.aread()
204
+ yield StreamChunk(
205
+ type="error",
206
+ error_body=body,
207
+ error_status_code=response.status_code,
208
+ error_request_id=response_request_id(response),
209
+ error=(
210
+ body.decode("utf-8", errors="replace")
211
+ or response.reason_phrase
212
+ ),
213
+ )
214
+ return
215
+
216
+ if "text/event-stream" not in content_type.lower():
217
+ body = await response.aread()
218
+ text = body.decode("utf-8", errors="replace")
219
+ error_text = text
220
+ try:
221
+ data = json.loads(text)
222
+ error_text = (
223
+ data.get("error", {}).get("message")
224
+ if isinstance(data.get("error"), dict)
225
+ else data.get("code_msg") or data.get("message") or text
226
+ )
227
+ except Exception:
228
+ pass
229
+ yield StreamChunk(
230
+ type="error", error=str(error_text), error_body=text,
231
+ error_status_code=response.status_code,
232
+ error_request_id=response_request_id(response),
233
+ )
234
+ return
235
+
236
+ current_tool_id = ""
237
+ current_tool_name = ""
238
+ tool_input_buffer = ""
239
+ current_block_type = ""
240
+ current_block_id = ""
241
+
242
+ async for raw_line in response.aiter_lines():
243
+ if not raw_line.startswith("data:"):
244
+ continue
245
+ raw_data = raw_line[5:].strip()
246
+ if not raw_data or raw_data == "[DONE]":
247
+ continue
248
+ try:
249
+ event = json.loads(raw_data)
250
+ except json.JSONDecodeError:
251
+ continue
252
+
253
+ event_type = event.get("type", "")
254
+ if event_type == "content_block_start":
255
+ block = event.get("content_block") or {}
256
+ block_type = block.get("type")
257
+ current_block_type = str(block_type or "")
258
+ current_block_id = str(
259
+ block.get("id") or event.get("index") or ""
260
+ )
261
+ if block_type == "tool_use":
262
+ current_tool_id = str(block.get("id") or "")
263
+ current_tool_name = str(block.get("name") or "")
264
+ tool_input_buffer = ""
265
+ yield StreamChunk(
266
+ type="tool_use_start",
267
+ tool_use_id=current_tool_id,
268
+ tool_name=current_tool_name,
269
+ )
270
+
271
+ elif event_type == "content_block_delta":
272
+ delta = event.get("delta") or {}
273
+ delta_type = delta.get("type")
274
+ if delta_type == "text_delta":
275
+ yield StreamChunk(
276
+ type="text",
277
+ text=safe_utf8_str(str(delta.get("text") or "")),
278
+ )
279
+ elif delta_type == "thinking_delta":
280
+ yield StreamChunk(
281
+ type="thinking",
282
+ text=safe_utf8_str(str(delta.get("thinking") or "")),
283
+ )
284
+ elif delta_type == "input_json_delta":
285
+ partial = str(delta.get("partial_json") or "")
286
+ tool_input_buffer += partial
287
+ yield StreamChunk(
288
+ type="tool_use_delta",
289
+ tool_use_id=current_tool_id,
290
+ tool_input_json=partial,
291
+ )
292
+
293
+ elif event_type == "content_block_stop":
294
+ if current_tool_id:
295
+ yield StreamChunk(
296
+ type="tool_use_end",
297
+ tool_use_id=current_tool_id,
298
+ tool_name=current_tool_name,
299
+ tool_input_json=tool_input_buffer,
300
+ )
301
+ current_tool_id = ""
302
+ current_tool_name = ""
303
+ tool_input_buffer = ""
304
+ yield StreamChunk(
305
+ type="response_item_done",
306
+ item_id=current_block_id,
307
+ item_type=current_block_type,
308
+ )
309
+ current_block_type = ""
310
+ current_block_id = ""
311
+
312
+ elif event_type == "message_delta":
313
+ usage = {}
314
+ raw_usage = event.get("usage")
315
+ if isinstance(raw_usage, dict):
316
+ usage = _anthropic_usage(
317
+ raw_usage,
318
+ include_input=True,
319
+ include_output=True,
320
+ )
321
+ delta = event.get("delta") or {}
322
+ yield StreamChunk(
323
+ type="message_delta",
324
+ stop_reason=str(delta.get("stop_reason") or ""),
325
+ usage=usage,
326
+ )
327
+
328
+ elif event_type == "message_start":
329
+ usage = {}
330
+ message = event.get("message") or {}
331
+ raw_usage = message.get("usage")
332
+ if isinstance(raw_usage, dict):
333
+ usage = _anthropic_usage(
334
+ raw_usage,
335
+ include_input=True,
336
+ include_output=True,
337
+ )
338
+ yield StreamChunk(type="message_start", usage=usage,
339
+ model_id=str(message.get("model") or ""))
340
+
341
+ elif event_type == "message_stop":
342
+ yield StreamChunk(type="message_stop")
343
+
344
+ elif event_type == "error":
345
+ error = event.get("error") or {}
346
+ if isinstance(error, dict):
347
+ message = error.get("message") or json.dumps(error, ensure_ascii=False)
348
+ else:
349
+ message = str(error or event)
350
+ yield StreamChunk(
351
+ type="error", error=message, error_body=event,
352
+ error_status_code=response.status_code,
353
+ error_request_id=response_request_id(response),
354
+ )
355
+
356
+ async def resolve_context_window(self) -> int:
357
+ """Query the Anthropic Models API for context window, with caching."""
358
+ from crabcode_core.api.model_info import DEFAULT_CONTEXT_WINDOW, lookup_context_window
359
+
360
+ if self.config.context_window:
361
+ return self.config.context_window
362
+
363
+ if self._cached_context_window is not None:
364
+ return self._cached_context_window
365
+
366
+ model = self.config.model
367
+ if model:
368
+ try:
369
+ model_info = await self.client.models.retrieve(model_id=model)
370
+ window = getattr(model_info, "max_input_tokens", None)
371
+ if window:
372
+ self._cached_context_window = window
373
+ return window
374
+ except Exception:
375
+ logger.debug("Failed to query Anthropic Models API for %s", model, exc_info=True)
376
+
377
+ looked_up = lookup_context_window(model)
378
+ if looked_up is not None:
379
+ self._cached_context_window = looked_up
380
+ return looked_up
381
+
382
+ self._cached_context_window = DEFAULT_CONTEXT_WINDOW
383
+ return DEFAULT_CONTEXT_WINDOW
384
+
385
+ def _request_params(
386
+ self, messages: list[Message], system: list[str],
387
+ tools: list[dict[str, Any]], config: ModelConfig,
388
+ ) -> dict[str, Any]:
389
+ model = config.model or self.config.model
390
+ if not model:
391
+ raise ValueError(
392
+ f"No model configured. Set api.model in {get_config_home() / 'settings.json'} or use the -m flag."
393
+ )
394
+
395
+ system_blocks = safe_utf8_json_tree(
396
+ [{"type": "text", "text": s} for s in system if s]
397
+ )
398
+ # Mark the real static boundary; custom gateways must explicitly opt in.
399
+ static_count = getattr(system, "static_count", 0)
400
+ cache_enabled = self.config.prompt_caching == "enabled" or (
401
+ self.config.prompt_caching == "auto" and type(self) is AnthropicAdapter
402
+ and not self.config.base_url
403
+ and str(self.client.base_url).rstrip("/") == "https://api.anthropic.com"
404
+ )
405
+ if cache_enabled and static_count:
406
+ index = sum(bool(s) for s in system[:static_count]) - 1
407
+ if index >= 0:
408
+ system_blocks[index]["cache_control"] = {"type": "ephemeral"}
409
+
410
+ params: dict[str, Any] = {
411
+ "model": model,
412
+ "max_tokens": config.max_tokens,
413
+ "system": system_blocks,
414
+ "messages": _messages_to_api(messages),
415
+ }
416
+
417
+ api_tools = _tools_to_api(tools)
418
+ if api_tools:
419
+ params["tools"] = api_tools
420
+
421
+ if config.thinking_enabled:
422
+ params["thinking"] = {
423
+ "type": "enabled",
424
+ "budget_tokens": config.thinking_budget,
425
+ }
426
+
427
+ if config.reasoning_effort in {"low", "medium", "high", "xhigh", "max"}:
428
+ params["output_config"] = {"effort": config.reasoning_effort}
429
+
430
+ if config.temperature is not None:
431
+ params["temperature"] = config.temperature
432
+
433
+ return params
434
+
435
+ async def count_input_tokens(
436
+ self, messages: list[Message], system: list[str],
437
+ tools: list[dict[str, Any]], config: ModelConfig,
438
+ ) -> int | None:
439
+ # Bedrock/Vertex use different authentication and service endpoints.
440
+ if isinstance(self, (BedrockAdapter, VertexAdapter)):
441
+ return None
442
+ payload = self._request_params(messages, system, tools, config)
443
+ payload.pop("max_tokens", None)
444
+ payload.pop("temperature", None)
445
+ headers = {key: value for key, value in self._manual_headers().items()
446
+ if key.lower() != "accept"}
447
+ headers["accept"] = "application/json"
448
+ return await self._count_tokens_http(
449
+ f"{self._messages_url()}/count_tokens", payload, headers,
450
+ )
451
+
452
+ async def stream_message(
453
+ self,
454
+ messages: list[Message],
455
+ system: list[str],
456
+ tools: list[dict[str, Any]],
457
+ config: ModelConfig,
458
+ ) -> AsyncGenerator[StreamChunk, None]:
459
+ params = self._request_params(messages, system, tools, config)
460
+
461
+ transport = self.config.anthropic_stream_transport
462
+ use_httpx_stream = transport == "httpx" or (
463
+ transport == "auto" and bool(self.config.base_url)
464
+ )
465
+ stream = (
466
+ self._stream_message_httpx(params) if use_httpx_stream
467
+ else self._stream_message_sdk(params)
468
+ )
469
+ stop_reason = ""
470
+ completed_tools = 0
471
+ pending_tool = False
472
+ try:
473
+ async for chunk in stream:
474
+ if chunk.stop_reason:
475
+ stop_reason = chunk.stop_reason
476
+ if chunk.type == "tool_use_start":
477
+ pending_tool = True
478
+ elif chunk.type == "tool_use_end":
479
+ pending_tool = False
480
+ completed_tools += 1
481
+ elif chunk.type == "message_stop":
482
+ if pending_tool or (stop_reason == "tool_use" and not completed_tools):
483
+ yield StreamChunk(
484
+ type="error", retryable=True,
485
+ error="Anthropic stream ended without a complete declared tool call",
486
+ )
487
+ return
488
+ yield chunk
489
+ return
490
+ yield chunk
491
+ if chunk.type == "error":
492
+ return
493
+ # HTTP EOF (even HTTP 200 or [DONE]) is not the Messages protocol's
494
+ # message_stop. Let the query loop checkpoint completed items and
495
+ # consume its bounded reconnect budget instead of declaring success.
496
+ yield StreamChunk(
497
+ type="error", retryable=True,
498
+ error="Anthropic stream ended before message_stop (incomplete response)",
499
+ )
500
+ finally:
501
+ await stream.aclose()
502
+
503
+ async def _stream_message_sdk(
504
+ self, params: dict[str, Any],
505
+ ) -> AsyncGenerator[StreamChunk, None]:
506
+ current_tool_id = ""
507
+ current_tool_name = ""
508
+ tool_input_buffer = ""
509
+ current_block_type = ""
510
+ current_block_id = ""
511
+
512
+ async with self.client.messages.stream(**params) as stream:
513
+ async for event in stream:
514
+ event_type = getattr(event, "type", "")
515
+
516
+ if event_type == "content_block_start":
517
+ block = event.content_block
518
+ if hasattr(block, "type"):
519
+ current_block_type = str(block.type)
520
+ current_block_id = str(
521
+ getattr(block, "id", "")
522
+ or getattr(event, "index", "")
523
+ or ""
524
+ )
525
+ if block.type == "tool_use":
526
+ current_tool_id = block.id
527
+ current_tool_name = block.name
528
+ tool_input_buffer = ""
529
+ yield StreamChunk(
530
+ type="tool_use_start",
531
+ tool_use_id=block.id,
532
+ tool_name=block.name,
533
+ )
534
+ elif block.type == "thinking":
535
+ pass
536
+ elif block.type == "text":
537
+ pass
538
+
539
+ elif event_type == "content_block_delta":
540
+ delta = event.delta
541
+ if hasattr(delta, "type"):
542
+ if delta.type == "text_delta":
543
+ yield StreamChunk(type="text", text=safe_utf8_str(delta.text))
544
+ elif delta.type == "thinking_delta":
545
+ yield StreamChunk(
546
+ type="thinking", text=safe_utf8_str(delta.thinking)
547
+ )
548
+ elif delta.type == "input_json_delta":
549
+ tool_input_buffer += delta.partial_json
550
+ yield StreamChunk(
551
+ type="tool_use_delta",
552
+ tool_use_id=current_tool_id,
553
+ tool_input_json=delta.partial_json,
554
+ )
555
+
556
+ elif event_type == "content_block_stop":
557
+ if current_tool_id:
558
+ yield StreamChunk(
559
+ type="tool_use_end",
560
+ tool_use_id=current_tool_id,
561
+ tool_name=current_tool_name,
562
+ tool_input_json=tool_input_buffer,
563
+ )
564
+ current_tool_id = ""
565
+ current_tool_name = ""
566
+ tool_input_buffer = ""
567
+ yield StreamChunk(
568
+ type="response_item_done",
569
+ item_id=current_block_id,
570
+ item_type=current_block_type,
571
+ )
572
+ current_block_type = ""
573
+ current_block_id = ""
574
+
575
+ elif event_type == "message_delta":
576
+ usage = {}
577
+ if hasattr(event, "usage") and event.usage:
578
+ usage = _anthropic_usage(
579
+ event.usage,
580
+ include_input=True,
581
+ include_output=True,
582
+ )
583
+ stop_reason = getattr(event.delta, "stop_reason", "") or ""
584
+ yield StreamChunk(
585
+ type="message_delta",
586
+ stop_reason=stop_reason,
587
+ usage=usage,
588
+ )
589
+
590
+ elif event_type == "message_start":
591
+ usage = {}
592
+ if hasattr(event.message, "usage") and event.message.usage:
593
+ usage = _anthropic_usage(
594
+ event.message.usage,
595
+ include_input=True,
596
+ include_output=True,
597
+ )
598
+ yield StreamChunk(type="message_start", usage=usage,
599
+ model_id=str(getattr(event.message, "model", "") or ""))
600
+
601
+ elif event_type == "message_stop":
602
+ yield StreamChunk(type="message_stop")
603
+
604
+ async def count_tokens(
605
+ self,
606
+ messages: list[Message],
607
+ system: list[str],
608
+ ) -> int:
609
+ try:
610
+ result = await self.client.messages.count_tokens(
611
+ model=self.config.model or "",
612
+ messages=_messages_to_api(messages),
613
+ system=[{"type": "text", "text": s} for s in system if s],
614
+ )
615
+ return result.input_tokens
616
+ except Exception:
617
+ logger.debug("Anthropic token counting failed; using heuristic estimate", exc_info=True)
618
+ total = sum(len(s) for s in system)
619
+ for msg in messages:
620
+ if isinstance(msg.content, str):
621
+ total += len(msg.content)
622
+ else:
623
+ for block in msg.content:
624
+ if isinstance(block, TextBlock):
625
+ total += len(block.text)
626
+ return total // 4
627
+
628
+
629
+ class BedrockAdapter(AnthropicAdapter):
630
+ """Adapter for Anthropic via AWS Bedrock."""
631
+
632
+ def __init__(self, config: ApiConfig):
633
+ self.config = config
634
+ from crabcode_core.api.network import sdk_options
635
+ self.client = anthropic.AsyncAnthropicBedrock(**sdk_options(config))
636
+
637
+
638
+ class VertexAdapter(AnthropicAdapter):
639
+ """Adapter for Anthropic via Google Vertex AI."""
640
+
641
+ def __init__(self, config: ApiConfig):
642
+ from crabcode_core.api.network import sdk_options
643
+ self.config = config
644
+ self.client = anthropic.AsyncAnthropicVertex(
645
+ region=os.environ.get("CLOUD_ML_REGION", "us-east5"),
646
+ project_id=os.environ.get("ANTHROPIC_VERTEX_PROJECT_ID"),
647
+ **sdk_options(config),
648
+ )