aidial-sdk 0.48.0.dev4__tar.gz → 0.48.0.dev6__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.dev4 → aidial_sdk-0.48.0.dev6}/PKG-INFO +1 -1
- aidial_sdk-0.48.0.dev6/aidial_sdk/_constants.py +3 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/application.py +13 -14
- aidial_sdk-0.48.0.dev6/aidial_sdk/translators/messages_to_chat_completions/backend/client.py +5 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/backend/http.py +16 -6
- aidial_sdk-0.48.0.dev6/aidial_sdk/translators/messages_to_chat_completions/chat_completions/_reasoning_content.py +95 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/app.py +4 -1
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/dial_extensions.py +0 -25
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/from_chat_completions.py +12 -17
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/streaming.py +19 -27
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/streaming.py +43 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/pyproject.toml +1 -1
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/LICENSE +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/README.md +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/_errors.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/_pydantic/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/_pydantic/_compat.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/_pydantic/_model_config.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/_cache.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/_types.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/base.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/choice.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/choice_base.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/chunks.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/enums.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/form.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/function_call.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/function_tool_call.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/request.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/response.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/stage.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/deployment/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/deployment/_headers.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/deployment/configuration.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/deployment/from_request_mixin.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/deployment/rate.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/deployment/tokenize.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/deployment/truncate_prompt.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/embeddings/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/embeddings/base.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/embeddings/request.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/embeddings/response.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/exceptions.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/header_propagator.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/py.typed +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/pydantic/v2.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/pydantic_v1/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/response_headers.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/telemetry/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/telemetry/_context.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/telemetry/init.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/telemetry/types.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/cache_breakpoints.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/reasoning.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/system_turns.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/to_chat_completions.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/settings.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/sse_stream.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/translation_state.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/__init__.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_attachment.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_cancel_scope.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_content_stream.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_disconnect_middleware.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_indexed_list.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_interpolation.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_json_log_formatter.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_logging.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_logging_middleware.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_reflection.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/env.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/errors.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/json.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/log_config.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/logging.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/merge_chunks.py +0 -0
- {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/pydantic.py +0 -0
|
@@ -9,6 +9,7 @@ from fastapi import FastAPI, HTTPException, Request
|
|
|
9
9
|
from fastapi.responses import JSONResponse, Response, StreamingResponse
|
|
10
10
|
from starlette.types import Receive, Scope, Send
|
|
11
11
|
|
|
12
|
+
from aidial_sdk._constants import DIAL_SDK_SSE_HEARTBEAT_INTERVAL
|
|
12
13
|
from aidial_sdk._errors import (
|
|
13
14
|
Handler,
|
|
14
15
|
dial_exception_handler,
|
|
@@ -45,7 +46,7 @@ from aidial_sdk.utils._interpolation import (
|
|
|
45
46
|
)
|
|
46
47
|
from aidial_sdk.utils._logging_middleware import LoggingMiddleware
|
|
47
48
|
from aidial_sdk.utils._reflection import get_method_implementation
|
|
48
|
-
from aidial_sdk.utils.env import
|
|
49
|
+
from aidial_sdk.utils.env import env_var_list
|
|
49
50
|
from aidial_sdk.utils.log_config import configure_sdk_logger
|
|
50
51
|
from aidial_sdk.utils.logging import (
|
|
51
52
|
reset_log_context,
|
|
@@ -53,7 +54,7 @@ from aidial_sdk.utils.logging import (
|
|
|
53
54
|
)
|
|
54
55
|
from aidial_sdk.utils.pydantic import model_validate_extra_fields
|
|
55
56
|
from aidial_sdk.utils.streaming import (
|
|
56
|
-
|
|
57
|
+
start_streaming,
|
|
57
58
|
to_block_response,
|
|
58
59
|
to_streaming_response,
|
|
59
60
|
)
|
|
@@ -203,6 +204,10 @@ class DIALApp(FastAPI):
|
|
|
203
204
|
*,
|
|
204
205
|
heartbeat_interval: float | None = None,
|
|
205
206
|
) -> "DIALApp":
|
|
207
|
+
heartbeat_interval = (
|
|
208
|
+
heartbeat_interval or DIAL_SDK_SSE_HEARTBEAT_INTERVAL
|
|
209
|
+
)
|
|
210
|
+
|
|
206
211
|
for endpoint, handler, methods in self._chat_completion_handlers(
|
|
207
212
|
deployment_name, impl, heartbeat_interval
|
|
208
213
|
):
|
|
@@ -394,10 +399,6 @@ class DIALApp(FastAPI):
|
|
|
394
399
|
*,
|
|
395
400
|
heartbeat_interval: float | None,
|
|
396
401
|
) -> Handler:
|
|
397
|
-
heartbeat_interval = heartbeat_interval or env_float(
|
|
398
|
-
"DIAL_SDK_SSE_HEARTBEAT_INTERVAL"
|
|
399
|
-
)
|
|
400
|
-
|
|
401
402
|
async def _handler(original_request: Request) -> Response:
|
|
402
403
|
request = await self._parse_request(
|
|
403
404
|
ChatCompletionRequest, original_request, deployment_id
|
|
@@ -407,15 +408,13 @@ class DIALApp(FastAPI):
|
|
|
407
408
|
stream = response._generate_stream(impl.chat_completion)
|
|
408
409
|
|
|
409
410
|
if request.stream:
|
|
410
|
-
|
|
411
|
-
stream
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
)
|
|
416
|
-
|
|
411
|
+
sse_stream = await start_streaming(
|
|
412
|
+
stream,
|
|
413
|
+
heartbeat_interval=heartbeat_interval,
|
|
414
|
+
heartbeat=": heartbeat\n\n",
|
|
415
|
+
)
|
|
417
416
|
resp = StreamingResponse(
|
|
418
|
-
await to_streaming_response(
|
|
417
|
+
await to_streaming_response(sse_stream),
|
|
419
418
|
media_type="text/event-stream",
|
|
420
419
|
)
|
|
421
420
|
else:
|
|
@@ -10,10 +10,15 @@ from openai import AsyncOpenAI, AsyncStream, Omit, omit
|
|
|
10
10
|
from openai.types.chat import ChatCompletion, ChatCompletionChunk
|
|
11
11
|
|
|
12
12
|
from aidial_sdk.exceptions import HTTPException as DIALException
|
|
13
|
+
from aidial_sdk.translators.messages_to_chat_completions.backend.client import (
|
|
14
|
+
HEARTBEAT,
|
|
15
|
+
Heartbeat,
|
|
16
|
+
)
|
|
13
17
|
from aidial_sdk.translators.messages_to_chat_completions.settings import (
|
|
14
18
|
get_dial_api_version,
|
|
15
19
|
)
|
|
16
20
|
from aidial_sdk.utils.logging import logger as log
|
|
21
|
+
from aidial_sdk.utils.streaming import with_heartbeat
|
|
17
22
|
|
|
18
23
|
# The headers of the inbound request that DIAL Core reads when the translator
|
|
19
24
|
# calls it back. Any other header is not forwarded.
|
|
@@ -122,12 +127,16 @@ def _core_errors() -> Iterator[None]:
|
|
|
122
127
|
raise _from_core_error(e) from e
|
|
123
128
|
|
|
124
129
|
|
|
125
|
-
async def
|
|
126
|
-
stream: AsyncStream[ChatCompletionChunk],
|
|
127
|
-
) -> AsyncGenerator[ChatCompletionChunk, None]:
|
|
130
|
+
async def _with_heartbeat(
|
|
131
|
+
stream: AsyncStream[ChatCompletionChunk], heartbeat_interval: float | None
|
|
132
|
+
) -> AsyncGenerator[ChatCompletionChunk | Heartbeat, None]:
|
|
128
133
|
with _core_errors():
|
|
129
134
|
async with stream:
|
|
130
|
-
async for item in
|
|
135
|
+
async for item in with_heartbeat(
|
|
136
|
+
aiter(stream),
|
|
137
|
+
heartbeat_interval=heartbeat_interval,
|
|
138
|
+
heartbeat=HEARTBEAT,
|
|
139
|
+
):
|
|
131
140
|
yield item
|
|
132
141
|
|
|
133
142
|
|
|
@@ -135,10 +144,11 @@ async def _chunks(
|
|
|
135
144
|
class ChatClientHttp:
|
|
136
145
|
client: AsyncOpenAI
|
|
137
146
|
core_headers: dict[str, str | Omit]
|
|
147
|
+
heartbeat_interval: float | None
|
|
138
148
|
|
|
139
149
|
async def create_stream(
|
|
140
150
|
self, body: dict
|
|
141
|
-
) -> AsyncGenerator[ChatCompletionChunk, None]:
|
|
151
|
+
) -> AsyncGenerator[ChatCompletionChunk | Heartbeat, None]:
|
|
142
152
|
with _core_errors():
|
|
143
153
|
stream = await self.client.post(
|
|
144
154
|
"/chat/completions",
|
|
@@ -148,7 +158,7 @@ class ChatClientHttp:
|
|
|
148
158
|
stream=True,
|
|
149
159
|
stream_cls=AsyncStream[ChatCompletionChunk],
|
|
150
160
|
)
|
|
151
|
-
return
|
|
161
|
+
return _with_heartbeat(stream, self.heartbeat_interval)
|
|
152
162
|
|
|
153
163
|
async def create_block(self, body: dict) -> ChatCompletion:
|
|
154
164
|
with _core_errors():
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from openai.types.chat import ChatCompletionMessage
|
|
6
|
+
from openai.types.chat.chat_completion_chunk import ChoiceDelta
|
|
7
|
+
|
|
8
|
+
from aidial_sdk.translators.messages_to_chat_completions.chat_completions.dial_extensions import (
|
|
9
|
+
CustomContent,
|
|
10
|
+
parse_extras,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
_Source = Literal["azure", "claude", "dial"]
|
|
14
|
+
|
|
15
|
+
_REASONING_STAGE = re.compile(r"think|thought|reason", re.IGNORECASE)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ReasoningContent:
|
|
20
|
+
content: str | None
|
|
21
|
+
signature: str | None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ReasoningContentProvider:
|
|
25
|
+
"""
|
|
26
|
+
Reads the reasoning of a message, or of a stream of message chunks, from
|
|
27
|
+
`reasoning_content`, the Claude state and the DIAL reasoning stages. They
|
|
28
|
+
carry the same text, chunked differently: only the text none of them has
|
|
29
|
+
delivered before is returned.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
# Every source is a prefix of the same text, so offsets suffice.
|
|
34
|
+
self._returned = 0
|
|
35
|
+
self._received: dict[_Source, int] = {
|
|
36
|
+
"azure": 0,
|
|
37
|
+
"claude": 0,
|
|
38
|
+
"dial": 0,
|
|
39
|
+
}
|
|
40
|
+
self._reasoning_stages: set[int | None] = set()
|
|
41
|
+
|
|
42
|
+
def get_reasoning_content(
|
|
43
|
+
self, message: ChatCompletionMessage | ChoiceDelta
|
|
44
|
+
) -> ReasoningContent:
|
|
45
|
+
custom_content = parse_extras(message.model_extra).custom_content
|
|
46
|
+
thinking, signature = _get_claude_reasoning_content(custom_content)
|
|
47
|
+
content = (
|
|
48
|
+
self._unseen("azure", _get_azure_reasoning_content(message))
|
|
49
|
+
+ self._unseen("claude", thinking)
|
|
50
|
+
+ self._unseen("dial", self._dial_reasoning_content(custom_content))
|
|
51
|
+
)
|
|
52
|
+
return ReasoningContent(
|
|
53
|
+
content=content or None,
|
|
54
|
+
signature=signature or None,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def _unseen(self, source: _Source, text: str | None) -> str:
|
|
58
|
+
text = text or ""
|
|
59
|
+
start = self._received[source]
|
|
60
|
+
self._received[source] += len(text)
|
|
61
|
+
unseen = text[self._returned - start :]
|
|
62
|
+
self._returned = max(self._returned, self._received[source])
|
|
63
|
+
return unseen
|
|
64
|
+
|
|
65
|
+
def _dial_reasoning_content(
|
|
66
|
+
self, custom_content: CustomContent | None
|
|
67
|
+
) -> str:
|
|
68
|
+
text = ""
|
|
69
|
+
for stage in (custom_content.stages if custom_content else None) or []:
|
|
70
|
+
# In a stream, only the first chunk of a stage carries its name.
|
|
71
|
+
if _is_reasoning_stage(stage.name):
|
|
72
|
+
self._reasoning_stages.add(stage.index)
|
|
73
|
+
if stage.index in self._reasoning_stages:
|
|
74
|
+
text += stage.content or ""
|
|
75
|
+
return text
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _get_azure_reasoning_content(
|
|
79
|
+
message: ChatCompletionMessage | ChoiceDelta,
|
|
80
|
+
) -> str | None:
|
|
81
|
+
return (message.model_extra or {}).get("reasoning_content")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _get_claude_reasoning_content(
|
|
85
|
+
custom_content: CustomContent | None,
|
|
86
|
+
) -> tuple[str | None, str | None]:
|
|
87
|
+
state = custom_content.state if custom_content else None
|
|
88
|
+
for block in (state.claude_message_content if state else None) or []:
|
|
89
|
+
if block.type == "thinking":
|
|
90
|
+
return block.thinking, block.signature
|
|
91
|
+
return None, None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _is_reasoning_stage(name: str | None) -> bool:
|
|
95
|
+
return name is not None and bool(_REASONING_STAGE.search(name))
|
|
@@ -8,6 +8,7 @@ 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._constants import DIAL_SDK_SSE_HEARTBEAT_INTERVAL
|
|
11
12
|
from aidial_sdk._errors import error_handler
|
|
12
13
|
from aidial_sdk.deployment.from_request_mixin import get_client_deployment_id
|
|
13
14
|
from aidial_sdk.exceptions import InvalidRequestError, ResourceNotFoundError
|
|
@@ -94,7 +95,9 @@ def create_anthropic_api_app(
|
|
|
94
95
|
else:
|
|
95
96
|
client = core_v1_client(dial_url, http_client)
|
|
96
97
|
|
|
97
|
-
return ChatClientHttp(
|
|
98
|
+
return ChatClientHttp(
|
|
99
|
+
client, core_headers(request), DIAL_SDK_SSE_HEARTBEAT_INTERVAL
|
|
100
|
+
)
|
|
98
101
|
|
|
99
102
|
@app.post("/anthropic/v1/messages")
|
|
100
103
|
@error_handler("anthropic")
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import re
|
|
2
1
|
from collections.abc import Mapping
|
|
3
2
|
|
|
4
3
|
from openai.types.chat.chat_completion_message import Annotation
|
|
@@ -7,8 +6,6 @@ from pydantic import ValidationError
|
|
|
7
6
|
from aidial_sdk.utils.logging import logger as log
|
|
8
7
|
from aidial_sdk.utils.pydantic import ExtraAllowModel
|
|
9
8
|
|
|
10
|
-
_REASONING_STAGE = re.compile(r"think|thought|reason", re.IGNORECASE)
|
|
11
|
-
|
|
12
9
|
|
|
13
10
|
class ClaudeMessageBlock(ExtraAllowModel):
|
|
14
11
|
type: str | None = None
|
|
@@ -42,25 +39,3 @@ def parse_extras(model_extra: Mapping[str, object] | None) -> DialExtras:
|
|
|
42
39
|
except ValidationError as e:
|
|
43
40
|
log.error(f"Failed to parse DIAL extension fields: {e}")
|
|
44
41
|
return DialExtras()
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def is_reasoning_stage(name: str | None) -> bool:
|
|
48
|
-
return name is not None and bool(_REASONING_STAGE.search(name))
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def get_claude_thinking_block(
|
|
52
|
-
custom_content: CustomContent | None,
|
|
53
|
-
) -> ClaudeMessageBlock | None:
|
|
54
|
-
state = custom_content.state if custom_content else None
|
|
55
|
-
for block in (state.claude_message_content if state else None) or []:
|
|
56
|
-
if block.type == "thinking" and block.thinking:
|
|
57
|
-
return block
|
|
58
|
-
return None
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def get_dial_reasoning_content(custom_content: CustomContent | None) -> str:
|
|
62
|
-
return "".join(
|
|
63
|
-
stage.content or ""
|
|
64
|
-
for stage in (custom_content.stages if custom_content else None) or []
|
|
65
|
-
if is_reasoning_stage(stage.name)
|
|
66
|
-
)
|
|
@@ -25,11 +25,8 @@ from openai.types.chat.chat_completion_message_tool_call import (
|
|
|
25
25
|
)
|
|
26
26
|
from openai.types.completion_usage import CompletionUsage, PromptTokensDetails
|
|
27
27
|
|
|
28
|
-
from aidial_sdk.translators.messages_to_chat_completions.chat_completions.
|
|
29
|
-
|
|
30
|
-
get_claude_thinking_block,
|
|
31
|
-
get_dial_reasoning_content,
|
|
32
|
-
parse_extras,
|
|
28
|
+
from aidial_sdk.translators.messages_to_chat_completions.chat_completions._reasoning_content import (
|
|
29
|
+
ReasoningContentProvider,
|
|
33
30
|
)
|
|
34
31
|
from aidial_sdk.translators.messages_to_chat_completions.translation_state import (
|
|
35
32
|
TranslationState,
|
|
@@ -76,8 +73,7 @@ def _to_anthropic_content_blocks(
|
|
|
76
73
|
|
|
77
74
|
content: list[AnthropicContentBlock] = []
|
|
78
75
|
|
|
79
|
-
|
|
80
|
-
if thinking := to_anthropic_thinking_block(extras.custom_content):
|
|
76
|
+
if thinking := to_anthropic_thinking_block(message):
|
|
81
77
|
content.append(thinking)
|
|
82
78
|
|
|
83
79
|
content.extend(
|
|
@@ -100,17 +96,16 @@ def _text_block(text: str) -> TextBlock:
|
|
|
100
96
|
|
|
101
97
|
|
|
102
98
|
def to_anthropic_thinking_block(
|
|
103
|
-
|
|
99
|
+
message: ChatCompletionMessage,
|
|
104
100
|
) -> ThinkingBlock | None:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return None
|
|
101
|
+
reasoning = ReasoningContentProvider().get_reasoning_content(message)
|
|
102
|
+
if reasoning.content is None:
|
|
103
|
+
return None
|
|
104
|
+
return ThinkingBlock(
|
|
105
|
+
type="thinking",
|
|
106
|
+
thinking=reasoning.content,
|
|
107
|
+
signature=reasoning.signature or "",
|
|
108
|
+
)
|
|
114
109
|
|
|
115
110
|
|
|
116
111
|
def _citations(annotations: list[Annotation] | None) -> list[tuple[str, str]]:
|
|
@@ -16,9 +16,13 @@ from openai.types.chat import ChatCompletionChunk
|
|
|
16
16
|
from openai.types.chat.chat_completion_chunk import Choice, ChoiceDeltaToolCall
|
|
17
17
|
from openai.types.completion_usage import CompletionUsage
|
|
18
18
|
|
|
19
|
+
from aidial_sdk.translators.messages_to_chat_completions.backend.client import (
|
|
20
|
+
Heartbeat,
|
|
21
|
+
)
|
|
22
|
+
from aidial_sdk.translators.messages_to_chat_completions.chat_completions._reasoning_content import (
|
|
23
|
+
ReasoningContentProvider,
|
|
24
|
+
)
|
|
19
25
|
from aidial_sdk.translators.messages_to_chat_completions.chat_completions.dial_extensions import (
|
|
20
|
-
CustomContent,
|
|
21
|
-
is_reasoning_stage,
|
|
22
26
|
parse_extras,
|
|
23
27
|
)
|
|
24
28
|
from aidial_sdk.translators.messages_to_chat_completions.chat_completions.from_chat_completions import (
|
|
@@ -55,15 +59,18 @@ class ChatCompletionsToAnthropicStream(AnthropicStreamState):
|
|
|
55
59
|
self.is_refusal = False
|
|
56
60
|
self.has_tool_call = False
|
|
57
61
|
self._translation_state = translation_state
|
|
58
|
-
self.
|
|
62
|
+
self._reasoning = ReasoningContentProvider()
|
|
59
63
|
self._seen_citations: set[str] = set()
|
|
60
|
-
self._signed = False
|
|
61
64
|
self._tools: dict[int, int] = {}
|
|
62
65
|
|
|
63
|
-
def handle(self, chunk: ChatCompletionChunk) -> Iterator[bytes]:
|
|
66
|
+
def handle(self, chunk: ChatCompletionChunk | Heartbeat) -> Iterator[bytes]:
|
|
64
67
|
if self.terminated:
|
|
65
68
|
return
|
|
66
69
|
|
|
70
|
+
if isinstance(chunk, Heartbeat):
|
|
71
|
+
yield format_sse({"type": "ping"})
|
|
72
|
+
return
|
|
73
|
+
|
|
67
74
|
if not self.started:
|
|
68
75
|
self.message_id = chunk.id
|
|
69
76
|
yield from self._start()
|
|
@@ -84,7 +91,12 @@ class ChatCompletionsToAnthropicStream(AnthropicStreamState):
|
|
|
84
91
|
|
|
85
92
|
if choice.finish_reason:
|
|
86
93
|
self.finish_reason = choice.finish_reason
|
|
87
|
-
|
|
94
|
+
|
|
95
|
+
reasoning = self._reasoning.get_reasoning_content(delta)
|
|
96
|
+
if reasoning.content:
|
|
97
|
+
yield from self._thinking_delta(reasoning.content)
|
|
98
|
+
if reasoning.signature:
|
|
99
|
+
yield from self._signature_delta(reasoning.signature)
|
|
88
100
|
|
|
89
101
|
if text := delta.content:
|
|
90
102
|
yield from self._emit_text(text)
|
|
@@ -106,26 +118,6 @@ class ChatCompletionsToAnthropicStream(AnthropicStreamState):
|
|
|
106
118
|
for call in delta.tool_calls or []:
|
|
107
119
|
yield from self._on_tool_call_delta(call)
|
|
108
120
|
|
|
109
|
-
def _on_custom_content(
|
|
110
|
-
self, custom_content: CustomContent | None
|
|
111
|
-
) -> Iterator[bytes]:
|
|
112
|
-
if custom_content is None:
|
|
113
|
-
return
|
|
114
|
-
|
|
115
|
-
for stage in custom_content.stages or []:
|
|
116
|
-
if is_reasoning_stage(stage.name):
|
|
117
|
-
self._reasoning_stages.add(stage.index)
|
|
118
|
-
if stage.index in self._reasoning_stages and stage.content:
|
|
119
|
-
yield from self._thinking_delta(stage.content)
|
|
120
|
-
|
|
121
|
-
if self._signed or custom_content.state is None:
|
|
122
|
-
return
|
|
123
|
-
for block in custom_content.state.claude_message_content or []:
|
|
124
|
-
if block.type == "thinking" and block.signature:
|
|
125
|
-
self._signed = True
|
|
126
|
-
yield from self._signature_delta(block.signature)
|
|
127
|
-
break
|
|
128
|
-
|
|
129
121
|
def _thinking_delta(self, text: str) -> Iterator[bytes]:
|
|
130
122
|
yield from self._close_tools()
|
|
131
123
|
if not self.is_open(_THINKING_KEY):
|
|
@@ -228,7 +220,7 @@ def _merge_usage(
|
|
|
228
220
|
|
|
229
221
|
|
|
230
222
|
async def translate_stream(
|
|
231
|
-
stream: AsyncGenerator[ChatCompletionChunk, None],
|
|
223
|
+
stream: AsyncGenerator[ChatCompletionChunk | Heartbeat, None],
|
|
232
224
|
requested_model: str,
|
|
233
225
|
translation_state: TranslationState,
|
|
234
226
|
) -> AsyncIterator[bytes]:
|
|
@@ -151,3 +151,46 @@ async def add_heartbeat(
|
|
|
151
151
|
|
|
152
152
|
if heartbeat_callback is not None:
|
|
153
153
|
await _call_heartbeat_callback(heartbeat_callback)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def with_heartbeat(
|
|
157
|
+
stream: AsyncIterator[_T],
|
|
158
|
+
*,
|
|
159
|
+
heartbeat_interval: float | None,
|
|
160
|
+
heartbeat: _T,
|
|
161
|
+
) -> AsyncIterator[_T]:
|
|
162
|
+
if not heartbeat_interval:
|
|
163
|
+
return stream
|
|
164
|
+
return add_heartbeat(
|
|
165
|
+
stream,
|
|
166
|
+
heartbeat_interval=heartbeat_interval,
|
|
167
|
+
heartbeat_object=heartbeat,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
async def start_streaming(
|
|
172
|
+
stream: AsyncIterator[_T],
|
|
173
|
+
*,
|
|
174
|
+
heartbeat_interval: float | None,
|
|
175
|
+
heartbeat: _T,
|
|
176
|
+
) -> AsyncIterator[_T]:
|
|
177
|
+
"""
|
|
178
|
+
Waits for the first item of the stream or, while it is idle, the first
|
|
179
|
+
heartbeat. A DIALException as the first item is raised, so that it becomes
|
|
180
|
+
an HTTP error, while a later one is left in the stream.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
stream = with_heartbeat(
|
|
184
|
+
stream, heartbeat_interval=heartbeat_interval, heartbeat=heartbeat
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
first_item = await anext(stream)
|
|
188
|
+
if isinstance(first_item, DIALException):
|
|
189
|
+
raise first_item
|
|
190
|
+
|
|
191
|
+
async def _stream() -> AsyncIterator[_T]:
|
|
192
|
+
yield first_item
|
|
193
|
+
async for item in stream:
|
|
194
|
+
yield item
|
|
195
|
+
|
|
196
|
+
return _stream()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "aidial-sdk"
|
|
3
|
-
version = "0.48.0.
|
|
3
|
+
version = "0.48.0.dev6"
|
|
4
4
|
description = "Framework to create applications and model adapters for AI DIAL"
|
|
5
5
|
authors = [{ name = "EPAM RAIL", email = "SpecialEPM-DIALDevTeam@epam.com" }]
|
|
6
6
|
license = "Apache-2.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/function_call.py
RENAMED
|
File without changes
|
{aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/function_tool_call.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/deployment/from_request_mixin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_disconnect_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|