devcopilot 0.2.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.
- api/__init__.py +17 -0
- api/admin_config.py +1303 -0
- api/admin_routes.py +287 -0
- api/admin_static/admin.css +459 -0
- api/admin_static/admin.js +497 -0
- api/admin_static/index.html +77 -0
- api/admin_urls.py +34 -0
- api/app.py +194 -0
- api/command_utils.py +164 -0
- api/dependencies.py +144 -0
- api/detection.py +152 -0
- api/gateway_model_ids.py +54 -0
- api/model_catalog.py +133 -0
- api/model_router.py +125 -0
- api/models/__init__.py +45 -0
- api/models/anthropic.py +234 -0
- api/models/openai_responses.py +28 -0
- api/models/responses.py +60 -0
- api/optimization_handlers.py +154 -0
- api/request_pipeline.py +424 -0
- api/routes.py +156 -0
- api/runtime.py +334 -0
- api/validation_log.py +48 -0
- api/web_server_tools.py +22 -0
- api/web_tools/__init__.py +17 -0
- api/web_tools/constants.py +15 -0
- api/web_tools/egress.py +99 -0
- api/web_tools/outbound.py +278 -0
- api/web_tools/parsers.py +104 -0
- api/web_tools/request.py +87 -0
- api/web_tools/streaming.py +206 -0
- cli/__init__.py +5 -0
- cli/claude_env.py +12 -0
- cli/entrypoints.py +166 -0
- cli/env.example +209 -0
- cli/launchers/__init__.py +1 -0
- cli/launchers/claude.py +84 -0
- cli/launchers/codex.py +204 -0
- cli/launchers/codex_model_catalog.py +186 -0
- cli/launchers/common.py +93 -0
- cli/managed/__init__.py +6 -0
- cli/managed/claude.py +215 -0
- cli/managed/manager.py +157 -0
- cli/managed/session.py +260 -0
- cli/process_registry.py +78 -0
- config/__init__.py +5 -0
- config/constants.py +13 -0
- config/logging_config.py +159 -0
- config/nim.py +118 -0
- config/paths.py +91 -0
- config/provider_catalog.py +259 -0
- config/provider_ids.py +7 -0
- config/settings.py +538 -0
- core/__init__.py +1 -0
- core/anthropic/__init__.py +46 -0
- core/anthropic/content.py +31 -0
- core/anthropic/conversion.py +587 -0
- core/anthropic/emitted_sse_tracker.py +346 -0
- core/anthropic/errors.py +70 -0
- core/anthropic/native_messages_request.py +280 -0
- core/anthropic/native_sse_block_policy.py +313 -0
- core/anthropic/provider_stream_error.py +34 -0
- core/anthropic/server_tool_sse.py +14 -0
- core/anthropic/sse.py +440 -0
- core/anthropic/stream_contracts.py +205 -0
- core/anthropic/stream_recovery.py +346 -0
- core/anthropic/stream_recovery_session.py +133 -0
- core/anthropic/thinking.py +140 -0
- core/anthropic/tokens.py +117 -0
- core/anthropic/tools.py +212 -0
- core/anthropic/utils.py +9 -0
- core/openai_responses/__init__.py +5 -0
- core/openai_responses/adapter.py +31 -0
- core/openai_responses/anthropic_sse.py +59 -0
- core/openai_responses/errors.py +22 -0
- core/openai_responses/events.py +19 -0
- core/openai_responses/ids.py +21 -0
- core/openai_responses/input.py +258 -0
- core/openai_responses/items.py +37 -0
- core/openai_responses/reasoning.py +52 -0
- core/openai_responses/stream.py +25 -0
- core/openai_responses/stream_state.py +654 -0
- core/openai_responses/tools.py +374 -0
- core/openai_responses/usage.py +37 -0
- core/rate_limit.py +60 -0
- core/trace.py +216 -0
- devcopilot-0.2.0.dist-info/METADATA +687 -0
- devcopilot-0.2.0.dist-info/RECORD +189 -0
- devcopilot-0.2.0.dist-info/WHEEL +4 -0
- devcopilot-0.2.0.dist-info/entry_points.txt +6 -0
- devcopilot-0.2.0.dist-info/licenses/LICENSE +21 -0
- messaging/__init__.py +26 -0
- messaging/cli_event_constants.py +67 -0
- messaging/command_context.py +66 -0
- messaging/command_dispatcher.py +37 -0
- messaging/commands.py +275 -0
- messaging/event_parser.py +181 -0
- messaging/limiter.py +300 -0
- messaging/models.py +36 -0
- messaging/node_event_pipeline.py +127 -0
- messaging/node_runner.py +342 -0
- messaging/platforms/__init__.py +15 -0
- messaging/platforms/base.py +228 -0
- messaging/platforms/discord.py +567 -0
- messaging/platforms/factory.py +103 -0
- messaging/platforms/outbox.py +144 -0
- messaging/platforms/telegram.py +688 -0
- messaging/platforms/voice_flow.py +295 -0
- messaging/rendering/__init__.py +3 -0
- messaging/rendering/discord_markdown.py +318 -0
- messaging/rendering/markdown_tables.py +49 -0
- messaging/rendering/profiles.py +55 -0
- messaging/rendering/telegram_markdown.py +327 -0
- messaging/safe_diagnostics.py +17 -0
- messaging/session.py +334 -0
- messaging/transcript.py +581 -0
- messaging/transcription.py +164 -0
- messaging/trees/__init__.py +15 -0
- messaging/trees/data.py +482 -0
- messaging/trees/manager.py +433 -0
- messaging/trees/processor.py +179 -0
- messaging/trees/repository.py +177 -0
- messaging/turn_intake.py +235 -0
- messaging/ui_updates.py +101 -0
- messaging/voice.py +76 -0
- messaging/workflow.py +200 -0
- providers/__init__.py +31 -0
- providers/base.py +152 -0
- providers/cerebras/__init__.py +7 -0
- providers/cerebras/client.py +31 -0
- providers/cerebras/request.py +55 -0
- providers/codestral/__init__.py +7 -0
- providers/codestral/client.py +34 -0
- providers/deepseek/__init__.py +11 -0
- providers/deepseek/client.py +51 -0
- providers/deepseek/request.py +475 -0
- providers/defaults.py +41 -0
- providers/error_mapping.py +309 -0
- providers/exceptions.py +113 -0
- providers/fireworks/__init__.py +5 -0
- providers/fireworks/client.py +45 -0
- providers/fireworks/request.py +48 -0
- providers/gemini/__init__.py +7 -0
- providers/gemini/client.py +49 -0
- providers/gemini/request.py +199 -0
- providers/groq/__init__.py +7 -0
- providers/groq/client.py +31 -0
- providers/groq/request.py +83 -0
- providers/kimi/__init__.py +10 -0
- providers/kimi/client.py +53 -0
- providers/kimi/request.py +42 -0
- providers/llamacpp/__init__.py +3 -0
- providers/llamacpp/client.py +16 -0
- providers/lmstudio/__init__.py +5 -0
- providers/lmstudio/client.py +16 -0
- providers/mistral/__init__.py +7 -0
- providers/mistral/client.py +31 -0
- providers/mistral/request.py +37 -0
- providers/model_listing.py +133 -0
- providers/nvidia_nim/__init__.py +7 -0
- providers/nvidia_nim/client.py +91 -0
- providers/nvidia_nim/request.py +430 -0
- providers/nvidia_nim/voice.py +95 -0
- providers/ollama/__init__.py +7 -0
- providers/ollama/client.py +39 -0
- providers/open_router/__init__.py +7 -0
- providers/open_router/client.py +124 -0
- providers/open_router/request.py +42 -0
- providers/opencode/__init__.py +11 -0
- providers/opencode/client.py +31 -0
- providers/opencode/request.py +35 -0
- providers/rate_limit.py +300 -0
- providers/registry.py +527 -0
- providers/transports/__init__.py +1 -0
- providers/transports/anthropic_messages/__init__.py +5 -0
- providers/transports/anthropic_messages/http.py +118 -0
- providers/transports/anthropic_messages/recovery.py +206 -0
- providers/transports/anthropic_messages/stream.py +295 -0
- providers/transports/anthropic_messages/transport.py +236 -0
- providers/transports/openai_chat/__init__.py +5 -0
- providers/transports/openai_chat/recovery.py +217 -0
- providers/transports/openai_chat/stream.py +384 -0
- providers/transports/openai_chat/tool_calls.py +293 -0
- providers/transports/openai_chat/transport.py +156 -0
- providers/wafer/__init__.py +10 -0
- providers/wafer/client.py +50 -0
- providers/zai/__init__.py +10 -0
- providers/zai/client.py +46 -0
- providers/zai/request.py +42 -0
core/anthropic/tokens.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Token estimation for Anthropic-compatible requests."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import tiktoken
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from .content import get_block_attr
|
|
9
|
+
|
|
10
|
+
ENCODER = tiktoken.get_encoding("cl100k_base")
|
|
11
|
+
|
|
12
|
+
_DISALLOWED_SPECIAL: tuple[str, ...] = ()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _count_text_tokens(text: str) -> int:
|
|
16
|
+
return len(ENCODER.encode(text, disallowed_special=_DISALLOWED_SPECIAL))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_token_count(
|
|
20
|
+
messages: list,
|
|
21
|
+
system: str | list | None = None,
|
|
22
|
+
tools: list | None = None,
|
|
23
|
+
) -> int:
|
|
24
|
+
"""Estimate token count for a request."""
|
|
25
|
+
total_tokens = 0
|
|
26
|
+
|
|
27
|
+
if system:
|
|
28
|
+
if isinstance(system, str):
|
|
29
|
+
total_tokens += _count_text_tokens(system)
|
|
30
|
+
elif isinstance(system, list):
|
|
31
|
+
for block in system:
|
|
32
|
+
text = get_block_attr(block, "text", "")
|
|
33
|
+
if text:
|
|
34
|
+
total_tokens += _count_text_tokens(str(text))
|
|
35
|
+
total_tokens += 4
|
|
36
|
+
|
|
37
|
+
for msg in messages:
|
|
38
|
+
if isinstance(msg.content, str):
|
|
39
|
+
total_tokens += _count_text_tokens(msg.content)
|
|
40
|
+
elif isinstance(msg.content, list):
|
|
41
|
+
for block in msg.content:
|
|
42
|
+
b_type = get_block_attr(block, "type") or None
|
|
43
|
+
|
|
44
|
+
if b_type == "text":
|
|
45
|
+
text = get_block_attr(block, "text", "")
|
|
46
|
+
total_tokens += _count_text_tokens(str(text))
|
|
47
|
+
elif b_type == "thinking":
|
|
48
|
+
thinking = get_block_attr(block, "thinking", "")
|
|
49
|
+
total_tokens += _count_text_tokens(str(thinking))
|
|
50
|
+
elif b_type == "tool_use":
|
|
51
|
+
name = get_block_attr(block, "name", "")
|
|
52
|
+
inp = get_block_attr(block, "input", {})
|
|
53
|
+
block_id = get_block_attr(block, "id", "")
|
|
54
|
+
total_tokens += _count_text_tokens(str(name))
|
|
55
|
+
total_tokens += _count_text_tokens(json.dumps(inp))
|
|
56
|
+
total_tokens += _count_text_tokens(str(block_id))
|
|
57
|
+
total_tokens += 15
|
|
58
|
+
elif b_type == "image":
|
|
59
|
+
source = get_block_attr(block, "source")
|
|
60
|
+
if isinstance(source, dict):
|
|
61
|
+
data = source.get("data") or source.get("base64") or ""
|
|
62
|
+
if data:
|
|
63
|
+
total_tokens += max(85, len(data) // 3000)
|
|
64
|
+
else:
|
|
65
|
+
total_tokens += 765
|
|
66
|
+
else:
|
|
67
|
+
total_tokens += 765
|
|
68
|
+
elif b_type == "tool_result":
|
|
69
|
+
content = get_block_attr(block, "content", "")
|
|
70
|
+
tool_use_id = get_block_attr(block, "tool_use_id", "")
|
|
71
|
+
if isinstance(content, str):
|
|
72
|
+
total_tokens += _count_text_tokens(content)
|
|
73
|
+
else:
|
|
74
|
+
total_tokens += _count_text_tokens(json.dumps(content))
|
|
75
|
+
total_tokens += _count_text_tokens(str(tool_use_id))
|
|
76
|
+
total_tokens += 8
|
|
77
|
+
elif b_type in (
|
|
78
|
+
"server_tool_use",
|
|
79
|
+
"web_search_tool_result",
|
|
80
|
+
"web_fetch_tool_result",
|
|
81
|
+
):
|
|
82
|
+
if hasattr(block, "model_dump"):
|
|
83
|
+
blob: object = block.model_dump()
|
|
84
|
+
else:
|
|
85
|
+
blob = block
|
|
86
|
+
try:
|
|
87
|
+
total_tokens += _count_text_tokens(
|
|
88
|
+
json.dumps(blob, default=str, ensure_ascii=False)
|
|
89
|
+
)
|
|
90
|
+
except (TypeError, ValueError, OverflowError) as e:
|
|
91
|
+
logger.debug(
|
|
92
|
+
"Block encode fallback b_type={} err={}", b_type, e
|
|
93
|
+
)
|
|
94
|
+
total_tokens += _count_text_tokens(str(blob))
|
|
95
|
+
total_tokens += 12
|
|
96
|
+
else:
|
|
97
|
+
logger.debug(
|
|
98
|
+
"Unexpected block type %r, falling back to json/str encoding",
|
|
99
|
+
b_type,
|
|
100
|
+
)
|
|
101
|
+
try:
|
|
102
|
+
total_tokens += _count_text_tokens(json.dumps(block))
|
|
103
|
+
except TypeError, ValueError:
|
|
104
|
+
total_tokens += _count_text_tokens(str(block))
|
|
105
|
+
|
|
106
|
+
if tools:
|
|
107
|
+
for tool in tools:
|
|
108
|
+
tool_str = (
|
|
109
|
+
tool.name + (tool.description or "") + json.dumps(tool.input_schema)
|
|
110
|
+
)
|
|
111
|
+
total_tokens += _count_text_tokens(tool_str)
|
|
112
|
+
|
|
113
|
+
total_tokens += len(messages) * 4
|
|
114
|
+
if tools:
|
|
115
|
+
total_tokens += len(tools) * 5
|
|
116
|
+
|
|
117
|
+
return max(1, total_tokens)
|
core/anthropic/tools.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Heuristic parser for text-emitted tool calls."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import uuid
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
_CONTROL_TOKEN_RE = re.compile(r"<\|[^|>]{1,80}\|>")
|
|
12
|
+
_CONTROL_TOKEN_START = "<|"
|
|
13
|
+
_CONTROL_TOKEN_END = "|>"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ParserState(Enum):
|
|
17
|
+
TEXT = 1
|
|
18
|
+
MATCHING_FUNCTION = 2
|
|
19
|
+
PARSING_PARAMETERS = 3
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HeuristicToolParser:
|
|
23
|
+
"""
|
|
24
|
+
Stateful parser for raw text tool calls.
|
|
25
|
+
|
|
26
|
+
Some OpenAI-compatible models emit tool calls as text rather than structured
|
|
27
|
+
chunks. This parser converts the common ``● <function=...>`` form into
|
|
28
|
+
Anthropic-style ``tool_use`` blocks.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
_FUNC_START_PATTERN = re.compile(r"●\s*<function=([^>]+)>")
|
|
32
|
+
_PARAM_PATTERN = re.compile(
|
|
33
|
+
r"<parameter=([^>]+)>(.*?)(?:</parameter>|$)", re.DOTALL
|
|
34
|
+
)
|
|
35
|
+
_WEB_TOOL_JSON_PATTERN = re.compile(
|
|
36
|
+
r"(?is)\b(?:use\s+)?(?P<tool>WebFetch|WebSearch)\b.*?(?P<json>\{.*?\})"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def __init__(self):
|
|
40
|
+
self._state = ParserState.TEXT
|
|
41
|
+
self._buffer = ""
|
|
42
|
+
self._current_tool_id = None
|
|
43
|
+
self._current_function_name = None
|
|
44
|
+
self._current_parameters = {}
|
|
45
|
+
|
|
46
|
+
def _extract_web_tool_json_calls(self) -> tuple[str, list[dict[str, Any]]]:
|
|
47
|
+
detected_tools: list[dict[str, Any]] = []
|
|
48
|
+
|
|
49
|
+
for match in self._WEB_TOOL_JSON_PATTERN.finditer(self._buffer):
|
|
50
|
+
try:
|
|
51
|
+
tool_input = json.loads(match.group("json"))
|
|
52
|
+
except json.JSONDecodeError:
|
|
53
|
+
continue
|
|
54
|
+
if not isinstance(tool_input, dict):
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
tool_name = match.group("tool")
|
|
58
|
+
if tool_name == "WebFetch" and "url" not in tool_input:
|
|
59
|
+
continue
|
|
60
|
+
if tool_name == "WebSearch" and "query" not in tool_input:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
detected_tools.append(
|
|
64
|
+
{
|
|
65
|
+
"type": "tool_use",
|
|
66
|
+
"id": f"toolu_heuristic_{uuid.uuid4().hex[:8]}",
|
|
67
|
+
"name": tool_name,
|
|
68
|
+
"input": tool_input,
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
logger.debug(
|
|
72
|
+
"Heuristic bypass: Detected JSON-style tool call '{}'",
|
|
73
|
+
tool_name,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if not detected_tools:
|
|
77
|
+
return self._buffer, []
|
|
78
|
+
|
|
79
|
+
return "", detected_tools
|
|
80
|
+
|
|
81
|
+
def _strip_control_tokens(self, text: str) -> str:
|
|
82
|
+
return _CONTROL_TOKEN_RE.sub("", text)
|
|
83
|
+
|
|
84
|
+
def _split_incomplete_control_token_tail(self) -> str:
|
|
85
|
+
start = self._buffer.rfind(_CONTROL_TOKEN_START)
|
|
86
|
+
if start == -1:
|
|
87
|
+
return ""
|
|
88
|
+
end = self._buffer.find(_CONTROL_TOKEN_END, start)
|
|
89
|
+
if end != -1:
|
|
90
|
+
return ""
|
|
91
|
+
|
|
92
|
+
prefix = self._buffer[:start]
|
|
93
|
+
self._buffer = self._buffer[start:]
|
|
94
|
+
return prefix
|
|
95
|
+
|
|
96
|
+
def feed(self, text: str) -> tuple[str, list[dict[str, Any]]]:
|
|
97
|
+
"""Feed text and return safe text plus detected tool calls."""
|
|
98
|
+
self._buffer += text
|
|
99
|
+
self._buffer = self._strip_control_tokens(self._buffer)
|
|
100
|
+
self._buffer, detected_tools = self._extract_web_tool_json_calls()
|
|
101
|
+
filtered_output_parts: list[str] = []
|
|
102
|
+
|
|
103
|
+
while True:
|
|
104
|
+
if self._state == ParserState.TEXT:
|
|
105
|
+
if "●" in self._buffer:
|
|
106
|
+
idx = self._buffer.find("●")
|
|
107
|
+
filtered_output_parts.append(self._buffer[:idx])
|
|
108
|
+
self._buffer = self._buffer[idx:]
|
|
109
|
+
self._state = ParserState.MATCHING_FUNCTION
|
|
110
|
+
else:
|
|
111
|
+
safe_prefix = self._split_incomplete_control_token_tail()
|
|
112
|
+
if safe_prefix:
|
|
113
|
+
filtered_output_parts.append(safe_prefix)
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
filtered_output_parts.append(self._buffer)
|
|
117
|
+
self._buffer = ""
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
if self._state == ParserState.MATCHING_FUNCTION:
|
|
121
|
+
match = self._FUNC_START_PATTERN.search(self._buffer)
|
|
122
|
+
if match:
|
|
123
|
+
self._current_function_name = match.group(1).strip()
|
|
124
|
+
self._current_tool_id = f"toolu_heuristic_{uuid.uuid4().hex[:8]}"
|
|
125
|
+
self._current_parameters = {}
|
|
126
|
+
self._buffer = self._buffer[match.end() :]
|
|
127
|
+
self._state = ParserState.PARSING_PARAMETERS
|
|
128
|
+
logger.debug(
|
|
129
|
+
"Heuristic bypass: Detected start of tool call '{}'",
|
|
130
|
+
self._current_function_name,
|
|
131
|
+
)
|
|
132
|
+
elif len(self._buffer) > 100:
|
|
133
|
+
filtered_output_parts.append(self._buffer[0])
|
|
134
|
+
self._buffer = self._buffer[1:]
|
|
135
|
+
self._state = ParserState.TEXT
|
|
136
|
+
else:
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
if self._state == ParserState.PARSING_PARAMETERS:
|
|
140
|
+
finished_tool_call = False
|
|
141
|
+
|
|
142
|
+
while True:
|
|
143
|
+
param_match = self._PARAM_PATTERN.search(self._buffer)
|
|
144
|
+
if param_match and "</parameter>" in param_match.group(0):
|
|
145
|
+
pre_match_text = self._buffer[: param_match.start()]
|
|
146
|
+
if pre_match_text:
|
|
147
|
+
filtered_output_parts.append(pre_match_text)
|
|
148
|
+
|
|
149
|
+
key = param_match.group(1).strip()
|
|
150
|
+
val = param_match.group(2).strip()
|
|
151
|
+
self._current_parameters[key] = val
|
|
152
|
+
self._buffer = self._buffer[param_match.end() :]
|
|
153
|
+
else:
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
if "●" in self._buffer:
|
|
157
|
+
idx = self._buffer.find("●")
|
|
158
|
+
if idx > 0:
|
|
159
|
+
filtered_output_parts.append(self._buffer[:idx])
|
|
160
|
+
self._buffer = self._buffer[idx:]
|
|
161
|
+
finished_tool_call = True
|
|
162
|
+
elif len(self._buffer) > 0 and not self._buffer.strip().startswith("<"):
|
|
163
|
+
if "<parameter=" not in self._buffer:
|
|
164
|
+
filtered_output_parts.append(self._buffer)
|
|
165
|
+
self._buffer = ""
|
|
166
|
+
finished_tool_call = True
|
|
167
|
+
|
|
168
|
+
if finished_tool_call:
|
|
169
|
+
detected_tools.append(
|
|
170
|
+
{
|
|
171
|
+
"type": "tool_use",
|
|
172
|
+
"id": self._current_tool_id,
|
|
173
|
+
"name": self._current_function_name,
|
|
174
|
+
"input": self._current_parameters,
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
logger.debug(
|
|
178
|
+
"Heuristic bypass: Emitting tool call '{}' with {} params",
|
|
179
|
+
self._current_function_name,
|
|
180
|
+
len(self._current_parameters),
|
|
181
|
+
)
|
|
182
|
+
self._state = ParserState.TEXT
|
|
183
|
+
else:
|
|
184
|
+
break
|
|
185
|
+
|
|
186
|
+
return "".join(filtered_output_parts), detected_tools
|
|
187
|
+
|
|
188
|
+
def flush(self) -> list[dict[str, Any]]:
|
|
189
|
+
"""Flush any remaining tool call in the buffer."""
|
|
190
|
+
self._buffer = self._strip_control_tokens(self._buffer)
|
|
191
|
+
detected_tools = []
|
|
192
|
+
if self._state == ParserState.PARSING_PARAMETERS:
|
|
193
|
+
partial_matches = re.finditer(
|
|
194
|
+
r"<parameter=([^>]+)>(.*)$", self._buffer, re.DOTALL
|
|
195
|
+
)
|
|
196
|
+
for match in partial_matches:
|
|
197
|
+
key = match.group(1).strip()
|
|
198
|
+
val = match.group(2).strip()
|
|
199
|
+
self._current_parameters[key] = val
|
|
200
|
+
|
|
201
|
+
detected_tools.append(
|
|
202
|
+
{
|
|
203
|
+
"type": "tool_use",
|
|
204
|
+
"id": self._current_tool_id,
|
|
205
|
+
"name": self._current_function_name,
|
|
206
|
+
"input": self._current_parameters,
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
self._state = ParserState.TEXT
|
|
210
|
+
self._buffer = ""
|
|
211
|
+
|
|
212
|
+
return detected_tools
|
core/anthropic/utils.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Facade for OpenAI Responses protocol adaptation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncIterable, AsyncIterator, Mapping
|
|
6
|
+
from typing import Any, ClassVar
|
|
7
|
+
|
|
8
|
+
from .errors import ResponsesConversionError, openai_error_payload
|
|
9
|
+
from .events import OPENAI_RESPONSES_SSE_HEADERS
|
|
10
|
+
from .input import convert_request_to_anthropic_payload
|
|
11
|
+
from .stream import iter_responses_sse_from_anthropic
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OpenAIResponsesAdapter:
|
|
15
|
+
"""Convert between OpenAI Responses and the proxy's Anthropic core path."""
|
|
16
|
+
|
|
17
|
+
ConversionError: ClassVar[type[ResponsesConversionError]] = ResponsesConversionError
|
|
18
|
+
sse_headers: ClassVar[dict[str, str]] = OPENAI_RESPONSES_SSE_HEADERS
|
|
19
|
+
|
|
20
|
+
def to_anthropic_payload(self, request: Mapping[str, Any]) -> dict[str, Any]:
|
|
21
|
+
return convert_request_to_anthropic_payload(request)
|
|
22
|
+
|
|
23
|
+
def iter_sse_from_anthropic(
|
|
24
|
+
self,
|
|
25
|
+
chunks: AsyncIterable[Any],
|
|
26
|
+
request: Mapping[str, Any],
|
|
27
|
+
) -> AsyncIterator[str]:
|
|
28
|
+
return iter_responses_sse_from_anthropic(chunks, request)
|
|
29
|
+
|
|
30
|
+
def error_payload(self, *, message: str, error_type: str) -> dict[str, Any]:
|
|
31
|
+
return openai_error_payload(message=message, error_type=error_type)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Anthropic SSE parsing used by the Responses stream adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import AsyncIterable, AsyncIterator
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(slots=True)
|
|
12
|
+
class AnthropicSseEvent:
|
|
13
|
+
event: str
|
|
14
|
+
data: dict[str, Any]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def iter_sse_events(
|
|
18
|
+
chunks: AsyncIterable[Any],
|
|
19
|
+
) -> AsyncIterator[AnthropicSseEvent]:
|
|
20
|
+
buffer = ""
|
|
21
|
+
async for chunk in chunks:
|
|
22
|
+
if isinstance(chunk, bytes):
|
|
23
|
+
buffer += chunk.decode("utf-8", errors="replace")
|
|
24
|
+
else:
|
|
25
|
+
buffer += str(chunk)
|
|
26
|
+
|
|
27
|
+
while "\n\n" in buffer:
|
|
28
|
+
raw, buffer = buffer.split("\n\n", 1)
|
|
29
|
+
event = parse_sse_event(raw)
|
|
30
|
+
if event is not None:
|
|
31
|
+
yield event
|
|
32
|
+
|
|
33
|
+
if buffer.strip():
|
|
34
|
+
event = parse_sse_event(buffer)
|
|
35
|
+
if event is not None:
|
|
36
|
+
yield event
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def parse_sse_event(raw: str) -> AnthropicSseEvent | None:
|
|
40
|
+
event_type = ""
|
|
41
|
+
data_parts: list[str] = []
|
|
42
|
+
for line in raw.splitlines():
|
|
43
|
+
stripped = line.rstrip("\r")
|
|
44
|
+
if stripped.startswith("event:"):
|
|
45
|
+
event_type = stripped.split(":", 1)[1].strip()
|
|
46
|
+
elif stripped.startswith("data:"):
|
|
47
|
+
data_parts.append(stripped.split(":", 1)[1].strip())
|
|
48
|
+
if not event_type and not data_parts:
|
|
49
|
+
return None
|
|
50
|
+
data_text = "\n".join(data_parts)
|
|
51
|
+
if data_text == "[DONE]":
|
|
52
|
+
return None
|
|
53
|
+
try:
|
|
54
|
+
parsed = json.loads(data_text) if data_text else {}
|
|
55
|
+
except json.JSONDecodeError:
|
|
56
|
+
parsed = {"raw": data_text}
|
|
57
|
+
if not isinstance(parsed, dict):
|
|
58
|
+
parsed = {"value": parsed}
|
|
59
|
+
return AnthropicSseEvent(event=event_type, data=parsed)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Errors and error envelopes for OpenAI Responses compatibility."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResponsesConversionError(ValueError):
|
|
9
|
+
"""Raised when a Responses request cannot be converted deterministically."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def openai_error_payload(*, message: str, error_type: str) -> dict[str, Any]:
|
|
13
|
+
"""Return an OpenAI-compatible error envelope."""
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
"error": {
|
|
17
|
+
"message": message,
|
|
18
|
+
"type": error_type,
|
|
19
|
+
"param": None,
|
|
20
|
+
"code": None,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""OpenAI Responses SSE event formatting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
OPENAI_RESPONSES_SSE_HEADERS: dict[str, str] = {
|
|
10
|
+
"X-Accel-Buffering": "no",
|
|
11
|
+
"Cache-Control": "no-cache",
|
|
12
|
+
"Connection": "keep-alive",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def format_response_sse_event(event_type: str, data: Mapping[str, Any]) -> str:
|
|
17
|
+
"""Format one OpenAI Responses SSE event."""
|
|
18
|
+
|
|
19
|
+
return f"event: {event_type}\ndata: {json.dumps(data)}\n\n"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Identifier helpers for OpenAI Responses payloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def new_response_id() -> str:
|
|
9
|
+
return f"resp_{uuid.uuid4().hex}"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def new_message_item_id() -> str:
|
|
13
|
+
return f"msg_{uuid.uuid4().hex}"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def new_reasoning_item_id() -> str:
|
|
17
|
+
return f"rs_{uuid.uuid4().hex}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def new_call_id() -> str:
|
|
21
|
+
return f"call_{uuid.uuid4().hex[:24]}"
|