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.
Files changed (189) hide show
  1. api/__init__.py +17 -0
  2. api/admin_config.py +1303 -0
  3. api/admin_routes.py +287 -0
  4. api/admin_static/admin.css +459 -0
  5. api/admin_static/admin.js +497 -0
  6. api/admin_static/index.html +77 -0
  7. api/admin_urls.py +34 -0
  8. api/app.py +194 -0
  9. api/command_utils.py +164 -0
  10. api/dependencies.py +144 -0
  11. api/detection.py +152 -0
  12. api/gateway_model_ids.py +54 -0
  13. api/model_catalog.py +133 -0
  14. api/model_router.py +125 -0
  15. api/models/__init__.py +45 -0
  16. api/models/anthropic.py +234 -0
  17. api/models/openai_responses.py +28 -0
  18. api/models/responses.py +60 -0
  19. api/optimization_handlers.py +154 -0
  20. api/request_pipeline.py +424 -0
  21. api/routes.py +156 -0
  22. api/runtime.py +334 -0
  23. api/validation_log.py +48 -0
  24. api/web_server_tools.py +22 -0
  25. api/web_tools/__init__.py +17 -0
  26. api/web_tools/constants.py +15 -0
  27. api/web_tools/egress.py +99 -0
  28. api/web_tools/outbound.py +278 -0
  29. api/web_tools/parsers.py +104 -0
  30. api/web_tools/request.py +87 -0
  31. api/web_tools/streaming.py +206 -0
  32. cli/__init__.py +5 -0
  33. cli/claude_env.py +12 -0
  34. cli/entrypoints.py +166 -0
  35. cli/env.example +209 -0
  36. cli/launchers/__init__.py +1 -0
  37. cli/launchers/claude.py +84 -0
  38. cli/launchers/codex.py +204 -0
  39. cli/launchers/codex_model_catalog.py +186 -0
  40. cli/launchers/common.py +93 -0
  41. cli/managed/__init__.py +6 -0
  42. cli/managed/claude.py +215 -0
  43. cli/managed/manager.py +157 -0
  44. cli/managed/session.py +260 -0
  45. cli/process_registry.py +78 -0
  46. config/__init__.py +5 -0
  47. config/constants.py +13 -0
  48. config/logging_config.py +159 -0
  49. config/nim.py +118 -0
  50. config/paths.py +91 -0
  51. config/provider_catalog.py +259 -0
  52. config/provider_ids.py +7 -0
  53. config/settings.py +538 -0
  54. core/__init__.py +1 -0
  55. core/anthropic/__init__.py +46 -0
  56. core/anthropic/content.py +31 -0
  57. core/anthropic/conversion.py +587 -0
  58. core/anthropic/emitted_sse_tracker.py +346 -0
  59. core/anthropic/errors.py +70 -0
  60. core/anthropic/native_messages_request.py +280 -0
  61. core/anthropic/native_sse_block_policy.py +313 -0
  62. core/anthropic/provider_stream_error.py +34 -0
  63. core/anthropic/server_tool_sse.py +14 -0
  64. core/anthropic/sse.py +440 -0
  65. core/anthropic/stream_contracts.py +205 -0
  66. core/anthropic/stream_recovery.py +346 -0
  67. core/anthropic/stream_recovery_session.py +133 -0
  68. core/anthropic/thinking.py +140 -0
  69. core/anthropic/tokens.py +117 -0
  70. core/anthropic/tools.py +212 -0
  71. core/anthropic/utils.py +9 -0
  72. core/openai_responses/__init__.py +5 -0
  73. core/openai_responses/adapter.py +31 -0
  74. core/openai_responses/anthropic_sse.py +59 -0
  75. core/openai_responses/errors.py +22 -0
  76. core/openai_responses/events.py +19 -0
  77. core/openai_responses/ids.py +21 -0
  78. core/openai_responses/input.py +258 -0
  79. core/openai_responses/items.py +37 -0
  80. core/openai_responses/reasoning.py +52 -0
  81. core/openai_responses/stream.py +25 -0
  82. core/openai_responses/stream_state.py +654 -0
  83. core/openai_responses/tools.py +374 -0
  84. core/openai_responses/usage.py +37 -0
  85. core/rate_limit.py +60 -0
  86. core/trace.py +216 -0
  87. devcopilot-0.2.0.dist-info/METADATA +687 -0
  88. devcopilot-0.2.0.dist-info/RECORD +189 -0
  89. devcopilot-0.2.0.dist-info/WHEEL +4 -0
  90. devcopilot-0.2.0.dist-info/entry_points.txt +6 -0
  91. devcopilot-0.2.0.dist-info/licenses/LICENSE +21 -0
  92. messaging/__init__.py +26 -0
  93. messaging/cli_event_constants.py +67 -0
  94. messaging/command_context.py +66 -0
  95. messaging/command_dispatcher.py +37 -0
  96. messaging/commands.py +275 -0
  97. messaging/event_parser.py +181 -0
  98. messaging/limiter.py +300 -0
  99. messaging/models.py +36 -0
  100. messaging/node_event_pipeline.py +127 -0
  101. messaging/node_runner.py +342 -0
  102. messaging/platforms/__init__.py +15 -0
  103. messaging/platforms/base.py +228 -0
  104. messaging/platforms/discord.py +567 -0
  105. messaging/platforms/factory.py +103 -0
  106. messaging/platforms/outbox.py +144 -0
  107. messaging/platforms/telegram.py +688 -0
  108. messaging/platforms/voice_flow.py +295 -0
  109. messaging/rendering/__init__.py +3 -0
  110. messaging/rendering/discord_markdown.py +318 -0
  111. messaging/rendering/markdown_tables.py +49 -0
  112. messaging/rendering/profiles.py +55 -0
  113. messaging/rendering/telegram_markdown.py +327 -0
  114. messaging/safe_diagnostics.py +17 -0
  115. messaging/session.py +334 -0
  116. messaging/transcript.py +581 -0
  117. messaging/transcription.py +164 -0
  118. messaging/trees/__init__.py +15 -0
  119. messaging/trees/data.py +482 -0
  120. messaging/trees/manager.py +433 -0
  121. messaging/trees/processor.py +179 -0
  122. messaging/trees/repository.py +177 -0
  123. messaging/turn_intake.py +235 -0
  124. messaging/ui_updates.py +101 -0
  125. messaging/voice.py +76 -0
  126. messaging/workflow.py +200 -0
  127. providers/__init__.py +31 -0
  128. providers/base.py +152 -0
  129. providers/cerebras/__init__.py +7 -0
  130. providers/cerebras/client.py +31 -0
  131. providers/cerebras/request.py +55 -0
  132. providers/codestral/__init__.py +7 -0
  133. providers/codestral/client.py +34 -0
  134. providers/deepseek/__init__.py +11 -0
  135. providers/deepseek/client.py +51 -0
  136. providers/deepseek/request.py +475 -0
  137. providers/defaults.py +41 -0
  138. providers/error_mapping.py +309 -0
  139. providers/exceptions.py +113 -0
  140. providers/fireworks/__init__.py +5 -0
  141. providers/fireworks/client.py +45 -0
  142. providers/fireworks/request.py +48 -0
  143. providers/gemini/__init__.py +7 -0
  144. providers/gemini/client.py +49 -0
  145. providers/gemini/request.py +199 -0
  146. providers/groq/__init__.py +7 -0
  147. providers/groq/client.py +31 -0
  148. providers/groq/request.py +83 -0
  149. providers/kimi/__init__.py +10 -0
  150. providers/kimi/client.py +53 -0
  151. providers/kimi/request.py +42 -0
  152. providers/llamacpp/__init__.py +3 -0
  153. providers/llamacpp/client.py +16 -0
  154. providers/lmstudio/__init__.py +5 -0
  155. providers/lmstudio/client.py +16 -0
  156. providers/mistral/__init__.py +7 -0
  157. providers/mistral/client.py +31 -0
  158. providers/mistral/request.py +37 -0
  159. providers/model_listing.py +133 -0
  160. providers/nvidia_nim/__init__.py +7 -0
  161. providers/nvidia_nim/client.py +91 -0
  162. providers/nvidia_nim/request.py +430 -0
  163. providers/nvidia_nim/voice.py +95 -0
  164. providers/ollama/__init__.py +7 -0
  165. providers/ollama/client.py +39 -0
  166. providers/open_router/__init__.py +7 -0
  167. providers/open_router/client.py +124 -0
  168. providers/open_router/request.py +42 -0
  169. providers/opencode/__init__.py +11 -0
  170. providers/opencode/client.py +31 -0
  171. providers/opencode/request.py +35 -0
  172. providers/rate_limit.py +300 -0
  173. providers/registry.py +527 -0
  174. providers/transports/__init__.py +1 -0
  175. providers/transports/anthropic_messages/__init__.py +5 -0
  176. providers/transports/anthropic_messages/http.py +118 -0
  177. providers/transports/anthropic_messages/recovery.py +206 -0
  178. providers/transports/anthropic_messages/stream.py +295 -0
  179. providers/transports/anthropic_messages/transport.py +236 -0
  180. providers/transports/openai_chat/__init__.py +5 -0
  181. providers/transports/openai_chat/recovery.py +217 -0
  182. providers/transports/openai_chat/stream.py +384 -0
  183. providers/transports/openai_chat/tool_calls.py +293 -0
  184. providers/transports/openai_chat/transport.py +156 -0
  185. providers/wafer/__init__.py +10 -0
  186. providers/wafer/client.py +50 -0
  187. providers/zai/__init__.py +10 -0
  188. providers/zai/client.py +46 -0
  189. 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
@@ -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,5 @@
1
+ """Fireworks AI provider exports."""
2
+
3
+ from .client import FIREWORKS_BASE_URL, FireworksProvider
4
+
5
+ __all__ = ["FIREWORKS_BASE_URL", "FireworksProvider"]
@@ -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,7 @@
1
+ """Google AI Studio Gemini (OpenAI-compat) adapter."""
2
+
3
+ from providers.defaults import GEMINI_DEFAULT_BASE
4
+
5
+ from .client import GeminiProvider
6
+
7
+ __all__ = ["GEMINI_DEFAULT_BASE", "GeminiProvider"]
@@ -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
+ )