aidial-sdk 0.47.0.dev10__tar.gz → 0.48.0.dev2__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 (78) hide show
  1. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/PKG-INFO +1 -1
  2. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/application.py +53 -30
  3. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/translators/messages_to_chat_completions/__init__.py +12 -5
  4. aidial_sdk-0.47.0.dev10/aidial_sdk/translators/messages_to_chat_completions/core_client.py → aidial_sdk-0.48.0.dev2/aidial_sdk/translators/messages_to_chat_completions/backend/http.py +29 -1
  5. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/app.py +32 -42
  6. aidial_sdk-0.48.0.dev2/aidial_sdk/utils/_interpolation.py +55 -0
  7. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/pyproject.toml +1 -2
  8. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/LICENSE +0 -0
  9. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/README.md +0 -0
  10. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/__init__.py +0 -0
  11. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/_errors.py +0 -0
  12. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/_pydantic/__init__.py +0 -0
  13. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/_pydantic/_compat.py +0 -0
  14. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/_pydantic/_model_config.py +0 -0
  15. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/chat_completion/__init__.py +0 -0
  16. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/chat_completion/_cache.py +0 -0
  17. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/chat_completion/_types.py +0 -0
  18. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/chat_completion/base.py +0 -0
  19. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/chat_completion/choice.py +0 -0
  20. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/chat_completion/choice_base.py +0 -0
  21. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/chat_completion/chunks.py +0 -0
  22. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/chat_completion/enums.py +0 -0
  23. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/chat_completion/form.py +0 -0
  24. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/chat_completion/function_call.py +0 -0
  25. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/chat_completion/function_tool_call.py +0 -0
  26. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/chat_completion/request.py +0 -0
  27. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/chat_completion/response.py +0 -0
  28. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/chat_completion/stage.py +0 -0
  29. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/deployment/__init__.py +0 -0
  30. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/deployment/_headers.py +0 -0
  31. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/deployment/configuration.py +0 -0
  32. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/deployment/from_request_mixin.py +0 -0
  33. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/deployment/rate.py +0 -0
  34. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/deployment/tokenize.py +0 -0
  35. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/deployment/truncate_prompt.py +0 -0
  36. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/embeddings/__init__.py +0 -0
  37. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/embeddings/base.py +0 -0
  38. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/embeddings/request.py +0 -0
  39. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/embeddings/response.py +0 -0
  40. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/exceptions.py +0 -0
  41. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/header_propagator.py +0 -0
  42. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/py.typed +0 -0
  43. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/pydantic/v2.py +0 -0
  44. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/pydantic_v1/__init__.py +0 -0
  45. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/response_headers.py +0 -0
  46. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/telemetry/__init__.py +0 -0
  47. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/telemetry/_context.py +0 -0
  48. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/telemetry/init.py +0 -0
  49. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/telemetry/types.py +0 -0
  50. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/cache_breakpoints.py +0 -0
  51. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/dial_extensions.py +0 -0
  52. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/from_chat_completions.py +0 -0
  53. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/reasoning.py +0 -0
  54. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/streaming.py +0 -0
  55. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/system_turns.py +0 -0
  56. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/translators/messages_to_chat_completions/chat_completions/to_chat_completions.py +0 -0
  57. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/translators/messages_to_chat_completions/common.py +0 -0
  58. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/translators/messages_to_chat_completions/errors.py +0 -0
  59. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/translators/messages_to_chat_completions/settings.py +0 -0
  60. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/translators/messages_to_chat_completions/sse_stream.py +0 -0
  61. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/translators/messages_to_chat_completions/translation_state.py +0 -0
  62. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/__init__.py +0 -0
  63. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/_attachment.py +0 -0
  64. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/_cancel_scope.py +0 -0
  65. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/_content_stream.py +0 -0
  66. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/_disconnect_middleware.py +0 -0
  67. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/_indexed_list.py +0 -0
  68. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/_json_log_formatter.py +0 -0
  69. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/_logging.py +0 -0
  70. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/_reflection.py +0 -0
  71. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/env.py +0 -0
  72. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/errors.py +0 -0
  73. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/json.py +0 -0
  74. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/log_config.py +0 -0
  75. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/logging.py +0 -0
  76. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/merge_chunks.py +0 -0
  77. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/pydantic.py +0 -0
  78. {aidial_sdk-0.47.0.dev10 → aidial_sdk-0.48.0.dev2}/aidial_sdk/utils/streaming.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aidial-sdk
3
- Version: 0.47.0.dev10
3
+ Version: 0.48.0.dev2
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
@@ -37,6 +37,10 @@ from aidial_sdk.header_propagator import HeaderPropagator
37
37
  from aidial_sdk.response_headers import NonForwardableHeadersMiddleware
38
38
  from aidial_sdk.telemetry.types import TelemetryConfig
39
39
  from aidial_sdk.utils._disconnect_middleware import DisconnectMiddleware
40
+ from aidial_sdk.utils._interpolation import (
41
+ interpolate_deployment_id,
42
+ match_deployment_id,
43
+ )
40
44
  from aidial_sdk.utils._reflection import get_method_implementation
41
45
  from aidial_sdk.utils.env import env_float, env_var_list
42
46
  from aidial_sdk.utils.log_config import configure_sdk_logger
@@ -59,16 +63,6 @@ RequestType = TypeVar("RequestType", bound=FromRequestMixin)
59
63
  Handler = Callable[[Request], Coroutine[Any, Any, Response]]
60
64
 
61
65
 
62
- def _interpolate_deployment_id(deployment_id: str, path_params: dict) -> str:
63
- result = deployment_id
64
- for key, value in path_params.items():
65
- # Replace {key} or {key:type} patterns with actual value
66
- # Pattern matches {key} or {key:int}, {key:path}, etc.
67
- pattern = r"\{" + re.escape(key) + r"(?::[^}]*)?\}"
68
- result = re.sub(pattern, str(value), result)
69
- return result
70
-
71
-
72
66
  class PathFilter(logging.Filter):
73
67
  path: str
74
68
 
@@ -83,7 +77,20 @@ class PathFilter(logging.Filter):
83
77
  class DIALApp(FastAPI):
84
78
  _allow_extra_request_fields: bool
85
79
  _dial_url: str | None
86
- _v1_handlers: dict[str, dict[str, Handler]]
80
+
81
+ _generic_handlers: dict[str, dict[str, Handler]]
82
+ """
83
+ Request handlers for endpoints that do not allow to distinguish deployments in the path.
84
+ A dictionary from such a deployment-id-less endpoint to the handlers for each deployment:
85
+
86
+ /openai/v1/chat/completions:
87
+ gpt-5.4: handler1
88
+ gpt-5.5: handler2
89
+ /openai/v1/embeddings:
90
+ ada-embeddings-2024-10-10: handler3
91
+ ...
92
+ """
93
+
87
94
  _reset_otel_context: Callable[[], AbstractContextManager[None]]
88
95
 
89
96
  def __init__(
@@ -110,7 +117,7 @@ class DIALApp(FastAPI):
110
117
 
111
118
  self._allow_extra_request_fields = allow_extra_request_fields
112
119
  self._dial_url = dial_url
113
- self._v1_handlers = {}
120
+ self._generic_handlers = {}
114
121
  self._reset_otel_context = nullcontext
115
122
 
116
123
  self.configure_telemetry(telemetry_config)
@@ -181,9 +188,9 @@ class DIALApp(FastAPI):
181
188
  methods=["POST"],
182
189
  )
183
190
 
184
- self._add_v1_route(
191
+ self._add_generic_route(
185
192
  deployment_name,
186
- "embeddings",
193
+ "/openai/v1/embeddings",
187
194
  self._embeddings(None, impl),
188
195
  methods=["POST"],
189
196
  )
@@ -209,8 +216,11 @@ class DIALApp(FastAPI):
209
216
  for endpoint, handler, methods in self._chat_completion_handlers(
210
217
  None, impl, heartbeat_interval
211
218
  ):
212
- self._add_v1_route(
213
- deployment_name, endpoint, handler, methods=methods
219
+ self._add_generic_route(
220
+ deployment_name,
221
+ f"/openai/v1/{endpoint}",
222
+ handler,
223
+ methods=methods,
214
224
  )
215
225
 
216
226
  return self
@@ -264,7 +274,7 @@ class DIALApp(FastAPI):
264
274
  ["GET"],
265
275
  )
266
276
 
267
- def _add_v1_route(
277
+ def _add_generic_route(
268
278
  self,
269
279
  deployment_name: str,
270
280
  endpoint: str,
@@ -272,28 +282,25 @@ class DIALApp(FastAPI):
272
282
  *,
273
283
  methods: list[str],
274
284
  ) -> None:
275
- """The /openai/v1 endpoints are shared by all the deployments of
276
- the application, so they are dispatched by the deployment id
277
- resolved from the request headers."""
278
-
279
- if endpoint not in self._v1_handlers:
280
- self._v1_handlers[endpoint] = {}
285
+ if endpoint not in self._generic_handlers:
286
+ self._generic_handlers[endpoint] = {}
281
287
  self.add_api_route(
282
- f"/openai/v1/{endpoint}",
283
- self._v1_dispatcher(endpoint),
288
+ endpoint,
289
+ self._generic_dispatcher(endpoint),
284
290
  methods=methods,
285
291
  )
292
+ self._generic_handlers[endpoint][deployment_name] = handler
286
293
 
287
- self._v1_handlers[endpoint][deployment_name] = handler
288
-
289
- def _v1_dispatcher(self, endpoint: str) -> Handler:
294
+ def _generic_dispatcher(self, endpoint: str) -> Handler:
290
295
  async def _handler(original_request: Request) -> Response:
291
296
  deployment_id = get_upstream_deployment_id(
292
297
  original_request.headers, None
293
298
  )
294
299
  set_log_deployment(deployment_id)
295
300
 
296
- handler = self._v1_handlers[endpoint].get(deployment_id)
301
+ handler = self._get_handler(
302
+ endpoint, deployment_id, original_request
303
+ )
297
304
  if handler is None:
298
305
  raise DeploymentNotFoundError(
299
306
  f"The deployment {deployment_id!r} doesn't provide the {endpoint!r} endpoint"
@@ -303,6 +310,22 @@ class DIALApp(FastAPI):
303
310
 
304
311
  return _handler
305
312
 
313
+ def _get_handler(
314
+ self, endpoint: str, deployment_id: str, request: Request
315
+ ) -> Handler | None:
316
+ handlers = self._generic_handlers[endpoint]
317
+
318
+ if (handler := handlers.get(deployment_id)) is not None:
319
+ return handler
320
+
321
+ for name, handler in handlers.items():
322
+ params = match_deployment_id(name, deployment_id)
323
+ if params is not None:
324
+ request.scope["path_params"] = params
325
+ return handler
326
+
327
+ return None
328
+
306
329
  def _endpoint_factory(
307
330
  self,
308
331
  deployment_id: str | None,
@@ -351,7 +374,7 @@ class DIALApp(FastAPI):
351
374
  deployment_id: str | None,
352
375
  ) -> RequestType:
353
376
  if deployment_id is not None:
354
- deployment_id = _interpolate_deployment_id(
377
+ deployment_id = interpolate_deployment_id(
355
378
  deployment_id, original_request.path_params
356
379
  )
357
380
  set_log_deployment(deployment_id)
@@ -1,4 +1,3 @@
1
- import httpx
2
1
  from fastapi import FastAPI
3
2
 
4
3
  from aidial_sdk._pydantic import PYDANTIC_V2
@@ -9,10 +8,18 @@ if not PYDANTIC_V2:
9
8
  "install pydantic>=2 and set PYDANTIC_V2=True"
10
9
  )
11
10
 
12
- from aidial_sdk.translators.messages_to_chat_completions.chat_completions.app import ( # noqa: E402
13
- ClientFactory,
14
- create_anthropic_api_app,
15
- )
11
+ try:
12
+ import httpx
13
+
14
+ from aidial_sdk.translators.messages_to_chat_completions.chat_completions.app import (
15
+ ClientFactory,
16
+ create_anthropic_api_app,
17
+ )
18
+ except ModuleNotFoundError as e:
19
+ raise ImportError(
20
+ "The Anthropic Messages API translator requires the translators extra: "
21
+ 'pip install "aidial-sdk[translators]"'
22
+ ) from e
16
23
 
17
24
  __all__ = ["ClientFactory", "create_anthropic_api_app", "mount_translator"]
18
25
 
@@ -1,8 +1,10 @@
1
+ from dataclasses import dataclass
1
2
  from urllib.parse import quote
2
3
 
3
4
  import httpx
4
5
  from fastapi import Request
5
- from openai import AsyncOpenAI, Omit, omit
6
+ from openai import AsyncOpenAI, AsyncStream, Omit, omit
7
+ from openai.types.chat import ChatCompletion, ChatCompletionChunk
6
8
 
7
9
  from aidial_sdk.translators.messages_to_chat_completions.settings import (
8
10
  get_dial_api_version,
@@ -62,3 +64,29 @@ def core_headers(request: Request) -> dict[str, str | Omit]:
62
64
  }
63
65
  )
64
66
  return headers
67
+
68
+
69
+ @dataclass
70
+ class ChatClientHttp:
71
+ client: AsyncOpenAI
72
+ core_headers: dict[str, str | Omit]
73
+
74
+ async def create_stream(
75
+ self, body: dict
76
+ ) -> AsyncStream[ChatCompletionChunk]:
77
+ return await self.client.post(
78
+ "/chat/completions",
79
+ cast_to=ChatCompletion,
80
+ body=body,
81
+ options={"headers": self.core_headers},
82
+ stream=True,
83
+ stream_cls=AsyncStream[ChatCompletionChunk],
84
+ )
85
+
86
+ async def create_block(self, body: dict) -> ChatCompletion:
87
+ return await self.client.post(
88
+ "/chat/completions",
89
+ cast_to=ChatCompletion,
90
+ body=body,
91
+ options={"headers": self.core_headers},
92
+ )
@@ -7,9 +7,14 @@ import httpx
7
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
- from openai import AsyncStream
11
- from openai.types.chat import ChatCompletion, ChatCompletionChunk
12
10
 
11
+ from aidial_sdk.deployment.from_request_mixin import get_client_deployment_id
12
+ from aidial_sdk.translators.messages_to_chat_completions.backend.http import (
13
+ ChatClientHttp,
14
+ core_headers,
15
+ core_legacy_client,
16
+ core_v1_client,
17
+ )
13
18
  from aidial_sdk.translators.messages_to_chat_completions.chat_completions.from_chat_completions import (
14
19
  to_anthropic_response,
15
20
  )
@@ -23,11 +28,6 @@ from aidial_sdk.translators.messages_to_chat_completions.common import (
23
28
  not_found,
24
29
  with_debug_logging,
25
30
  )
26
- from aidial_sdk.translators.messages_to_chat_completions.core_client import (
27
- core_headers,
28
- core_legacy_client,
29
- core_v1_client,
30
- )
31
31
  from aidial_sdk.translators.messages_to_chat_completions.errors import (
32
32
  AnthropicErrorType,
33
33
  AnthropicHTTPError,
@@ -45,13 +45,10 @@ ClientT = TypeVar("ClientT")
45
45
  ClientFactory = Callable[[Request], Awaitable[ClientT]] | ClientT
46
46
 
47
47
 
48
- async def _handle_messages(
49
- request: Request,
50
- get_client: ClientFactory[httpx.AsyncClient],
51
- legacy: bool,
48
+ async def handle_anthropic_messages(
49
+ request: Request, client: ChatClientHttp
52
50
  ) -> Response:
53
- dial_url = get_dial_url()
54
- translation_state = TranslationState()
51
+ deployment = get_client_deployment_id(request.headers)
55
52
 
56
53
  try:
57
54
  req: MessageCreateParams = await request.json()
@@ -61,7 +58,7 @@ async def _handle_messages(
61
58
  f"Request body is not valid JSON: {e}",
62
59
  ) from e
63
60
 
64
- deployment = request.headers.get("x-dial-deployment-id") or req["model"]
61
+ translation_state = TranslationState()
65
62
  chat_request = to_chat_completions_request(
66
63
  req, deployment, translation_state
67
64
  )
@@ -70,38 +67,14 @@ async def _handle_messages(
70
67
  if log.isEnabledFor(logging.DEBUG):
71
68
  log.debug(f"Translated request to Core: {json.dumps(body)}")
72
69
 
73
- http_client = (
74
- get_client
75
- if isinstance(get_client, httpx.AsyncClient)
76
- else await get_client(request)
77
- )
78
- client = (
79
- core_legacy_client(dial_url, deployment, http_client)
80
- if legacy
81
- else core_v1_client(dial_url, http_client)
82
- )
83
- headers = core_headers(request)
84
-
85
70
  if chat_request.stream:
86
- events = await client.post(
87
- "/chat/completions",
88
- cast_to=ChatCompletion,
89
- body=body,
90
- options={"headers": headers},
91
- stream=True,
92
- stream_cls=AsyncStream[ChatCompletionChunk],
93
- )
71
+ events = await client.create_stream(body)
94
72
  return StreamingResponse(
95
73
  translate_stream(events, deployment, translation_state),
96
74
  media_type="text/event-stream",
97
75
  )
98
76
 
99
- response = await client.post(
100
- "/chat/completions",
101
- cast_to=ChatCompletion,
102
- body=body,
103
- options={"headers": headers},
104
- )
77
+ response = await client.create_block(body)
105
78
  message = to_anthropic_response(response, deployment, translation_state)
106
79
  return JSONResponse(
107
80
  content=message.model_dump(mode="json", exclude_unset=True)
@@ -113,17 +86,34 @@ def create_anthropic_api_app(
113
86
  ) -> FastAPI:
114
87
  app = FastAPI()
115
88
 
89
+ async def _chat_client(request: Request, *, legacy: bool) -> ChatClientHttp:
90
+ http_client = (
91
+ await get_client(request) if callable(get_client) else get_client
92
+ )
93
+ dial_url = get_dial_url()
94
+ if legacy:
95
+ deployment_id = get_client_deployment_id(request.headers)
96
+ client = core_legacy_client(dial_url, deployment_id, http_client)
97
+ else:
98
+ client = core_v1_client(dial_url, http_client)
99
+
100
+ return ChatClientHttp(client, core_headers(request))
101
+
116
102
  @app.post("/anthropic/v1/messages")
117
103
  @with_debug_logging
118
104
  @translator_error_handler
119
105
  async def _handle_v1_messages(request: Request) -> Response:
120
- return await _handle_messages(request, get_client, legacy=False)
106
+ return await handle_anthropic_messages(
107
+ request, await _chat_client(request, legacy=False)
108
+ )
121
109
 
122
110
  @app.post("/legacy/anthropic/v1/messages")
123
111
  @with_debug_logging
124
112
  @translator_error_handler
125
113
  async def _handle_legacy_messages(request: Request) -> Response:
126
- return await _handle_messages(request, get_client, legacy=True)
114
+ return await handle_anthropic_messages(
115
+ request, await _chat_client(request, legacy=True)
116
+ )
127
117
 
128
118
  app.add_api_route(
129
119
  "/{full_path:path}",
@@ -0,0 +1,55 @@
1
+ """Deployment names may be templates, e.g. "app-{idx}" or "app-{idx:int}",
2
+ in the same syntax Starlette uses for the path params."""
3
+
4
+ from collections.abc import Callable
5
+ from functools import cache
6
+ from re import Match
7
+ from typing import Any
8
+
9
+ from starlette.convertors import Convertor
10
+ from starlette.routing import compile_path
11
+
12
+
13
+ @cache
14
+ def _compile_path_template(
15
+ name: str,
16
+ ) -> tuple[Callable[[str], Match[str] | None], str, dict[str, Convertor[Any]]]:
17
+ # compile_path reads a pattern with no leading slash as a *host* pattern,
18
+ # which it truncates at ":", so the name is compiled as a path instead.
19
+ regex, path_format, convertors = compile_path("/" + name)
20
+
21
+ def matcher(deployment_id: str) -> Match[str] | None:
22
+ return regex.match("/" + deployment_id)
23
+
24
+ return matcher, path_format.removeprefix("/"), convertors
25
+
26
+
27
+ def match_deployment_id(
28
+ template: str, deployment_id: str
29
+ ) -> dict[str, Any] | None:
30
+ """Matches a deployment id against a deployment name template
31
+ (e.g. "app-{idx}"), returning the path params it resolves to,
32
+ or None if the name isn't a template or doesn't match."""
33
+
34
+ if "{" not in template:
35
+ return None
36
+
37
+ matcher, _path_format, convertors = _compile_path_template(template)
38
+
39
+ if (match := matcher(deployment_id)) is None:
40
+ return None
41
+
42
+ return {
43
+ key: convertors[key].convert(value)
44
+ for key, value in match.groupdict().items()
45
+ }
46
+
47
+
48
+ def interpolate_deployment_id(deployment_id: str, path_params: dict) -> str:
49
+ """Fills a deployment name template (e.g. "app-{idx:int}") with the
50
+ path params the request it came in on was routed by."""
51
+
52
+ _matcher, path_format, convertors = _compile_path_template(deployment_id)
53
+
54
+ # the format drops the convertor types, so "app-{idx:int}" -> "app-{idx}"
55
+ return path_format.format(**path_params) if convertors else deployment_id
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aidial-sdk"
3
- version = "0.47.0.dev10"
3
+ version = "0.48.0.dev2"
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"
@@ -52,7 +52,6 @@ aiointercept = "^0.1.7"
52
52
  requests = "^2.33"
53
53
  responses = "^0.25.3"
54
54
  httpx2 = "^2.12.0" # required for Starlette.TestClient
55
- openai = "^2.46.0" # third-party logger for log-takeover tests
56
55
 
57
56
  [tool.poetry.group.lint.dependencies]
58
57
  pyright = "1.1.385"