aidial-sdk 0.48.0.dev3__tar.gz → 0.48.0.dev4__tar.gz
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.
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/PKG-INFO +1 -1
- aidial_sdk-0.48.0.dev4/aidial_sdk/_errors.py +81 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/application.py +24 -26
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/exceptions.py +58 -16
- aidial_sdk-0.48.0.dev4/aidial_sdk/translators/messages_to_chat_completions/backend/http.py +161 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/app.py +14 -19
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/reasoning.py +3 -7
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/streaming.py +2 -4
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/to_chat_completions.py +7 -16
- aidial_sdk-0.48.0.dev4/aidial_sdk/translators/messages_to_chat_completions/settings.py +18 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/translators/messages_to_chat_completions/sse_stream.py +17 -49
- aidial_sdk-0.48.0.dev4/aidial_sdk/utils/_logging_middleware.py +73 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/streaming.py +0 -2
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/pyproject.toml +1 -1
- aidial_sdk-0.48.0.dev3/aidial_sdk/_errors.py +0 -32
- aidial_sdk-0.48.0.dev3/aidial_sdk/translators/messages_to_chat_completions/backend/http.py +0 -92
- aidial_sdk-0.48.0.dev3/aidial_sdk/translators/messages_to_chat_completions/common.py +0 -61
- aidial_sdk-0.48.0.dev3/aidial_sdk/translators/messages_to_chat_completions/errors.py +0 -153
- aidial_sdk-0.48.0.dev3/aidial_sdk/translators/messages_to_chat_completions/settings.py +0 -21
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/LICENSE +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/README.md +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/_pydantic/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/_pydantic/_compat.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/_pydantic/_model_config.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/chat_completion/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/chat_completion/_cache.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/chat_completion/_types.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/chat_completion/base.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/chat_completion/choice.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/chat_completion/choice_base.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/chat_completion/chunks.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/chat_completion/enums.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/chat_completion/form.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/chat_completion/function_call.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/chat_completion/function_tool_call.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/chat_completion/request.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/chat_completion/response.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/chat_completion/stage.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/deployment/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/deployment/_headers.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/deployment/configuration.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/deployment/from_request_mixin.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/deployment/rate.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/deployment/tokenize.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/deployment/truncate_prompt.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/embeddings/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/embeddings/base.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/embeddings/request.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/embeddings/response.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/header_propagator.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/py.typed +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/pydantic/v2.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/pydantic_v1/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/response_headers.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/telemetry/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/telemetry/_context.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/telemetry/init.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/telemetry/types.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/translators/messages_to_chat_completions/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/cache_breakpoints.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/dial_extensions.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/from_chat_completions.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/system_turns.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/translators/messages_to_chat_completions/translation_state.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/_attachment.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/_cancel_scope.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/_content_stream.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/_disconnect_middleware.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/_indexed_list.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/_interpolation.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/_json_log_formatter.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/_logging.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/_reflection.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/env.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/errors.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/json.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/log_config.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/logging.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/merge_chunks.py +0 -0
- {aidial_sdk-0.48.0.dev3 → aidial_sdk-0.48.0.dev4}/aidial_sdk/utils/pydantic.py +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from collections.abc import Awaitable, Callable
|
|
2
|
+
|
|
3
|
+
from fastapi import HTTPException, Request
|
|
4
|
+
from fastapi.responses import JSONResponse, Response
|
|
5
|
+
from fastapi.routing import APIRoute
|
|
6
|
+
|
|
7
|
+
from aidial_sdk._pydantic import ValidationError
|
|
8
|
+
from aidial_sdk.exceptions import (
|
|
9
|
+
ExceptionType,
|
|
10
|
+
InternalServerError,
|
|
11
|
+
InvalidRequestError,
|
|
12
|
+
)
|
|
13
|
+
from aidial_sdk.exceptions import HTTPException as DIALException
|
|
14
|
+
from aidial_sdk.utils.logging import logger as log
|
|
15
|
+
|
|
16
|
+
INTERNAL_ERROR_MESSAGE = "Internal server error"
|
|
17
|
+
|
|
18
|
+
Handler = Callable[[Request], Awaitable[Response]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_exception_type(request: Request) -> ExceptionType:
|
|
22
|
+
route = request.scope.get("route")
|
|
23
|
+
if isinstance(route, APIRoute) and route.path.startswith("/anthropic/"):
|
|
24
|
+
return "anthropic"
|
|
25
|
+
return "openai"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def invalid_request_error(e: ValidationError) -> InvalidRequestError:
|
|
29
|
+
error = e.errors()[0]
|
|
30
|
+
path = ".".join(map(str, error["loc"]))
|
|
31
|
+
return InvalidRequestError(
|
|
32
|
+
f"Your request contained invalid structure on path {path}. {error['msg']}"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def to_dial_exception(e: Exception) -> DIALException:
|
|
37
|
+
match e:
|
|
38
|
+
case DIALException():
|
|
39
|
+
return e
|
|
40
|
+
case _:
|
|
41
|
+
log.exception("Unexpected error in the request handler")
|
|
42
|
+
return InternalServerError(INTERNAL_ERROR_MESSAGE)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _error_response(
|
|
46
|
+
request: Request, e: Exception, api: ExceptionType | None
|
|
47
|
+
) -> Response:
|
|
48
|
+
if isinstance(e, HTTPException):
|
|
49
|
+
return JSONResponse(
|
|
50
|
+
status_code=e.status_code,
|
|
51
|
+
content=e.detail,
|
|
52
|
+
headers=e.headers,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
api = api or _get_exception_type(request)
|
|
56
|
+
return to_dial_exception(e).to_fastapi_response(api)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def error_handler(api: ExceptionType | None) -> Callable[[Handler], Handler]:
|
|
60
|
+
def _decorator(handler: Handler) -> Handler:
|
|
61
|
+
async def _handler(request: Request) -> Response:
|
|
62
|
+
try:
|
|
63
|
+
return await handler(request)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
return _error_response(request, e, api)
|
|
66
|
+
|
|
67
|
+
return _handler
|
|
68
|
+
|
|
69
|
+
return _decorator
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def dial_exception_handler(request: Request, exc: Exception) -> Response:
|
|
73
|
+
return _error_response(request, exc, None)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def unhandled_exception_handler(request: Request, exc: Exception) -> Response:
|
|
77
|
+
# ServerErrorMiddleware re-raises the exception for the server to log it
|
|
78
|
+
error = InternalServerError(
|
|
79
|
+
f"Unhandled SDK exception: {INTERNAL_ERROR_MESSAGE}"
|
|
80
|
+
)
|
|
81
|
+
return _error_response(request, error, None)
|
|
@@ -10,9 +10,11 @@ from fastapi.responses import JSONResponse, Response, StreamingResponse
|
|
|
10
10
|
from starlette.types import Receive, Scope, Send
|
|
11
11
|
|
|
12
12
|
from aidial_sdk._errors import (
|
|
13
|
+
Handler,
|
|
13
14
|
dial_exception_handler,
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
error_handler,
|
|
16
|
+
invalid_request_error,
|
|
17
|
+
unhandled_exception_handler,
|
|
16
18
|
)
|
|
17
19
|
from aidial_sdk._pydantic import ValidationError
|
|
18
20
|
from aidial_sdk._pydantic._compat import BaseModel
|
|
@@ -41,11 +43,11 @@ from aidial_sdk.utils._interpolation import (
|
|
|
41
43
|
interpolate_deployment_id,
|
|
42
44
|
match_deployment_id,
|
|
43
45
|
)
|
|
46
|
+
from aidial_sdk.utils._logging_middleware import LoggingMiddleware
|
|
44
47
|
from aidial_sdk.utils._reflection import get_method_implementation
|
|
45
48
|
from aidial_sdk.utils.env import env_float, env_var_list
|
|
46
49
|
from aidial_sdk.utils.log_config import configure_sdk_logger
|
|
47
50
|
from aidial_sdk.utils.logging import (
|
|
48
|
-
log_debug,
|
|
49
51
|
reset_log_context,
|
|
50
52
|
set_log_deployment,
|
|
51
53
|
)
|
|
@@ -60,8 +62,6 @@ configure_sdk_logger()
|
|
|
60
62
|
|
|
61
63
|
RequestType = TypeVar("RequestType", bound=FromRequestMixin)
|
|
62
64
|
|
|
63
|
-
Handler = Callable[[Request], Coroutine[Any, Any, Response]]
|
|
64
|
-
|
|
65
65
|
|
|
66
66
|
class PathFilter(logging.Filter):
|
|
67
67
|
path: str
|
|
@@ -145,14 +145,12 @@ class DIALApp(FastAPI):
|
|
|
145
145
|
|
|
146
146
|
self.add_middleware(DisconnectMiddleware)
|
|
147
147
|
self.add_middleware(NonForwardableHeadersMiddleware)
|
|
148
|
+
self.add_middleware(LoggingMiddleware)
|
|
148
149
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
self.add_exception_handler(HTTPException, fastapi_exception_handler)
|
|
154
|
-
|
|
150
|
+
# The routes DIALApp adds have error_handler, these are for custom ones
|
|
151
|
+
self.add_exception_handler(HTTPException, dial_exception_handler)
|
|
155
152
|
self.add_exception_handler(DIALException, dial_exception_handler)
|
|
153
|
+
self.add_exception_handler(Exception, unhandled_exception_handler)
|
|
156
154
|
|
|
157
155
|
def configure_telemetry(self, config: TelemetryConfig | None):
|
|
158
156
|
if config is None or config.is_noop():
|
|
@@ -184,7 +182,7 @@ class DIALApp(FastAPI):
|
|
|
184
182
|
) -> "DIALApp":
|
|
185
183
|
self.add_api_route(
|
|
186
184
|
f"/openai/deployments/{deployment_name}/embeddings",
|
|
187
|
-
self._embeddings(deployment_name, impl),
|
|
185
|
+
error_handler("openai")(self._embeddings(deployment_name, impl)),
|
|
188
186
|
methods=["POST"],
|
|
189
187
|
)
|
|
190
188
|
|
|
@@ -193,6 +191,7 @@ class DIALApp(FastAPI):
|
|
|
193
191
|
"/openai/v1/embeddings",
|
|
194
192
|
self._embeddings(None, impl),
|
|
195
193
|
methods=["POST"],
|
|
194
|
+
handler_decorator=error_handler("openai"),
|
|
196
195
|
)
|
|
197
196
|
|
|
198
197
|
return self
|
|
@@ -209,7 +208,7 @@ class DIALApp(FastAPI):
|
|
|
209
208
|
):
|
|
210
209
|
self.add_api_route(
|
|
211
210
|
f"/openai/deployments/{deployment_name}/{endpoint}",
|
|
212
|
-
handler,
|
|
211
|
+
error_handler("openai")(handler),
|
|
213
212
|
methods=methods,
|
|
214
213
|
)
|
|
215
214
|
|
|
@@ -221,6 +220,7 @@ class DIALApp(FastAPI):
|
|
|
221
220
|
f"/openai/v1/{endpoint}",
|
|
222
221
|
handler,
|
|
223
222
|
methods=methods,
|
|
223
|
+
handler_decorator=error_handler("openai"),
|
|
224
224
|
)
|
|
225
225
|
|
|
226
226
|
return self
|
|
@@ -281,12 +281,13 @@ class DIALApp(FastAPI):
|
|
|
281
281
|
handler: Handler,
|
|
282
282
|
*,
|
|
283
283
|
methods: list[str],
|
|
284
|
+
handler_decorator: Callable[[Handler], Handler],
|
|
284
285
|
) -> None:
|
|
285
286
|
if endpoint not in self._generic_handlers:
|
|
286
287
|
self._generic_handlers[endpoint] = {}
|
|
287
288
|
self.add_api_route(
|
|
288
289
|
endpoint,
|
|
289
|
-
self._generic_dispatcher(endpoint),
|
|
290
|
+
handler_decorator(self._generic_dispatcher(endpoint)),
|
|
290
291
|
methods=methods,
|
|
291
292
|
)
|
|
292
293
|
self._generic_handlers[endpoint][deployment_name] = handler
|
|
@@ -337,7 +338,6 @@ class DIALApp(FastAPI):
|
|
|
337
338
|
request = await self._parse_request(
|
|
338
339
|
request_type, original_request, deployment_id
|
|
339
340
|
)
|
|
340
|
-
log_debug(f"request[{endpoint}]: {request}")
|
|
341
341
|
|
|
342
342
|
response = await endpoint_impl(request)
|
|
343
343
|
|
|
@@ -350,8 +350,6 @@ class DIALApp(FastAPI):
|
|
|
350
350
|
f"Unexpected response type from {endpoint}: {type(response)}"
|
|
351
351
|
)
|
|
352
352
|
|
|
353
|
-
log_debug(f"response[{endpoint}]: {response_json}")
|
|
354
|
-
|
|
355
353
|
return JSONResponse(content=response_json)
|
|
356
354
|
|
|
357
355
|
return _handler
|
|
@@ -379,11 +377,14 @@ class DIALApp(FastAPI):
|
|
|
379
377
|
)
|
|
380
378
|
set_log_deployment(deployment_id)
|
|
381
379
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
380
|
+
try:
|
|
381
|
+
ret = await request.from_request(
|
|
382
|
+
original_request, deployment_id, self._dial_url
|
|
383
|
+
)
|
|
384
|
+
if not self._allow_extra_request_fields:
|
|
385
|
+
model_validate_extra_fields(ret)
|
|
386
|
+
except ValidationError as e:
|
|
387
|
+
raise invalid_request_error(e) from e
|
|
387
388
|
return ret
|
|
388
389
|
|
|
389
390
|
def _chat_completion(
|
|
@@ -392,7 +393,7 @@ class DIALApp(FastAPI):
|
|
|
392
393
|
impl: ChatCompletion,
|
|
393
394
|
*,
|
|
394
395
|
heartbeat_interval: float | None,
|
|
395
|
-
):
|
|
396
|
+
) -> Handler:
|
|
396
397
|
heartbeat_interval = heartbeat_interval or env_float(
|
|
397
398
|
"DIAL_SDK_SSE_HEARTBEAT_INTERVAL"
|
|
398
399
|
)
|
|
@@ -403,7 +404,6 @@ class DIALApp(FastAPI):
|
|
|
403
404
|
)
|
|
404
405
|
|
|
405
406
|
response = ChatCompletionResponse(request)
|
|
406
|
-
|
|
407
407
|
stream = response._generate_stream(impl.chat_completion)
|
|
408
408
|
|
|
409
409
|
if request.stream:
|
|
@@ -411,7 +411,6 @@ class DIALApp(FastAPI):
|
|
|
411
411
|
stream = add_heartbeat(
|
|
412
412
|
stream,
|
|
413
413
|
heartbeat_interval=heartbeat_interval,
|
|
414
|
-
heartbeat_callback=lambda: log_debug("heartbeat"),
|
|
415
414
|
heartbeat_object=": heartbeat\n\n",
|
|
416
415
|
)
|
|
417
416
|
|
|
@@ -421,7 +420,6 @@ class DIALApp(FastAPI):
|
|
|
421
420
|
)
|
|
422
421
|
else:
|
|
423
422
|
response_json = await to_block_response(stream)
|
|
424
|
-
log_debug(f"response: {response_json}")
|
|
425
423
|
resp = JSONResponse(content=response_json)
|
|
426
424
|
|
|
427
425
|
for key, value in response.headers:
|
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import warnings
|
|
3
|
+
from enum import Enum
|
|
3
4
|
from http import HTTPStatus
|
|
5
|
+
from typing import Literal, assert_never
|
|
4
6
|
|
|
5
7
|
from fastapi import HTTPException as FastAPIException
|
|
6
8
|
from fastapi.responses import JSONResponse
|
|
7
9
|
|
|
8
10
|
from aidial_sdk.utils.json import remove_nones
|
|
9
11
|
|
|
12
|
+
ExceptionType = Literal["openai", "anthropic"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _AnthropicErrorType(Enum):
|
|
16
|
+
INVALID_REQUEST = "invalid_request_error", 400, 422
|
|
17
|
+
AUTHENTICATION = "authentication_error", 401
|
|
18
|
+
PERMISSION = "permission_error", 403
|
|
19
|
+
NOT_FOUND = "not_found_error", 404
|
|
20
|
+
REQUEST_TOO_LARGE = "request_too_large", 413
|
|
21
|
+
RATE_LIMIT = "rate_limit_error", 429
|
|
22
|
+
API = "api_error", 500, 502
|
|
23
|
+
OVERLOADED = "overloaded_error", 529, 503
|
|
24
|
+
|
|
25
|
+
def __init__(self, code: str, *status_codes: int) -> None:
|
|
26
|
+
self.code = code
|
|
27
|
+
self.status_codes = status_codes
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_status_code(cls, status_code: int) -> "_AnthropicErrorType":
|
|
31
|
+
for error_type in cls:
|
|
32
|
+
if status_code in error_type.status_codes:
|
|
33
|
+
return error_type
|
|
34
|
+
return cls.API
|
|
35
|
+
|
|
10
36
|
|
|
11
37
|
class HTTPException(Exception):
|
|
12
38
|
def __init__(
|
|
@@ -47,31 +73,47 @@ class HTTPException(Exception):
|
|
|
47
73
|
args = ", ".join([f"{k}={v!r}" for k, v in error.items()])
|
|
48
74
|
return f"{self.__class__.__name__}({args})"
|
|
49
75
|
|
|
50
|
-
def json_error(self) -> dict:
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
{
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
def json_error(self, api: ExceptionType = "openai") -> dict:
|
|
77
|
+
match api:
|
|
78
|
+
case "openai":
|
|
79
|
+
return {
|
|
80
|
+
"error": remove_nones(
|
|
81
|
+
{
|
|
82
|
+
"message": self.message,
|
|
83
|
+
"type": self.type,
|
|
84
|
+
"param": self.param,
|
|
85
|
+
"code": self.code,
|
|
86
|
+
"display_message": self.display_message,
|
|
87
|
+
**self.extra_fields,
|
|
88
|
+
}
|
|
89
|
+
)
|
|
60
90
|
}
|
|
61
|
-
|
|
62
|
-
|
|
91
|
+
case "anthropic":
|
|
92
|
+
error_type = _AnthropicErrorType.from_status_code(
|
|
93
|
+
self.status_code
|
|
94
|
+
)
|
|
95
|
+
return {
|
|
96
|
+
"type": "error",
|
|
97
|
+
"error": {"type": error_type.code, "message": self.message},
|
|
98
|
+
}
|
|
99
|
+
case _:
|
|
100
|
+
assert_never(api)
|
|
63
101
|
|
|
64
|
-
def to_fastapi_response(
|
|
102
|
+
def to_fastapi_response(
|
|
103
|
+
self, api: ExceptionType = "openai"
|
|
104
|
+
) -> JSONResponse:
|
|
65
105
|
return JSONResponse(
|
|
66
106
|
status_code=self.status_code,
|
|
67
|
-
content=self.json_error(),
|
|
107
|
+
content=self.json_error(api),
|
|
68
108
|
headers=self.headers,
|
|
69
109
|
)
|
|
70
110
|
|
|
71
|
-
def to_fastapi_exception(
|
|
111
|
+
def to_fastapi_exception(
|
|
112
|
+
self, api: ExceptionType = "openai"
|
|
113
|
+
) -> FastAPIException:
|
|
72
114
|
return FastAPIException(
|
|
73
115
|
status_code=self.status_code,
|
|
74
|
-
detail=self.json_error(),
|
|
116
|
+
detail=self.json_error(api),
|
|
75
117
|
headers=self.headers,
|
|
76
118
|
)
|
|
77
119
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator, Iterator
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import openai
|
|
8
|
+
from fastapi import Request
|
|
9
|
+
from openai import AsyncOpenAI, AsyncStream, Omit, omit
|
|
10
|
+
from openai.types.chat import ChatCompletion, ChatCompletionChunk
|
|
11
|
+
|
|
12
|
+
from aidial_sdk.exceptions import HTTPException as DIALException
|
|
13
|
+
from aidial_sdk.translators.messages_to_chat_completions.settings import (
|
|
14
|
+
get_dial_api_version,
|
|
15
|
+
)
|
|
16
|
+
from aidial_sdk.utils.logging import logger as log
|
|
17
|
+
|
|
18
|
+
# The headers of the inbound request that DIAL Core reads when the translator
|
|
19
|
+
# calls it back. Any other header is not forwarded.
|
|
20
|
+
_FORWARDED_HEADERS = {
|
|
21
|
+
# Core's per-request key
|
|
22
|
+
"api-key",
|
|
23
|
+
# Prompt caching policy of the request
|
|
24
|
+
"x-dial-cache-policy",
|
|
25
|
+
# Pins the request to the same upstream
|
|
26
|
+
"x-upstream-id",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _client(
|
|
31
|
+
base_url: str,
|
|
32
|
+
http_client: httpx.AsyncClient,
|
|
33
|
+
default_query: dict[str, str] | None = None,
|
|
34
|
+
) -> AsyncOpenAI:
|
|
35
|
+
return AsyncOpenAI(
|
|
36
|
+
# Required by the SDK; Core authenticates the forwarded `api-key` header.
|
|
37
|
+
api_key="-",
|
|
38
|
+
base_url=base_url,
|
|
39
|
+
default_query=default_query,
|
|
40
|
+
max_retries=0,
|
|
41
|
+
http_client=http_client,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def core_v1_client(
|
|
46
|
+
dial_url: str, http_client: httpx.AsyncClient
|
|
47
|
+
) -> AsyncOpenAI:
|
|
48
|
+
return _client(f"{dial_url}/openai/v1", http_client)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def core_legacy_client(
|
|
52
|
+
dial_url: str, deployment: str, http_client: httpx.AsyncClient
|
|
53
|
+
) -> AsyncOpenAI:
|
|
54
|
+
return _client(
|
|
55
|
+
f"{dial_url}/openai/deployments/{quote(deployment, safe='')}",
|
|
56
|
+
http_client,
|
|
57
|
+
{"api-version": get_dial_api_version()},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def core_headers(request: Request) -> dict[str, str | Omit]:
|
|
62
|
+
# Without the override the SDK sends `Authorization: Bearer -`
|
|
63
|
+
headers: dict[str, str | Omit] = {"Authorization": omit}
|
|
64
|
+
headers.update(
|
|
65
|
+
{
|
|
66
|
+
name: value
|
|
67
|
+
for name, value in request.headers.items()
|
|
68
|
+
if name.lower() in _FORWARDED_HEADERS
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
return headers
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# An error event in a stream has no status code, only a type or a code
|
|
75
|
+
_STREAM_ERROR_STATUS_CODES: dict[str | None, int] = {
|
|
76
|
+
"invalid_request_error": 400,
|
|
77
|
+
"rate_limit_exceeded": 429,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _extract_error_message(e: openai.APIStatusError) -> str:
|
|
82
|
+
body = e.body
|
|
83
|
+
if isinstance(body, dict):
|
|
84
|
+
error = body.get("error")
|
|
85
|
+
if isinstance(error, dict) and isinstance(error.get("message"), str):
|
|
86
|
+
return error["message"]
|
|
87
|
+
if isinstance(error, str) and error:
|
|
88
|
+
return error
|
|
89
|
+
if isinstance(body.get("message"), str):
|
|
90
|
+
return body["message"]
|
|
91
|
+
|
|
92
|
+
if isinstance(body, str) and body.strip():
|
|
93
|
+
return body.strip()
|
|
94
|
+
|
|
95
|
+
return e.message
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _from_core_error(e: openai.APIError) -> DIALException:
|
|
99
|
+
match e:
|
|
100
|
+
case openai.APIStatusError():
|
|
101
|
+
return DIALException(
|
|
102
|
+
_extract_error_message(e), status_code=e.status_code
|
|
103
|
+
)
|
|
104
|
+
case openai.APIConnectionError():
|
|
105
|
+
log.error(f"Failed to reach DIAL Core: {e}")
|
|
106
|
+
return DIALException("Failed to reach DIAL Core", status_code=502)
|
|
107
|
+
case _:
|
|
108
|
+
log.error(f"DIAL Core reported an error in the stream: {e}")
|
|
109
|
+
status_code = (
|
|
110
|
+
_STREAM_ERROR_STATUS_CODES.get(e.type)
|
|
111
|
+
or _STREAM_ERROR_STATUS_CODES.get(e.code)
|
|
112
|
+
or 500
|
|
113
|
+
)
|
|
114
|
+
return DIALException(e.message, status_code=status_code)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@contextmanager
|
|
118
|
+
def _core_errors() -> Iterator[None]:
|
|
119
|
+
try:
|
|
120
|
+
yield
|
|
121
|
+
except openai.APIError as e:
|
|
122
|
+
raise _from_core_error(e) from e
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def _chunks(
|
|
126
|
+
stream: AsyncStream[ChatCompletionChunk],
|
|
127
|
+
) -> AsyncGenerator[ChatCompletionChunk, None]:
|
|
128
|
+
with _core_errors():
|
|
129
|
+
async with stream:
|
|
130
|
+
async for item in stream:
|
|
131
|
+
yield item
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class ChatClientHttp:
|
|
136
|
+
client: AsyncOpenAI
|
|
137
|
+
core_headers: dict[str, str | Omit]
|
|
138
|
+
|
|
139
|
+
async def create_stream(
|
|
140
|
+
self, body: dict
|
|
141
|
+
) -> AsyncGenerator[ChatCompletionChunk, None]:
|
|
142
|
+
with _core_errors():
|
|
143
|
+
stream = await self.client.post(
|
|
144
|
+
"/chat/completions",
|
|
145
|
+
cast_to=ChatCompletion,
|
|
146
|
+
body=body,
|
|
147
|
+
options={"headers": self.core_headers},
|
|
148
|
+
stream=True,
|
|
149
|
+
stream_cls=AsyncStream[ChatCompletionChunk],
|
|
150
|
+
)
|
|
151
|
+
return _chunks(stream)
|
|
152
|
+
|
|
153
|
+
async def create_block(self, body: dict) -> ChatCompletion:
|
|
154
|
+
with _core_errors():
|
|
155
|
+
response = await self.client.post(
|
|
156
|
+
"/chat/completions",
|
|
157
|
+
cast_to=ChatCompletion,
|
|
158
|
+
body=body,
|
|
159
|
+
options={"headers": self.core_headers},
|
|
160
|
+
)
|
|
161
|
+
return response
|
|
@@ -8,7 +8,9 @@ from anthropic.types.beta.message_create_params import MessageCreateParams
|
|
|
8
8
|
from fastapi import FastAPI, Request
|
|
9
9
|
from fastapi.responses import JSONResponse, Response, StreamingResponse
|
|
10
10
|
|
|
11
|
+
from aidial_sdk._errors import error_handler
|
|
11
12
|
from aidial_sdk.deployment.from_request_mixin import get_client_deployment_id
|
|
13
|
+
from aidial_sdk.exceptions import InvalidRequestError, ResourceNotFoundError
|
|
12
14
|
from aidial_sdk.translators.messages_to_chat_completions.backend.http import (
|
|
13
15
|
ChatClientHttp,
|
|
14
16
|
core_headers,
|
|
@@ -24,27 +26,24 @@ from aidial_sdk.translators.messages_to_chat_completions.chat_completions.stream
|
|
|
24
26
|
from aidial_sdk.translators.messages_to_chat_completions.chat_completions.to_chat_completions import (
|
|
25
27
|
to_chat_completions_request,
|
|
26
28
|
)
|
|
27
|
-
from aidial_sdk.translators.messages_to_chat_completions.common import (
|
|
28
|
-
not_found,
|
|
29
|
-
with_debug_logging,
|
|
30
|
-
)
|
|
31
|
-
from aidial_sdk.translators.messages_to_chat_completions.errors import (
|
|
32
|
-
AnthropicErrorType,
|
|
33
|
-
AnthropicHTTPError,
|
|
34
|
-
translator_error_handler,
|
|
35
|
-
)
|
|
36
29
|
from aidial_sdk.translators.messages_to_chat_completions.settings import (
|
|
37
30
|
get_dial_url,
|
|
38
31
|
)
|
|
39
32
|
from aidial_sdk.translators.messages_to_chat_completions.translation_state import (
|
|
40
33
|
TranslationState,
|
|
41
34
|
)
|
|
35
|
+
from aidial_sdk.utils._logging_middleware import LoggingMiddleware
|
|
42
36
|
from aidial_sdk.utils.logging import logger as log
|
|
43
37
|
|
|
44
38
|
ClientT = TypeVar("ClientT")
|
|
45
39
|
ClientFactory = Callable[[Request], Awaitable[ClientT]] | ClientT
|
|
46
40
|
|
|
47
41
|
|
|
42
|
+
async def _not_found(request: Request) -> Response:
|
|
43
|
+
error = ResourceNotFoundError(f"Unknown endpoint: {request.url.path}")
|
|
44
|
+
return error.to_fastapi_response("anthropic")
|
|
45
|
+
|
|
46
|
+
|
|
48
47
|
async def handle_anthropic_messages(
|
|
49
48
|
request: Request, client: ChatClientHttp
|
|
50
49
|
) -> Response:
|
|
@@ -53,10 +52,7 @@ async def handle_anthropic_messages(
|
|
|
53
52
|
try:
|
|
54
53
|
req: MessageCreateParams = await request.json()
|
|
55
54
|
except ValueError as e:
|
|
56
|
-
raise
|
|
57
|
-
AnthropicErrorType.INVALID_REQUEST,
|
|
58
|
-
f"Request body is not valid JSON: {e}",
|
|
59
|
-
) from e
|
|
55
|
+
raise InvalidRequestError(f"Request body is not valid JSON: {e}") from e
|
|
60
56
|
|
|
61
57
|
translation_state = TranslationState()
|
|
62
58
|
chat_request = to_chat_completions_request(
|
|
@@ -65,7 +61,7 @@ async def handle_anthropic_messages(
|
|
|
65
61
|
|
|
66
62
|
body = chat_request.model_dump(mode="json", exclude_none=True)
|
|
67
63
|
if log.isEnabledFor(logging.DEBUG):
|
|
68
|
-
log.debug(f"
|
|
64
|
+
log.debug(f"chat request: {json.dumps(body)}")
|
|
69
65
|
|
|
70
66
|
if chat_request.stream:
|
|
71
67
|
events = await client.create_stream(body)
|
|
@@ -85,6 +81,7 @@ def create_anthropic_api_app(
|
|
|
85
81
|
get_client: ClientFactory[httpx.AsyncClient],
|
|
86
82
|
) -> FastAPI:
|
|
87
83
|
app = FastAPI()
|
|
84
|
+
app.add_middleware(LoggingMiddleware)
|
|
88
85
|
|
|
89
86
|
async def _chat_client(request: Request, *, legacy: bool) -> ChatClientHttp:
|
|
90
87
|
http_client = (
|
|
@@ -100,16 +97,14 @@ def create_anthropic_api_app(
|
|
|
100
97
|
return ChatClientHttp(client, core_headers(request))
|
|
101
98
|
|
|
102
99
|
@app.post("/anthropic/v1/messages")
|
|
103
|
-
@
|
|
104
|
-
@translator_error_handler
|
|
100
|
+
@error_handler("anthropic")
|
|
105
101
|
async def _handle_v1_messages(request: Request) -> Response:
|
|
106
102
|
return await handle_anthropic_messages(
|
|
107
103
|
request, await _chat_client(request, legacy=False)
|
|
108
104
|
)
|
|
109
105
|
|
|
110
106
|
@app.post("/legacy/anthropic/v1/messages")
|
|
111
|
-
@
|
|
112
|
-
@translator_error_handler
|
|
107
|
+
@error_handler("anthropic")
|
|
113
108
|
async def _handle_legacy_messages(request: Request) -> Response:
|
|
114
109
|
return await handle_anthropic_messages(
|
|
115
110
|
request, await _chat_client(request, legacy=True)
|
|
@@ -117,7 +112,7 @@ def create_anthropic_api_app(
|
|
|
117
112
|
|
|
118
113
|
app.add_api_route(
|
|
119
114
|
"/{full_path:path}",
|
|
120
|
-
|
|
115
|
+
_not_found,
|
|
121
116
|
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
|
122
117
|
)
|
|
123
118
|
|
|
@@ -2,13 +2,10 @@ from anthropic.types.beta.message_create_params import MessageCreateParams
|
|
|
2
2
|
from openai.types.shared import ReasoningEffort
|
|
3
3
|
from pydantic import TypeAdapter, ValidationError
|
|
4
4
|
|
|
5
|
+
from aidial_sdk.exceptions import InvalidRequestError
|
|
5
6
|
from aidial_sdk.translators.messages_to_chat_completions.chat_completions.system_turns import (
|
|
6
7
|
visible_messages,
|
|
7
8
|
)
|
|
8
|
-
from aidial_sdk.translators.messages_to_chat_completions.errors import (
|
|
9
|
-
AnthropicErrorType,
|
|
10
|
-
AnthropicHTTPError,
|
|
11
|
-
)
|
|
12
9
|
from aidial_sdk.utils.logging import logger as log
|
|
13
10
|
|
|
14
11
|
_EFFORT = TypeAdapter(ReasoningEffort)
|
|
@@ -30,10 +27,9 @@ def resolve_effort(req: MessageCreateParams) -> ReasoningEffort:
|
|
|
30
27
|
try:
|
|
31
28
|
return _EFFORT.validate_python(effort)
|
|
32
29
|
except ValidationError:
|
|
33
|
-
raise
|
|
34
|
-
AnthropicErrorType.INVALID_REQUEST,
|
|
30
|
+
raise InvalidRequestError(
|
|
35
31
|
f"Cannot convert Anthropic Messages API reasoning effort {effort} "
|
|
36
|
-
"to OpenAI Chat Completions API reasoning effort"
|
|
32
|
+
"to OpenAI Chat Completions API reasoning effort"
|
|
37
33
|
) from None
|
|
38
34
|
|
|
39
35
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from collections.abc import AsyncIterator, Iterator
|
|
1
|
+
from collections.abc import AsyncGenerator, AsyncIterator, Iterator
|
|
2
2
|
|
|
3
3
|
from anthropic.types import (
|
|
4
4
|
RawContentBlockDeltaEvent,
|
|
@@ -28,7 +28,6 @@ from aidial_sdk.translators.messages_to_chat_completions.chat_completions.from_c
|
|
|
28
28
|
)
|
|
29
29
|
from aidial_sdk.translators.messages_to_chat_completions.sse_stream import (
|
|
30
30
|
AnthropicStreamState,
|
|
31
|
-
ClosableAsyncStream,
|
|
32
31
|
format_sse,
|
|
33
32
|
run_sse_stream,
|
|
34
33
|
)
|
|
@@ -229,7 +228,7 @@ def _merge_usage(
|
|
|
229
228
|
|
|
230
229
|
|
|
231
230
|
async def translate_stream(
|
|
232
|
-
stream:
|
|
231
|
+
stream: AsyncGenerator[ChatCompletionChunk, None],
|
|
233
232
|
requested_model: str,
|
|
234
233
|
translation_state: TranslationState,
|
|
235
234
|
) -> AsyncIterator[bytes]:
|
|
@@ -240,6 +239,5 @@ async def translate_stream(
|
|
|
240
239
|
state,
|
|
241
240
|
state.handle,
|
|
242
241
|
on_finalize=state.finalize,
|
|
243
|
-
log_context="Chat Completions",
|
|
244
242
|
):
|
|
245
243
|
yield chunk
|