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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Native Anthropic Messages recovery event construction."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncIterator, Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from core.anthropic.emitted_sse_tracker import EmittedNativeSseTracker
|
|
11
|
+
from core.anthropic.stream_contracts import parse_sse_text
|
|
12
|
+
from core.anthropic.stream_recovery import (
|
|
13
|
+
MIDSTREAM_RECOVERY_ATTEMPTS,
|
|
14
|
+
accept_tool_json_repair,
|
|
15
|
+
continuation_suffix,
|
|
16
|
+
is_retryable_stream_error,
|
|
17
|
+
make_native_text_recovery_body,
|
|
18
|
+
make_native_tool_repair_body,
|
|
19
|
+
parse_complete_tool_input,
|
|
20
|
+
tool_schemas_by_name,
|
|
21
|
+
)
|
|
22
|
+
from core.trace import trace_event
|
|
23
|
+
|
|
24
|
+
from .http import maybe_await_aclose
|
|
25
|
+
|
|
26
|
+
IterStreamChunks = Callable[..., AsyncIterator[str]]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AnthropicMessagesRecovery:
|
|
30
|
+
"""Construct recovery events for interrupted native Anthropic streams."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
transport: Any,
|
|
35
|
+
*,
|
|
36
|
+
iter_stream_chunks: IterStreamChunks,
|
|
37
|
+
) -> None:
|
|
38
|
+
self._transport = transport
|
|
39
|
+
self._iter_stream_chunks = iter_stream_chunks
|
|
40
|
+
|
|
41
|
+
async def collect_text(
|
|
42
|
+
self,
|
|
43
|
+
body: dict[str, Any],
|
|
44
|
+
*,
|
|
45
|
+
req_tag: str,
|
|
46
|
+
thinking_enabled: bool,
|
|
47
|
+
) -> tuple[str, str]:
|
|
48
|
+
"""Collect text/thinking from an internal native recovery request."""
|
|
49
|
+
last_error: Exception | None = None
|
|
50
|
+
for attempt in range(MIDSTREAM_RECOVERY_ATTEMPTS):
|
|
51
|
+
response: httpx.Response | None = None
|
|
52
|
+
try:
|
|
53
|
+
response = (
|
|
54
|
+
await self._transport._global_rate_limiter.execute_with_retry(
|
|
55
|
+
self._transport._validated_stream_send, body, req_tag=req_tag
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
state = self._transport._new_stream_state(
|
|
59
|
+
None, thinking_enabled=thinking_enabled
|
|
60
|
+
)
|
|
61
|
+
chunks = [
|
|
62
|
+
chunk
|
|
63
|
+
async for chunk in self._iter_stream_chunks(
|
|
64
|
+
response,
|
|
65
|
+
state=state,
|
|
66
|
+
thinking_enabled=thinking_enabled,
|
|
67
|
+
)
|
|
68
|
+
]
|
|
69
|
+
text_parts: list[str] = []
|
|
70
|
+
thinking_parts: list[str] = []
|
|
71
|
+
for event in parse_sse_text("".join(chunks)):
|
|
72
|
+
delta = event.data.get("delta")
|
|
73
|
+
if not isinstance(delta, dict):
|
|
74
|
+
continue
|
|
75
|
+
text = delta.get("text")
|
|
76
|
+
if isinstance(text, str):
|
|
77
|
+
text_parts.append(text)
|
|
78
|
+
thinking = delta.get("thinking")
|
|
79
|
+
if isinstance(thinking, str):
|
|
80
|
+
thinking_parts.append(thinking)
|
|
81
|
+
return "".join(text_parts), "".join(thinking_parts)
|
|
82
|
+
except Exception as error:
|
|
83
|
+
last_error = error
|
|
84
|
+
if not is_retryable_stream_error(error):
|
|
85
|
+
raise
|
|
86
|
+
trace_event(
|
|
87
|
+
stage="provider",
|
|
88
|
+
event="provider.recovery.retry",
|
|
89
|
+
source="provider",
|
|
90
|
+
provider=self._transport._provider_name,
|
|
91
|
+
recovery_kind="native_text",
|
|
92
|
+
attempt=attempt + 1,
|
|
93
|
+
max_attempts=MIDSTREAM_RECOVERY_ATTEMPTS,
|
|
94
|
+
exc_type=type(error).__name__,
|
|
95
|
+
)
|
|
96
|
+
finally:
|
|
97
|
+
if response is not None and not response.is_closed:
|
|
98
|
+
await maybe_await_aclose(response)
|
|
99
|
+
if last_error is not None:
|
|
100
|
+
raise last_error
|
|
101
|
+
return "", ""
|
|
102
|
+
|
|
103
|
+
async def events(
|
|
104
|
+
self,
|
|
105
|
+
*,
|
|
106
|
+
body: dict[str, Any],
|
|
107
|
+
request: Any,
|
|
108
|
+
tracker: EmittedNativeSseTracker,
|
|
109
|
+
error: Exception,
|
|
110
|
+
request_id: str | None,
|
|
111
|
+
req_tag: str,
|
|
112
|
+
thinking_enabled: bool,
|
|
113
|
+
) -> list[str] | None:
|
|
114
|
+
"""Build recovery events, or return None when recovery is impossible."""
|
|
115
|
+
if not is_retryable_stream_error(error):
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
schemas = tool_schemas_by_name(request)
|
|
119
|
+
if tracker.has_tool_block():
|
|
120
|
+
repair_events: list[str] = []
|
|
121
|
+
for index, block in enumerate(tracker.tool_blocks()):
|
|
122
|
+
if (
|
|
123
|
+
block.tool_id
|
|
124
|
+
and block.name
|
|
125
|
+
and parse_complete_tool_input(block.content, block.name, schemas)
|
|
126
|
+
is not None
|
|
127
|
+
):
|
|
128
|
+
continue
|
|
129
|
+
schema = schemas.get(block.name)
|
|
130
|
+
recovery_body = make_native_tool_repair_body(
|
|
131
|
+
body,
|
|
132
|
+
tool_name=block.name,
|
|
133
|
+
prefix=block.content,
|
|
134
|
+
input_schema=schema.input_schema if schema is not None else None,
|
|
135
|
+
)
|
|
136
|
+
accepted_suffix: str | None = None
|
|
137
|
+
for attempt in range(MIDSTREAM_RECOVERY_ATTEMPTS):
|
|
138
|
+
text, _ = await self.collect_text(
|
|
139
|
+
recovery_body,
|
|
140
|
+
req_tag=req_tag,
|
|
141
|
+
thinking_enabled=thinking_enabled,
|
|
142
|
+
)
|
|
143
|
+
repair = accept_tool_json_repair(
|
|
144
|
+
block.content,
|
|
145
|
+
text,
|
|
146
|
+
tool_name=block.name,
|
|
147
|
+
schemas=schemas,
|
|
148
|
+
)
|
|
149
|
+
if repair is not None:
|
|
150
|
+
accepted_suffix = repair.suffix
|
|
151
|
+
trace_event(
|
|
152
|
+
stage="provider",
|
|
153
|
+
event="provider.recovery.tool_repaired",
|
|
154
|
+
source="provider",
|
|
155
|
+
provider=self._transport._provider_name,
|
|
156
|
+
tool_name=block.name,
|
|
157
|
+
attempt=attempt + 1,
|
|
158
|
+
)
|
|
159
|
+
break
|
|
160
|
+
if accepted_suffix is None:
|
|
161
|
+
return None
|
|
162
|
+
repair_events.extend(
|
|
163
|
+
tracker.append_tool_repair_suffix(index, accepted_suffix)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if not tracker.can_salvage_tool_use(schemas):
|
|
167
|
+
return None
|
|
168
|
+
events = list(repair_events)
|
|
169
|
+
events.extend(tracker.iter_success_tail("tool_use"))
|
|
170
|
+
trace_event(
|
|
171
|
+
stage="provider",
|
|
172
|
+
event="provider.recovery.tool_salvaged",
|
|
173
|
+
source="provider",
|
|
174
|
+
provider=self._transport._provider_name,
|
|
175
|
+
request_id=request_id,
|
|
176
|
+
)
|
|
177
|
+
return events
|
|
178
|
+
|
|
179
|
+
partial_text = tracker.emitted_text()
|
|
180
|
+
partial_thinking = tracker.emitted_thinking()
|
|
181
|
+
if not partial_text and not partial_thinking:
|
|
182
|
+
return None
|
|
183
|
+
recovery_body = make_native_text_recovery_body(body, partial_text)
|
|
184
|
+
text, thinking = await self.collect_text(
|
|
185
|
+
recovery_body,
|
|
186
|
+
req_tag=req_tag,
|
|
187
|
+
thinking_enabled=thinking_enabled,
|
|
188
|
+
)
|
|
189
|
+
text_suffix = continuation_suffix(partial_text, text)
|
|
190
|
+
thinking_suffix = continuation_suffix(partial_thinking, thinking)
|
|
191
|
+
events: list[str] = []
|
|
192
|
+
if thinking_suffix:
|
|
193
|
+
events.extend(tracker.append_thinking_suffix(thinking_suffix))
|
|
194
|
+
if text_suffix:
|
|
195
|
+
events.extend(tracker.append_text_suffix(text_suffix))
|
|
196
|
+
if not events:
|
|
197
|
+
return None
|
|
198
|
+
events.extend(tracker.iter_success_tail("end_turn"))
|
|
199
|
+
trace_event(
|
|
200
|
+
stage="provider",
|
|
201
|
+
event="provider.recovery.continued",
|
|
202
|
+
source="provider",
|
|
203
|
+
provider=self._transport._provider_name,
|
|
204
|
+
request_id=request_id,
|
|
205
|
+
)
|
|
206
|
+
return events
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Per-request native Anthropic Messages stream runner."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncIterator
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from core.anthropic.emitted_sse_tracker import EmittedNativeSseTracker
|
|
11
|
+
from core.anthropic.native_sse_block_policy import NativeSseBlockPolicyState
|
|
12
|
+
from core.anthropic.stream_recovery import (
|
|
13
|
+
TruncatedProviderStreamError,
|
|
14
|
+
tool_schemas_by_name,
|
|
15
|
+
)
|
|
16
|
+
from core.anthropic.stream_recovery_session import (
|
|
17
|
+
StreamFailureAction,
|
|
18
|
+
StreamRecoverySession,
|
|
19
|
+
)
|
|
20
|
+
from core.trace import provider_native_messages_body_snapshot, trace_event
|
|
21
|
+
|
|
22
|
+
from .http import maybe_await_aclose
|
|
23
|
+
from .recovery import AnthropicMessagesRecovery
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def iter_sse_lines(response: httpx.Response) -> AsyncIterator[str]:
|
|
27
|
+
"""Yield raw SSE line chunks preserving local provider behavior."""
|
|
28
|
+
async for line in response.aiter_lines():
|
|
29
|
+
if line:
|
|
30
|
+
yield f"{line}\n"
|
|
31
|
+
else:
|
|
32
|
+
yield "\n"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def iter_sse_events(response: httpx.Response) -> AsyncIterator[str]:
|
|
36
|
+
"""Group line-delimited SSE responses into full SSE events."""
|
|
37
|
+
event_lines: list[str] = []
|
|
38
|
+
async for line in response.aiter_lines():
|
|
39
|
+
if line:
|
|
40
|
+
event_lines.append(line)
|
|
41
|
+
continue
|
|
42
|
+
if event_lines:
|
|
43
|
+
yield "\n".join(event_lines) + "\n\n"
|
|
44
|
+
event_lines.clear()
|
|
45
|
+
if event_lines:
|
|
46
|
+
yield "\n".join(event_lines) + "\n\n"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AnthropicMessagesStreamRunner:
|
|
50
|
+
"""Own mutable state for one native Anthropic provider stream."""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
transport: Any,
|
|
55
|
+
*,
|
|
56
|
+
request: Any,
|
|
57
|
+
input_tokens: int,
|
|
58
|
+
request_id: str | None,
|
|
59
|
+
thinking_enabled: bool | None,
|
|
60
|
+
) -> None:
|
|
61
|
+
self._transport = transport
|
|
62
|
+
self._request = request
|
|
63
|
+
self._input_tokens = input_tokens
|
|
64
|
+
self._request_id = request_id
|
|
65
|
+
self._thinking_enabled = thinking_enabled
|
|
66
|
+
self._recovery = AnthropicMessagesRecovery(
|
|
67
|
+
transport,
|
|
68
|
+
iter_stream_chunks=self.iter_stream_chunks,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
async def run(self) -> AsyncIterator[str]:
|
|
72
|
+
"""Stream response via a native Anthropic-compatible messages endpoint."""
|
|
73
|
+
tag = self._transport._provider_name
|
|
74
|
+
req_tag = f" request_id={self._request_id}" if self._request_id else ""
|
|
75
|
+
body = self._transport._build_request_body(
|
|
76
|
+
self._request, thinking_enabled=self._thinking_enabled
|
|
77
|
+
)
|
|
78
|
+
thinking_enabled = self._transport._is_thinking_enabled(
|
|
79
|
+
self._request, self._thinking_enabled
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
trace_event(
|
|
83
|
+
stage="provider",
|
|
84
|
+
event="provider.request.sent",
|
|
85
|
+
source="provider",
|
|
86
|
+
provider=tag,
|
|
87
|
+
gateway_model=self._request.model,
|
|
88
|
+
downstream_model=body.get("model"),
|
|
89
|
+
message_count=len(body.get("messages", [])),
|
|
90
|
+
tool_count=len(body.get("tools", [])),
|
|
91
|
+
body=provider_native_messages_body_snapshot(body),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
response: httpx.Response | None = None
|
|
95
|
+
sent_any_event = False
|
|
96
|
+
state = self._transport._new_stream_state(
|
|
97
|
+
self._request, thinking_enabled=thinking_enabled
|
|
98
|
+
)
|
|
99
|
+
emitted_tracker = EmittedNativeSseTracker()
|
|
100
|
+
recovery_session = StreamRecoverySession(
|
|
101
|
+
provider_name=tag,
|
|
102
|
+
request_id=self._request_id,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async with self._transport._global_rate_limiter.concurrency_slot():
|
|
106
|
+
while True:
|
|
107
|
+
stream_opened = False
|
|
108
|
+
try:
|
|
109
|
+
response = (
|
|
110
|
+
await self._transport._global_rate_limiter.execute_with_retry(
|
|
111
|
+
self._transport._validated_stream_send,
|
|
112
|
+
body,
|
|
113
|
+
req_tag=req_tag,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
stream_opened = True
|
|
117
|
+
|
|
118
|
+
chunk_count = 0
|
|
119
|
+
chunk_bytes = 0
|
|
120
|
+
|
|
121
|
+
async for chunk in self.iter_stream_chunks(
|
|
122
|
+
response,
|
|
123
|
+
state=state,
|
|
124
|
+
thinking_enabled=thinking_enabled,
|
|
125
|
+
):
|
|
126
|
+
chunk_count += 1
|
|
127
|
+
chunk_bytes += len(chunk.encode("utf-8", errors="replace"))
|
|
128
|
+
emitted_tracker.feed(chunk)
|
|
129
|
+
for event in recovery_session.push(chunk):
|
|
130
|
+
sent_any_event = True
|
|
131
|
+
yield event
|
|
132
|
+
|
|
133
|
+
if not emitted_tracker.has_terminal_message():
|
|
134
|
+
raise TruncatedProviderStreamError(
|
|
135
|
+
"Provider stream ended without message_stop."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
trace_event(
|
|
139
|
+
stage="provider",
|
|
140
|
+
event="provider.response.completed",
|
|
141
|
+
source="provider",
|
|
142
|
+
provider=tag,
|
|
143
|
+
gateway_model=self._request.model,
|
|
144
|
+
sse_chunks_out=chunk_count,
|
|
145
|
+
sse_bytes_out=chunk_bytes,
|
|
146
|
+
)
|
|
147
|
+
for event in recovery_session.flush():
|
|
148
|
+
sent_any_event = True
|
|
149
|
+
yield event
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
except Exception as error:
|
|
153
|
+
generated_output = emitted_tracker.has_content_block()
|
|
154
|
+
complete_tool_salvageable = (
|
|
155
|
+
generated_output
|
|
156
|
+
and emitted_tracker.can_salvage_tool_use(
|
|
157
|
+
tool_schemas_by_name(self._request)
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
decision = recovery_session.advance_failure(
|
|
161
|
+
error,
|
|
162
|
+
stream_opened=stream_opened,
|
|
163
|
+
generated_output=generated_output,
|
|
164
|
+
complete_tool_salvageable=complete_tool_salvageable,
|
|
165
|
+
)
|
|
166
|
+
if decision.action == StreamFailureAction.EARLY_RETRY:
|
|
167
|
+
if response is not None and not response.is_closed:
|
|
168
|
+
await maybe_await_aclose(response)
|
|
169
|
+
response = None
|
|
170
|
+
state = self._transport._new_stream_state(
|
|
171
|
+
self._request, thinking_enabled=thinking_enabled
|
|
172
|
+
)
|
|
173
|
+
emitted_tracker = EmittedNativeSseTracker()
|
|
174
|
+
sent_any_event = False
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
if decision.action == StreamFailureAction.MIDSTREAM_RECOVERY:
|
|
178
|
+
try:
|
|
179
|
+
recovery_events = await self._recovery.events(
|
|
180
|
+
body=body,
|
|
181
|
+
request=self._request,
|
|
182
|
+
tracker=emitted_tracker,
|
|
183
|
+
error=error,
|
|
184
|
+
request_id=self._request_id,
|
|
185
|
+
req_tag=req_tag,
|
|
186
|
+
thinking_enabled=thinking_enabled,
|
|
187
|
+
)
|
|
188
|
+
except Exception as recovery_error:
|
|
189
|
+
trace_event(
|
|
190
|
+
stage="provider",
|
|
191
|
+
event="provider.recovery.failed",
|
|
192
|
+
source="provider",
|
|
193
|
+
provider=tag,
|
|
194
|
+
request_id=self._request_id,
|
|
195
|
+
exc_type=type(recovery_error).__name__,
|
|
196
|
+
)
|
|
197
|
+
recovery_events = None
|
|
198
|
+
if recovery_events is not None:
|
|
199
|
+
for event in recovery_session.flush_uncommitted(decision):
|
|
200
|
+
sent_any_event = True
|
|
201
|
+
yield event
|
|
202
|
+
for event in recovery_events:
|
|
203
|
+
yield event
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
if not isinstance(error, httpx.HTTPStatusError):
|
|
207
|
+
self._transport._log_stream_transport_error(
|
|
208
|
+
tag, req_tag, error, request_id=self._request_id
|
|
209
|
+
)
|
|
210
|
+
error_message = self._transport._get_error_message(
|
|
211
|
+
error, self._request_id
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if response is not None and not response.is_closed:
|
|
215
|
+
await maybe_await_aclose(response)
|
|
216
|
+
|
|
217
|
+
trace_event(
|
|
218
|
+
stage="provider",
|
|
219
|
+
event="provider.response.error",
|
|
220
|
+
source="provider",
|
|
221
|
+
provider=tag,
|
|
222
|
+
error_message=error_message,
|
|
223
|
+
exc_type=type(error).__name__,
|
|
224
|
+
mid_stream=(
|
|
225
|
+
sent_any_event
|
|
226
|
+
or decision.committed
|
|
227
|
+
or decision.has_buffered
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
if decision.committed or decision.has_buffered:
|
|
231
|
+
if not decision.committed:
|
|
232
|
+
for event in recovery_session.flush():
|
|
233
|
+
sent_any_event = True
|
|
234
|
+
yield event
|
|
235
|
+
for event in emitted_tracker.iter_close_unclosed_blocks():
|
|
236
|
+
yield event
|
|
237
|
+
for event in emitted_tracker.iter_midstream_error_tail(
|
|
238
|
+
error_message,
|
|
239
|
+
request=self._request,
|
|
240
|
+
input_tokens=self._input_tokens,
|
|
241
|
+
log_raw_sse_events=(
|
|
242
|
+
self._transport._config.log_raw_sse_events
|
|
243
|
+
),
|
|
244
|
+
):
|
|
245
|
+
yield event
|
|
246
|
+
else:
|
|
247
|
+
recovery_session.discard()
|
|
248
|
+
for event in self._transport._emit_error_events(
|
|
249
|
+
request=self._request,
|
|
250
|
+
input_tokens=self._input_tokens,
|
|
251
|
+
error_message=error_message,
|
|
252
|
+
sent_any_event=False,
|
|
253
|
+
):
|
|
254
|
+
yield event
|
|
255
|
+
return
|
|
256
|
+
finally:
|
|
257
|
+
if response is not None and not response.is_closed:
|
|
258
|
+
await maybe_await_aclose(response)
|
|
259
|
+
|
|
260
|
+
async def iter_stream_chunks(
|
|
261
|
+
self,
|
|
262
|
+
response: httpx.Response,
|
|
263
|
+
*,
|
|
264
|
+
state: Any,
|
|
265
|
+
thinking_enabled: bool,
|
|
266
|
+
) -> AsyncIterator[str]:
|
|
267
|
+
"""Yield chunks according to the provider's observable stream shape."""
|
|
268
|
+
if self._transport.stream_chunk_mode == "line" and isinstance(
|
|
269
|
+
state, NativeSseBlockPolicyState
|
|
270
|
+
):
|
|
271
|
+
async for event in iter_sse_events(response):
|
|
272
|
+
output_event = self._transport._transform_stream_event(
|
|
273
|
+
event,
|
|
274
|
+
state,
|
|
275
|
+
thinking_enabled=thinking_enabled,
|
|
276
|
+
)
|
|
277
|
+
if output_event is None:
|
|
278
|
+
continue
|
|
279
|
+
for line in output_event.splitlines(keepends=True):
|
|
280
|
+
yield line
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
if self._transport.stream_chunk_mode == "line":
|
|
284
|
+
async for chunk in iter_sse_lines(response):
|
|
285
|
+
yield chunk
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
async for event in iter_sse_events(response):
|
|
289
|
+
output_event = self._transport._transform_stream_event(
|
|
290
|
+
event,
|
|
291
|
+
state,
|
|
292
|
+
thinking_enabled=thinking_enabled,
|
|
293
|
+
)
|
|
294
|
+
if output_event is not None:
|
|
295
|
+
yield output_event
|