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.
Files changed (80) hide show
  1. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/PKG-INFO +1 -1
  2. aidial_sdk-0.48.0.dev6/aidial_sdk/_constants.py +3 -0
  3. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/application.py +13 -14
  4. aidial_sdk-0.48.0.dev6/aidial_sdk/translators/messages_to_chat_completions/backend/client.py +5 -0
  5. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/backend/http.py +16 -6
  6. aidial_sdk-0.48.0.dev6/aidial_sdk/translators/messages_to_chat_completions/chat_completions/_reasoning_content.py +95 -0
  7. {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
  8. {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
  9. {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
  10. {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
  11. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/streaming.py +43 -0
  12. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/pyproject.toml +1 -1
  13. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/LICENSE +0 -0
  14. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/README.md +0 -0
  15. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/__init__.py +0 -0
  16. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/_errors.py +0 -0
  17. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/_pydantic/__init__.py +0 -0
  18. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/_pydantic/_compat.py +0 -0
  19. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/_pydantic/_model_config.py +0 -0
  20. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/__init__.py +0 -0
  21. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/_cache.py +0 -0
  22. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/_types.py +0 -0
  23. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/base.py +0 -0
  24. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/choice.py +0 -0
  25. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/choice_base.py +0 -0
  26. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/chunks.py +0 -0
  27. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/enums.py +0 -0
  28. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/form.py +0 -0
  29. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/function_call.py +0 -0
  30. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/function_tool_call.py +0 -0
  31. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/request.py +0 -0
  32. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/response.py +0 -0
  33. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/chat_completion/stage.py +0 -0
  34. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/deployment/__init__.py +0 -0
  35. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/deployment/_headers.py +0 -0
  36. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/deployment/configuration.py +0 -0
  37. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/deployment/from_request_mixin.py +0 -0
  38. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/deployment/rate.py +0 -0
  39. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/deployment/tokenize.py +0 -0
  40. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/deployment/truncate_prompt.py +0 -0
  41. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/embeddings/__init__.py +0 -0
  42. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/embeddings/base.py +0 -0
  43. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/embeddings/request.py +0 -0
  44. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/embeddings/response.py +0 -0
  45. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/exceptions.py +0 -0
  46. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/header_propagator.py +0 -0
  47. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/py.typed +0 -0
  48. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/pydantic/v2.py +0 -0
  49. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/pydantic_v1/__init__.py +0 -0
  50. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/response_headers.py +0 -0
  51. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/telemetry/__init__.py +0 -0
  52. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/telemetry/_context.py +0 -0
  53. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/telemetry/init.py +0 -0
  54. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/telemetry/types.py +0 -0
  55. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/__init__.py +0 -0
  56. {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
  57. {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
  58. {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
  59. {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
  60. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/settings.py +0 -0
  61. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/sse_stream.py +0 -0
  62. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/translators/messages_to_chat_completions/translation_state.py +0 -0
  63. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/__init__.py +0 -0
  64. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_attachment.py +0 -0
  65. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_cancel_scope.py +0 -0
  66. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_content_stream.py +0 -0
  67. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_disconnect_middleware.py +0 -0
  68. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_indexed_list.py +0 -0
  69. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_interpolation.py +0 -0
  70. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_json_log_formatter.py +0 -0
  71. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_logging.py +0 -0
  72. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_logging_middleware.py +0 -0
  73. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/_reflection.py +0 -0
  74. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/env.py +0 -0
  75. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/errors.py +0 -0
  76. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/json.py +0 -0
  77. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/log_config.py +0 -0
  78. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/logging.py +0 -0
  79. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/merge_chunks.py +0 -0
  80. {aidial_sdk-0.48.0.dev4 → aidial_sdk-0.48.0.dev6}/aidial_sdk/utils/pydantic.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aidial-sdk
3
- Version: 0.48.0.dev4
3
+ Version: 0.48.0.dev6
4
4
  Summary: Framework to create applications and model adapters for AI DIAL
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -0,0 +1,3 @@
1
+ from aidial_sdk.utils.env import env_float
2
+
3
+ DIAL_SDK_SSE_HEARTBEAT_INTERVAL = env_float("DIAL_SDK_SSE_HEARTBEAT_INTERVAL")
@@ -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 env_float, env_var_list
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
- add_heartbeat,
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
- if heartbeat_interval:
411
- stream = add_heartbeat(
412
- stream,
413
- heartbeat_interval=heartbeat_interval,
414
- heartbeat_object=": heartbeat\n\n",
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(stream),
417
+ await to_streaming_response(sse_stream),
419
418
  media_type="text/event-stream",
420
419
  )
421
420
  else:
@@ -0,0 +1,5 @@
1
+ class Heartbeat:
2
+ """Yielded by a chat stream for each heartbeat interval it was idle."""
3
+
4
+
5
+ HEARTBEAT = Heartbeat()
@@ -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 _chunks(
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 stream:
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 _chunks(stream)
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(client, core_headers(request))
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.dial_extensions import (
29
- CustomContent,
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
- extras = parse_extras(message.model_extra)
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
- custom_content: CustomContent | None,
99
+ message: ChatCompletionMessage,
104
100
  ) -> ThinkingBlock | None:
105
- if block := get_claude_thinking_block(custom_content):
106
- return ThinkingBlock(
107
- type="thinking",
108
- thinking=block.thinking or "",
109
- signature=block.signature or "",
110
- )
111
- if text := get_dial_reasoning_content(custom_content):
112
- return ThinkingBlock(type="thinking", thinking=text, signature="")
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._reasoning_stages: set[int | None] = set()
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
- yield from self._on_custom_content(extras.custom_content)
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.dev4"
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"