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.
- crabcode-0.1.6.dist-info/METADATA +63 -0
- crabcode-0.1.6.dist-info/RECORD +205 -0
- crabcode-0.1.6.dist-info/WHEEL +4 -0
- crabcode-0.1.6.dist-info/entry_points.txt +2 -0
- crabcode_cli/__init__.py +7 -0
- crabcode_cli/__main__.py +4 -0
- crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
- crabcode_cli/_native/runtime.json +6 -0
- crabcode_cli/app.py +776 -0
- crabcode_cli/banner.py +39 -0
- crabcode_cli/computer_use.py +286 -0
- crabcode_cli/model_manager.py +649 -0
- crabcode_cli/model_picker.py +168 -0
- crabcode_cli/pipe.py +148 -0
- crabcode_cli/render/__init__.py +0 -0
- crabcode_cli/render/messages.py +19 -0
- crabcode_cli/repl.py +4841 -0
- crabcode_cli/session_picker.py +239 -0
- crabcode_core/__init__.py +7 -0
- crabcode_core/_version.py +3 -0
- crabcode_core/_windows_process.py +68 -0
- crabcode_core/agent_manager.py +1731 -0
- crabcode_core/api/__init__.py +20 -0
- crabcode_core/api/anthropic_adapter.py +648 -0
- crabcode_core/api/azure_adapter.py +69 -0
- crabcode_core/api/base.py +233 -0
- crabcode_core/api/codex_adapter.py +1327 -0
- crabcode_core/api/error_logging.py +132 -0
- crabcode_core/api/gemini_adapter.py +26 -0
- crabcode_core/api/model_info.py +257 -0
- crabcode_core/api/network.py +62 -0
- crabcode_core/api/ollama_adapter.py +27 -0
- crabcode_core/api/openai_adapter.py +323 -0
- crabcode_core/api/registry.py +58 -0
- crabcode_core/command_line.py +18 -0
- crabcode_core/compact/__init__.py +17 -0
- crabcode_core/compact/compact.py +514 -0
- crabcode_core/compact/context_tokens.py +165 -0
- crabcode_core/computer_use_mode.py +38 -0
- crabcode_core/computer_use_observation.py +144 -0
- crabcode_core/config/__init__.py +3 -0
- crabcode_core/config/claudemd.py +61 -0
- crabcode_core/config/manager.py +237 -0
- crabcode_core/config/model_catalog.py +256 -0
- crabcode_core/events.py +4896 -0
- crabcode_core/file_lock.py +81 -0
- crabcode_core/filesystem.py +22 -0
- crabcode_core/goal.py +149 -0
- crabcode_core/hooks/__init__.py +5 -0
- crabcode_core/hooks/manager.py +290 -0
- crabcode_core/image_io.py +111 -0
- crabcode_core/io_worker.py +211 -0
- crabcode_core/logging_utils.py +168 -0
- crabcode_core/lsp/__init__.py +6 -0
- crabcode_core/lsp/client.py +786 -0
- crabcode_core/lsp/diagnostics.py +150 -0
- crabcode_core/lsp/manager.py +583 -0
- crabcode_core/mcp/__init__.py +4 -0
- crabcode_core/mcp/client.py +169 -0
- crabcode_core/mcp/config.py +57 -0
- crabcode_core/path_validation.py +34 -0
- crabcode_core/paths.py +24 -0
- crabcode_core/peer/__init__.py +10 -0
- crabcode_core/peer/runtime.py +518 -0
- crabcode_core/permissions/__init__.py +7 -0
- crabcode_core/permissions/ai_reviewer.py +184 -0
- crabcode_core/permissions/manager.py +187 -0
- crabcode_core/plan/__init__.py +5 -0
- crabcode_core/plan/executor.py +270 -0
- crabcode_core/plan/types.py +169 -0
- crabcode_core/prompts/__init__.py +0 -0
- crabcode_core/prompts/blocks.py +10 -0
- crabcode_core/prompts/context.py +229 -0
- crabcode_core/prompts/library.py +92 -0
- crabcode_core/prompts/profile.py +79 -0
- crabcode_core/prompts/system.py +356 -0
- crabcode_core/prompts/templates.py +104 -0
- crabcode_core/query/__init__.py +0 -0
- crabcode_core/query/computer_use_history.py +83 -0
- crabcode_core/query/loop.py +2446 -0
- crabcode_core/query/retry.py +116 -0
- crabcode_core/schedule/__init__.py +13 -0
- crabcode_core/schedule/manager.py +631 -0
- crabcode_core/schedule/models.py +204 -0
- crabcode_core/schedule/store.py +558 -0
- crabcode_core/schedule/timing.py +287 -0
- crabcode_core/session/__init__.py +12 -0
- crabcode_core/session/export.py +141 -0
- crabcode_core/session/meta_db.py +684 -0
- crabcode_core/session/storage.py +2032 -0
- crabcode_core/session/title_gen.py +52 -0
- crabcode_core/skills/__init__.py +6 -0
- crabcode_core/skills/context.py +23 -0
- crabcode_core/skills/loader.py +214 -0
- crabcode_core/skills/matcher.py +203 -0
- crabcode_core/snapshot/__init__.py +21 -0
- crabcode_core/snapshot/snapshot.py +436 -0
- crabcode_core/snapshot/tracker.py +359 -0
- crabcode_core/subprocess_utils.py +260 -0
- crabcode_core/team/__init__.py +27 -0
- crabcode_core/team/bridge.py +66 -0
- crabcode_core/team/inbox.py +237 -0
- crabcode_core/team/manager.py +1660 -0
- crabcode_core/team/message_bus.py +821 -0
- crabcode_core/team/models.py +191 -0
- crabcode_core/team/recovery.py +192 -0
- crabcode_core/text_io.py +52 -0
- crabcode_core/tools/__init__.py +112 -0
- crabcode_core/tools/_input_helpers.py +39 -0
- crabcode_core/tools/_input_schema.py +130 -0
- crabcode_core/tools/agent.py +413 -0
- crabcode_core/tools/apply_patch.py +587 -0
- crabcode_core/tools/ask_user.py +122 -0
- crabcode_core/tools/bash.py +154 -0
- crabcode_core/tools/browser.py +678 -0
- crabcode_core/tools/checklist.py +241 -0
- crabcode_core/tools/checkpoint.py +78 -0
- crabcode_core/tools/computer_use.py +599 -0
- crabcode_core/tools/diff_utils.py +123 -0
- crabcode_core/tools/file_edit.py +240 -0
- crabcode_core/tools/file_read.py +187 -0
- crabcode_core/tools/file_write.py +154 -0
- crabcode_core/tools/glob.py +123 -0
- crabcode_core/tools/goal.py +134 -0
- crabcode_core/tools/grep.py +247 -0
- crabcode_core/tools/image.py +173 -0
- crabcode_core/tools/image_generate.py +115 -0
- crabcode_core/tools/lint.py +388 -0
- crabcode_core/tools/loading.py +187 -0
- crabcode_core/tools/memory.py +456 -0
- crabcode_core/tools/monitor.py +677 -0
- crabcode_core/tools/peer.py +103 -0
- crabcode_core/tools/revert.py +90 -0
- crabcode_core/tools/schedule.py +683 -0
- crabcode_core/tools/skill.py +108 -0
- crabcode_core/tools/switch_mode.py +213 -0
- crabcode_core/tools/team.py +329 -0
- crabcode_core/tools/tool_search.py +64 -0
- crabcode_core/tools/web_search.py +444 -0
- crabcode_core/turn_details.py +64 -0
- crabcode_core/types/__init__.py +95 -0
- crabcode_core/types/config.py +434 -0
- crabcode_core/types/event.py +334 -0
- crabcode_core/types/message.py +263 -0
- crabcode_core/types/tool.py +208 -0
- crabcode_core/usage.py +289 -0
- crabcode_core/utf8_sanitize.py +29 -0
- crabcode_debugger/__init__.py +5 -0
- crabcode_debugger/adapters.py +332 -0
- crabcode_debugger/dap.py +304 -0
- crabcode_debugger/memory.py +1806 -0
- crabcode_debugger/process.py +707 -0
- crabcode_debugger/runtime.py +58 -0
- crabcode_debugger/sessions.py +387 -0
- crabcode_debugger/tool.py +731 -0
- crabcode_gateway/__init__.py +18 -0
- crabcode_gateway/acp/__init__.py +13 -0
- crabcode_gateway/acp/agent.py +1581 -0
- crabcode_gateway/acp/session.py +114 -0
- crabcode_gateway/acp/transport.py +39 -0
- crabcode_gateway/acp/types.py +90 -0
- crabcode_gateway/adapter.py +30 -0
- crabcode_gateway/auth.py +139 -0
- crabcode_gateway/computer_use.py +364 -0
- crabcode_gateway/document_engine.py +637 -0
- crabcode_gateway/document_worker.py +301 -0
- crabcode_gateway/event_bus.py +655 -0
- crabcode_gateway/grpc/__init__.py +0 -0
- crabcode_gateway/grpc/crabcode_pb2.py +70 -0
- crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
- crabcode_gateway/grpc/proto/crabcode.proto +160 -0
- crabcode_gateway/grpc/server.py +765 -0
- crabcode_gateway/middleware.py +344 -0
- crabcode_gateway/protocol.py +17 -0
- crabcode_gateway/routes/__init__.py +0 -0
- crabcode_gateway/routes/agent.py +392 -0
- crabcode_gateway/routes/auth.py +150 -0
- crabcode_gateway/routes/computer_use.py +101 -0
- crabcode_gateway/routes/config.py +1698 -0
- crabcode_gateway/routes/document.py +1810 -0
- crabcode_gateway/routes/event.py +3801 -0
- crabcode_gateway/routes/git_review.py +232 -0
- crabcode_gateway/routes/health.py +14 -0
- crabcode_gateway/routes/peer.py +78 -0
- crabcode_gateway/routes/permission.py +121 -0
- crabcode_gateway/routes/schedule.py +300 -0
- crabcode_gateway/routes/session.py +1496 -0
- crabcode_gateway/routes/snapshot.py +211 -0
- crabcode_gateway/routes/tasks.py +318 -0
- crabcode_gateway/routes/team.py +392 -0
- crabcode_gateway/routes/usage.py +49 -0
- crabcode_gateway/routes/workspace.py +299 -0
- crabcode_gateway/safe_file.py +63 -0
- crabcode_gateway/schemas.py +1820 -0
- crabcode_gateway/server.py +514 -0
- crabcode_gateway/session_registry.py +36 -0
- crabcode_gateway/task_registry.py +708 -0
- crabcode_gateway/turn_details.py +60 -0
- crabcode_search/__init__.py +11 -0
- crabcode_search/background.py +368 -0
- crabcode_search/chunker.py +517 -0
- crabcode_search/embedder.py +497 -0
- crabcode_search/indexer.py +215 -0
- crabcode_search/store.py +222 -0
- 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
|
+
)
|