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,309 @@
|
|
|
1
|
+
"""Provider-specific exception mapping and user-visible diagnostics."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import openai
|
|
9
|
+
|
|
10
|
+
from config.constants import PROVIDER_ERROR_BODY_DISPLAY_CAP_BYTES
|
|
11
|
+
from core.anthropic import get_user_facing_error_message
|
|
12
|
+
from providers.exceptions import (
|
|
13
|
+
APIError,
|
|
14
|
+
AuthenticationError,
|
|
15
|
+
InvalidRequestError,
|
|
16
|
+
OverloadedError,
|
|
17
|
+
RateLimitError,
|
|
18
|
+
)
|
|
19
|
+
from providers.rate_limit import GlobalRateLimiter
|
|
20
|
+
|
|
21
|
+
_BODY_ATTR = "_fcc_provider_error_body"
|
|
22
|
+
_BODY_TRUNCATED_ATTR = "_fcc_provider_error_body_truncated"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class ProviderErrorDetail:
|
|
27
|
+
"""Structured upstream error detail surfaced directly to users."""
|
|
28
|
+
|
|
29
|
+
status_code: int | None = None
|
|
30
|
+
body_text: str | None = None
|
|
31
|
+
exception_text: str | None = None
|
|
32
|
+
error_type_hint: str | None = None
|
|
33
|
+
body_truncated: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def attach_provider_error_body(
|
|
37
|
+
exc: Exception, body: bytes | str, *, truncated: bool = False
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Attach a streamed HTTP error body to an exception for later formatting."""
|
|
40
|
+
setattr(exc, _BODY_ATTR, body)
|
|
41
|
+
setattr(exc, _BODY_TRUNCATED_ATTR, truncated)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _status_code_from_exception(exc: Exception) -> int | None:
|
|
45
|
+
status = getattr(exc, "status_code", None)
|
|
46
|
+
if isinstance(status, int):
|
|
47
|
+
return status
|
|
48
|
+
response = getattr(exc, "response", None)
|
|
49
|
+
response_status = getattr(response, "status_code", None)
|
|
50
|
+
if isinstance(response_status, int):
|
|
51
|
+
return response_status
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _body_from_response(exc: Exception) -> Any:
|
|
56
|
+
response = getattr(exc, "response", None)
|
|
57
|
+
if response is None:
|
|
58
|
+
return None
|
|
59
|
+
try:
|
|
60
|
+
return response.json()
|
|
61
|
+
except ValueError, RuntimeError:
|
|
62
|
+
pass
|
|
63
|
+
try:
|
|
64
|
+
return response.text
|
|
65
|
+
except httpx.ResponseNotRead, RuntimeError:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _normalize_body_text(body: Any) -> str | None:
|
|
70
|
+
if body is None:
|
|
71
|
+
return None
|
|
72
|
+
if isinstance(body, bytes):
|
|
73
|
+
text = body.decode("utf-8", errors="replace")
|
|
74
|
+
elif isinstance(body, str):
|
|
75
|
+
text = body
|
|
76
|
+
else:
|
|
77
|
+
try:
|
|
78
|
+
return json.dumps(body, ensure_ascii=False, separators=(",", ":"))
|
|
79
|
+
except TypeError:
|
|
80
|
+
text = str(body)
|
|
81
|
+
stripped = text.strip()
|
|
82
|
+
if not stripped:
|
|
83
|
+
return None
|
|
84
|
+
try:
|
|
85
|
+
parsed = json.loads(stripped)
|
|
86
|
+
except ValueError:
|
|
87
|
+
return stripped
|
|
88
|
+
return json.dumps(parsed, ensure_ascii=False, separators=(",", ":"))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _cap_text_bytes(text: str, max_bytes: int) -> tuple[str, bool]:
|
|
92
|
+
encoded = text.encode("utf-8", errors="replace")
|
|
93
|
+
if len(encoded) <= max_bytes:
|
|
94
|
+
return text, False
|
|
95
|
+
capped = encoded[:max_bytes].decode("utf-8", errors="replace")
|
|
96
|
+
return f"{capped}\n... [truncated after {max_bytes} bytes]", True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _error_type_hint_from_body(body: Any, body_text: str | None) -> str | None:
|
|
100
|
+
parsed = body
|
|
101
|
+
if isinstance(parsed, bytes):
|
|
102
|
+
text = parsed.decode("utf-8", errors="replace")
|
|
103
|
+
try:
|
|
104
|
+
parsed = json.loads(text)
|
|
105
|
+
except ValueError:
|
|
106
|
+
parsed = None
|
|
107
|
+
elif isinstance(parsed, str):
|
|
108
|
+
try:
|
|
109
|
+
parsed = json.loads(parsed)
|
|
110
|
+
except ValueError:
|
|
111
|
+
parsed = None
|
|
112
|
+
if isinstance(parsed, dict):
|
|
113
|
+
error = parsed.get("error")
|
|
114
|
+
if isinstance(error, dict):
|
|
115
|
+
for key in ("type", "code"):
|
|
116
|
+
value = error.get(key)
|
|
117
|
+
if isinstance(value, str) and value.strip():
|
|
118
|
+
return value.strip()
|
|
119
|
+
for key in ("type", "code"):
|
|
120
|
+
value = parsed.get(key)
|
|
121
|
+
if isinstance(value, str) and value.strip():
|
|
122
|
+
return value.strip()
|
|
123
|
+
if (
|
|
124
|
+
body_text
|
|
125
|
+
and "model" in body_text.lower()
|
|
126
|
+
and "unsupported" in body_text.lower()
|
|
127
|
+
):
|
|
128
|
+
return "upstream_model_error"
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def extract_provider_error_detail(exc: Exception) -> ProviderErrorDetail:
|
|
133
|
+
"""Extract copyable upstream status/body/exception detail from provider errors."""
|
|
134
|
+
raw_body = getattr(exc, _BODY_ATTR, None)
|
|
135
|
+
raw_body_truncated = bool(getattr(exc, _BODY_TRUNCATED_ATTR, False))
|
|
136
|
+
if raw_body is None:
|
|
137
|
+
raw_body = getattr(exc, "body", None)
|
|
138
|
+
if raw_body is None:
|
|
139
|
+
raw_body = _body_from_response(exc)
|
|
140
|
+
|
|
141
|
+
body_text = _normalize_body_text(raw_body)
|
|
142
|
+
display_truncated = raw_body_truncated
|
|
143
|
+
if body_text is not None:
|
|
144
|
+
body_text, cap_truncated = _cap_text_bytes(
|
|
145
|
+
body_text, PROVIDER_ERROR_BODY_DISPLAY_CAP_BYTES
|
|
146
|
+
)
|
|
147
|
+
display_truncated = display_truncated or cap_truncated
|
|
148
|
+
|
|
149
|
+
exception_text = str(exc).strip() or None
|
|
150
|
+
if exception_text is not None:
|
|
151
|
+
exception_text, _ = _cap_text_bytes(
|
|
152
|
+
exception_text, PROVIDER_ERROR_BODY_DISPLAY_CAP_BYTES
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return ProviderErrorDetail(
|
|
156
|
+
status_code=_status_code_from_exception(exc),
|
|
157
|
+
body_text=body_text,
|
|
158
|
+
exception_text=exception_text,
|
|
159
|
+
error_type_hint=_error_type_hint_from_body(raw_body, body_text),
|
|
160
|
+
body_truncated=display_truncated,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _provider_error_category(mapped: Exception) -> str | None:
|
|
165
|
+
error_type = getattr(mapped, "error_type", None)
|
|
166
|
+
if isinstance(error_type, str) and error_type.strip():
|
|
167
|
+
return error_type.strip()
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _append_request_id_lines(lines: list[str], request_id: str | None) -> None:
|
|
172
|
+
if request_id:
|
|
173
|
+
lines.extend(("", f"Request ID: {request_id}"))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def format_provider_error_message(
|
|
177
|
+
mapped: Exception,
|
|
178
|
+
detail: ProviderErrorDetail,
|
|
179
|
+
*,
|
|
180
|
+
provider_name: str,
|
|
181
|
+
read_timeout_s: float | None,
|
|
182
|
+
request_id: str | None = None,
|
|
183
|
+
) -> str:
|
|
184
|
+
"""Return a copyable user-facing provider error including upstream detail."""
|
|
185
|
+
stable_message = get_user_facing_error_message(
|
|
186
|
+
mapped, read_timeout_s=read_timeout_s
|
|
187
|
+
)
|
|
188
|
+
has_upstream_detail = detail.status_code is not None or detail.body_text is not None
|
|
189
|
+
if not has_upstream_detail:
|
|
190
|
+
lines = [stable_message]
|
|
191
|
+
if detail.exception_text and detail.exception_text != stable_message:
|
|
192
|
+
lines.extend(("", "Provider exception:", detail.exception_text))
|
|
193
|
+
_append_request_id_lines(lines, request_id)
|
|
194
|
+
return "\n".join(lines)
|
|
195
|
+
|
|
196
|
+
if detail.status_code == 405:
|
|
197
|
+
lines = [
|
|
198
|
+
f"Upstream provider {provider_name} rejected the request method "
|
|
199
|
+
"or endpoint (HTTP 405)."
|
|
200
|
+
]
|
|
201
|
+
elif detail.status_code is not None:
|
|
202
|
+
lines = [
|
|
203
|
+
f"Upstream provider {provider_name} returned HTTP {detail.status_code}."
|
|
204
|
+
]
|
|
205
|
+
else:
|
|
206
|
+
lines = [f"Upstream provider {provider_name} returned an error."]
|
|
207
|
+
|
|
208
|
+
category = detail.error_type_hint or _provider_error_category(mapped)
|
|
209
|
+
if category:
|
|
210
|
+
lines.append(f"Category: {category}")
|
|
211
|
+
if stable_message and stable_message != lines[0]:
|
|
212
|
+
lines.append(f"Mapped message: {stable_message}")
|
|
213
|
+
|
|
214
|
+
lines.extend(("", "Upstream error:"))
|
|
215
|
+
if detail.body_text:
|
|
216
|
+
lines.append(detail.body_text)
|
|
217
|
+
else:
|
|
218
|
+
lines.append("(empty upstream error body)")
|
|
219
|
+
if detail.body_truncated and (
|
|
220
|
+
detail.body_text is None
|
|
221
|
+
or f"truncated after {PROVIDER_ERROR_BODY_DISPLAY_CAP_BYTES} bytes"
|
|
222
|
+
not in detail.body_text
|
|
223
|
+
):
|
|
224
|
+
lines.append(
|
|
225
|
+
f"... [truncated after {PROVIDER_ERROR_BODY_DISPLAY_CAP_BYTES} bytes]"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
_append_request_id_lines(lines, request_id)
|
|
229
|
+
return "\n".join(lines)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def user_visible_message_for_mapped_provider_error(
|
|
233
|
+
mapped: Exception,
|
|
234
|
+
*,
|
|
235
|
+
provider_name: str,
|
|
236
|
+
read_timeout_s: float | None,
|
|
237
|
+
detail: ProviderErrorDetail | None = None,
|
|
238
|
+
request_id: str | None = None,
|
|
239
|
+
) -> str:
|
|
240
|
+
"""Return the user-visible string after :func:`map_error` (405 + mapped types)."""
|
|
241
|
+
if detail is not None:
|
|
242
|
+
return format_provider_error_message(
|
|
243
|
+
mapped,
|
|
244
|
+
detail,
|
|
245
|
+
provider_name=provider_name,
|
|
246
|
+
read_timeout_s=read_timeout_s,
|
|
247
|
+
request_id=request_id,
|
|
248
|
+
)
|
|
249
|
+
if getattr(mapped, "status_code", None) == 405:
|
|
250
|
+
return (
|
|
251
|
+
f"Upstream provider {provider_name} rejected the request method "
|
|
252
|
+
"or endpoint (HTTP 405)."
|
|
253
|
+
)
|
|
254
|
+
return get_user_facing_error_message(mapped, read_timeout_s=read_timeout_s)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def map_error(
|
|
258
|
+
e: Exception, *, rate_limiter: GlobalRateLimiter | None = None
|
|
259
|
+
) -> Exception:
|
|
260
|
+
"""Map OpenAI or HTTPX exception to specific ProviderError.
|
|
261
|
+
|
|
262
|
+
Streaming transports should pass their scoped limiter (``self._global_rate_limiter``)
|
|
263
|
+
so reactive 429 handling applies to the correct provider. Tests may omit
|
|
264
|
+
``rate_limiter`` to use the process-wide singleton.
|
|
265
|
+
"""
|
|
266
|
+
message = get_user_facing_error_message(e)
|
|
267
|
+
limiter = rate_limiter or GlobalRateLimiter.get_instance()
|
|
268
|
+
|
|
269
|
+
if isinstance(e, openai.AuthenticationError):
|
|
270
|
+
return AuthenticationError(message, raw_error=str(e))
|
|
271
|
+
if isinstance(e, openai.RateLimitError):
|
|
272
|
+
limiter.set_blocked(60)
|
|
273
|
+
return RateLimitError(message, raw_error=str(e))
|
|
274
|
+
if isinstance(e, openai.BadRequestError):
|
|
275
|
+
return InvalidRequestError(message, raw_error=str(e))
|
|
276
|
+
if isinstance(e, openai.InternalServerError):
|
|
277
|
+
raw_message = str(e)
|
|
278
|
+
sdk_status = getattr(e, "status_code", None)
|
|
279
|
+
if "overloaded" in raw_message.lower() or "capacity" in raw_message.lower():
|
|
280
|
+
return OverloadedError(message, raw_error=raw_message)
|
|
281
|
+
if isinstance(sdk_status, int) and 500 <= sdk_status <= 599:
|
|
282
|
+
stable = APIError("_", status_code=sdk_status)
|
|
283
|
+
return APIError(
|
|
284
|
+
get_user_facing_error_message(stable),
|
|
285
|
+
status_code=sdk_status,
|
|
286
|
+
raw_error=str(e),
|
|
287
|
+
)
|
|
288
|
+
return APIError(message, status_code=500, raw_error=str(e))
|
|
289
|
+
if isinstance(e, openai.APIError):
|
|
290
|
+
return APIError(
|
|
291
|
+
message, status_code=getattr(e, "status_code", 500), raw_error=str(e)
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if isinstance(e, httpx.HTTPStatusError):
|
|
295
|
+
status = e.response.status_code
|
|
296
|
+
if status in (401, 403):
|
|
297
|
+
return AuthenticationError(message, raw_error=str(e))
|
|
298
|
+
if status == 429:
|
|
299
|
+
limiter.set_blocked(60)
|
|
300
|
+
return RateLimitError(message, raw_error=str(e))
|
|
301
|
+
if status == 400:
|
|
302
|
+
return InvalidRequestError(message, raw_error=str(e))
|
|
303
|
+
if status >= 500:
|
|
304
|
+
if status in (502, 503, 504):
|
|
305
|
+
return OverloadedError(message, raw_error=str(e))
|
|
306
|
+
return APIError(message, status_code=status, raw_error=str(e))
|
|
307
|
+
return APIError(message, status_code=status, raw_error=str(e))
|
|
308
|
+
|
|
309
|
+
return e
|
providers/exceptions.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Unified exception hierarchy for providers."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProviderError(Exception):
|
|
7
|
+
"""Base exception for all provider errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
status_code: int = 500,
|
|
13
|
+
error_type: str = "api_error",
|
|
14
|
+
raw_error: Any = None,
|
|
15
|
+
):
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
self.message = message
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
self.error_type = error_type
|
|
20
|
+
self.raw_error = raw_error
|
|
21
|
+
|
|
22
|
+
def to_anthropic_format(self) -> dict:
|
|
23
|
+
"""Convert to Anthropic-compatible error response."""
|
|
24
|
+
return {
|
|
25
|
+
"type": "error",
|
|
26
|
+
"error": {
|
|
27
|
+
"type": self.error_type,
|
|
28
|
+
"message": self.message,
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AuthenticationError(ProviderError):
|
|
34
|
+
"""Raised when API key is invalid or missing."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, message: str, raw_error: Any = None):
|
|
37
|
+
super().__init__(
|
|
38
|
+
message,
|
|
39
|
+
status_code=401,
|
|
40
|
+
error_type="authentication_error",
|
|
41
|
+
raw_error=raw_error,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class InvalidRequestError(ProviderError):
|
|
46
|
+
"""Raised when the request parameters are invalid."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, message: str, raw_error: Any = None):
|
|
49
|
+
super().__init__(
|
|
50
|
+
message,
|
|
51
|
+
status_code=400,
|
|
52
|
+
error_type="invalid_request_error",
|
|
53
|
+
raw_error=raw_error,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class RateLimitError(ProviderError):
|
|
58
|
+
"""Raised when rate limit is exceeded."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, message: str, raw_error: Any = None):
|
|
61
|
+
super().__init__(
|
|
62
|
+
message,
|
|
63
|
+
status_code=429,
|
|
64
|
+
error_type="rate_limit_error",
|
|
65
|
+
raw_error=raw_error,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class OverloadedError(ProviderError):
|
|
70
|
+
"""Raised when the provider is overloaded."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, message: str, raw_error: Any = None):
|
|
73
|
+
super().__init__(
|
|
74
|
+
message,
|
|
75
|
+
status_code=529,
|
|
76
|
+
error_type="overloaded_error",
|
|
77
|
+
raw_error=raw_error,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class APIError(ProviderError):
|
|
82
|
+
"""Raised when the provider returns a generic API error."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, message: str, status_code: int = 500, raw_error: Any = None):
|
|
85
|
+
super().__init__(
|
|
86
|
+
message,
|
|
87
|
+
status_code=status_code,
|
|
88
|
+
error_type="api_error",
|
|
89
|
+
raw_error=raw_error,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class UnknownProviderTypeError(InvalidRequestError):
|
|
94
|
+
"""Raised when ``provider_id`` is not registered in the provider map."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, message: str) -> None:
|
|
97
|
+
super().__init__(message)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class ServiceUnavailableError(ProviderError):
|
|
101
|
+
"""Raised when the server is not ready (e.g. app lifespan did not wire state)."""
|
|
102
|
+
|
|
103
|
+
def __init__(self, message: str, raw_error: Any = None):
|
|
104
|
+
super().__init__(
|
|
105
|
+
message,
|
|
106
|
+
status_code=503,
|
|
107
|
+
error_type="api_error",
|
|
108
|
+
raw_error=raw_error,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ModelListResponseError(ServiceUnavailableError):
|
|
113
|
+
"""Raised when a provider model-list response cannot be parsed safely."""
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Fireworks AI provider using native Anthropic-compatible Messages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from providers.base import ProviderConfig
|
|
8
|
+
from providers.transports.anthropic_messages import AnthropicMessagesTransport
|
|
9
|
+
|
|
10
|
+
from .request import build_request_body
|
|
11
|
+
|
|
12
|
+
FIREWORKS_BASE_URL = "https://api.fireworks.ai/inference/v1"
|
|
13
|
+
_ANTHROPIC_VERSION = "2023-06-01"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FireworksProvider(AnthropicMessagesTransport):
|
|
17
|
+
"""Fireworks AI using Anthropic-compatible Messages."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config: ProviderConfig):
|
|
20
|
+
super().__init__(
|
|
21
|
+
config,
|
|
22
|
+
provider_name="FIREWORKS",
|
|
23
|
+
default_base_url=FIREWORKS_BASE_URL,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def _build_request_body(
|
|
27
|
+
self, request: Any, thinking_enabled: bool | None = None
|
|
28
|
+
) -> dict:
|
|
29
|
+
if thinking_enabled is None:
|
|
30
|
+
thinking_enabled = self._is_thinking_enabled(request)
|
|
31
|
+
return build_request_body(
|
|
32
|
+
request,
|
|
33
|
+
thinking_enabled=thinking_enabled,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def _request_headers(self) -> dict[str, str]:
|
|
37
|
+
return {
|
|
38
|
+
"Accept": "text/event-stream",
|
|
39
|
+
"Authorization": f"Bearer {self._api_key}",
|
|
40
|
+
"Content-Type": "application/json",
|
|
41
|
+
"anthropic-version": _ANTHROPIC_VERSION,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
def _model_list_headers(self) -> dict[str, str]:
|
|
45
|
+
return {"Authorization": f"Bearer {self._api_key}"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Native Anthropic Messages request builder for Fireworks AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
from config.constants import ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS
|
|
10
|
+
from core.anthropic.native_messages_request import (
|
|
11
|
+
OpenRouterExtraBodyError,
|
|
12
|
+
build_base_native_anthropic_request_body,
|
|
13
|
+
validate_openrouter_extra_body,
|
|
14
|
+
)
|
|
15
|
+
from providers.exceptions import InvalidRequestError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_request_body(request_data: Any, *, thinking_enabled: bool) -> dict:
|
|
19
|
+
"""Build JSON for Fireworks Anthropic-compat ``POST …/messages``."""
|
|
20
|
+
logger.debug(
|
|
21
|
+
"FIREWORKS_REQUEST: native build model={} msgs={}",
|
|
22
|
+
getattr(request_data, "model", "?"),
|
|
23
|
+
len(getattr(request_data, "messages", [])),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
body = build_base_native_anthropic_request_body(
|
|
27
|
+
request_data,
|
|
28
|
+
default_max_tokens=ANTHROPIC_DEFAULT_MAX_OUTPUT_TOKENS,
|
|
29
|
+
thinking_enabled=thinking_enabled,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
extra = getattr(request_data, "extra_body", None)
|
|
33
|
+
if isinstance(extra, dict) and extra:
|
|
34
|
+
try:
|
|
35
|
+
validate_openrouter_extra_body(extra)
|
|
36
|
+
except OpenRouterExtraBodyError as exc:
|
|
37
|
+
raise InvalidRequestError(str(exc)) from exc
|
|
38
|
+
body.update(extra)
|
|
39
|
+
|
|
40
|
+
body["stream"] = True
|
|
41
|
+
|
|
42
|
+
logger.debug(
|
|
43
|
+
"FIREWORKS_REQUEST: build done model={} msgs={} tools={}",
|
|
44
|
+
body.get("model"),
|
|
45
|
+
len(body.get("messages", [])),
|
|
46
|
+
len(body.get("tools", [])),
|
|
47
|
+
)
|
|
48
|
+
return body
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Google AI Studio Gemini provider (OpenAI-compatible chat completions)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from providers.base import ProviderConfig
|
|
9
|
+
from providers.defaults import GEMINI_DEFAULT_BASE
|
|
10
|
+
from providers.transports.openai_chat import OpenAIChatTransport
|
|
11
|
+
|
|
12
|
+
from .request import build_request_body
|
|
13
|
+
|
|
14
|
+
_MAX_TOOL_CALL_EXTRA_CONTENT_CACHE = 4096
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GeminiProvider(OpenAIChatTransport):
|
|
18
|
+
"""Gemini API using ``https://generativelanguage.googleapis.com/v1beta/openai/``."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, config: ProviderConfig):
|
|
21
|
+
super().__init__(
|
|
22
|
+
config,
|
|
23
|
+
provider_name="GEMINI",
|
|
24
|
+
base_url=config.base_url or GEMINI_DEFAULT_BASE,
|
|
25
|
+
api_key=config.api_key,
|
|
26
|
+
)
|
|
27
|
+
self._tool_call_extra_content_by_id: dict[str, dict[str, Any]] = {}
|
|
28
|
+
|
|
29
|
+
def _record_tool_call_extra_content(
|
|
30
|
+
self, tool_call_id: str, extra_content: dict[str, Any]
|
|
31
|
+
) -> None:
|
|
32
|
+
if (
|
|
33
|
+
tool_call_id not in self._tool_call_extra_content_by_id
|
|
34
|
+
and len(self._tool_call_extra_content_by_id)
|
|
35
|
+
>= _MAX_TOOL_CALL_EXTRA_CONTENT_CACHE
|
|
36
|
+
):
|
|
37
|
+
self._tool_call_extra_content_by_id.pop(
|
|
38
|
+
next(iter(self._tool_call_extra_content_by_id))
|
|
39
|
+
)
|
|
40
|
+
self._tool_call_extra_content_by_id[tool_call_id] = deepcopy(extra_content)
|
|
41
|
+
|
|
42
|
+
def _build_request_body(
|
|
43
|
+
self, request: Any, thinking_enabled: bool | None = None
|
|
44
|
+
) -> dict:
|
|
45
|
+
return build_request_body(
|
|
46
|
+
request,
|
|
47
|
+
thinking_enabled=self._is_thinking_enabled(request, thinking_enabled),
|
|
48
|
+
tool_call_extra_content_by_id=self._tool_call_extra_content_by_id,
|
|
49
|
+
)
|