unique_toolkit 2026.44.0.dev9__py3-none-any.whl → 2026.46.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,6 +35,9 @@ from unique_toolkit.language_model.constants import (
35
35
  DEFAULT_COMPLETE_TEMPERATURE,
36
36
  DEFAULT_COMPLETE_TIMEOUT,
37
37
  )
38
+ from unique_toolkit.language_model.errors import (
39
+ _reraise_as_language_model_call_error,
40
+ )
38
41
  from unique_toolkit.language_model.functions import (
39
42
  _prepare_all_completions_params_util,
40
43
  )
@@ -954,7 +957,7 @@ def stream_complete_with_references(
954
957
  return LanguageModelStreamResponse(**response)
955
958
  except Exception as e:
956
959
  _LOGGER.error(f"Error streaming completion: {e}")
957
- raise e
960
+ _reraise_as_language_model_call_error(e)
958
961
 
959
962
 
960
963
  @deprecated("Use stream_complete_with_references_async instead")
@@ -1051,7 +1054,7 @@ async def stream_complete_with_references_async(
1051
1054
  return LanguageModelStreamResponse(**response)
1052
1055
  except Exception as e:
1053
1056
  _LOGGER.error(f"Error streaming completion: {e}")
1054
- raise e
1057
+ _reraise_as_language_model_call_error(e)
1055
1058
 
1056
1059
 
1057
1060
  def _get_model_dump_or_none(model: BaseModel | None) -> dict[str, Any] | None:
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  import logging
3
3
  import random
4
- from collections.abc import Awaitable, Callable
4
+ from collections.abc import Awaitable, Callable, Mapping
5
5
  from typing import Any, NamedTuple, Sequence
6
6
 
7
7
  import unique_sdk
@@ -30,6 +30,9 @@ from unique_toolkit.content.schemas import ContentChunk
30
30
  from unique_toolkit.language_model.constants import (
31
31
  DEFAULT_COMPLETE_TEMPERATURE,
32
32
  )
33
+ from unique_toolkit.language_model.errors import (
34
+ _reraise_as_language_model_call_error,
35
+ )
33
36
  from unique_toolkit.language_model.functions import (
34
37
  SearchContext,
35
38
  _to_search_context,
@@ -407,8 +410,26 @@ class RateLimitRetryConfig(BaseSettings):
407
410
  rate_limit_retry_config = RateLimitRetryConfig()
408
411
 
409
412
 
413
+ def _retry_after_seconds(exc: BaseException | None) -> float | None:
414
+ params = getattr(exc, "params", None)
415
+ if not isinstance(params, Mapping):
416
+ return None
417
+ retry_after = params.get("retryAfterSeconds")
418
+ if (
419
+ isinstance(retry_after, (int, float))
420
+ and not isinstance(retry_after, bool)
421
+ and retry_after >= 0
422
+ ):
423
+ return float(retry_after)
424
+ return None
425
+
426
+
410
427
  def _rate_limit_wait(retry_state: RetryCallState) -> float:
411
- """Exponential backoff with up to 10% jitter: initial_delay → 2x → 4x."""
428
+ """Wait ``retryAfterSeconds`` when the API sent one, else exponential backoff."""
429
+ exc = retry_state.outcome.exception() if retry_state.outcome else None
430
+ retry_after = _retry_after_seconds(exc)
431
+ if retry_after is not None:
432
+ return retry_after
412
433
  n = retry_state.attempt_number - 1 # 0-indexed
413
434
  base = rate_limit_retry_config.initial_delay_seconds * (2.0**n)
414
435
  return base + random.uniform(0.0, base * 0.1)
@@ -421,9 +442,8 @@ async def _responses_stream_with_rate_limit_retry(
421
442
  ) -> "unique_sdk.Integrated.ResponsesStreamResult":
422
443
  """Wrap responses_stream_async with exponential backoff for rate-limit errors.
423
444
 
424
- The SDK already retries 3× (1s/2s/4s) before raising. This layer adds further
425
- retries with longer delays, which are needed for models with tighter RPM limits
426
- (e.g. GPT-4o) when large-token requests (web search, code execution) are in the loop.
445
+ The SDK does not retry 429s. This layer retries a retryable ``RateLimitError``,
446
+ waiting ``params.retryAfterSeconds`` when the API sent one.
427
447
 
428
448
  Args:
429
449
  responses_args: Keyword arguments forwarded to responses_stream_async.
@@ -434,6 +454,11 @@ async def _responses_stream_with_rate_limit_retry(
434
454
  """
435
455
 
436
456
  def _is_rate_limit(exc: BaseException) -> bool:
457
+ if isinstance(exc, unique_sdk.RateLimitError):
458
+ params = getattr(exc, "params", None)
459
+ if isinstance(params, Mapping) and params.get("retryable") is False:
460
+ return False
461
+ return True
437
462
  return isinstance(exc, unique_sdk.APIError) and any(
438
463
  kw in str(exc).lower() for kw in _RATE_LIMIT_KEYWORDS
439
464
  )
@@ -539,11 +564,11 @@ def stream_responses_with_references(
539
564
  other_options=other_options,
540
565
  )
541
566
 
542
- return ResponsesLanguageModelStreamResponse.model_validate(
543
- unique_sdk.Integrated.responses_stream(
544
- **responses_args,
545
- )
546
- )
567
+ try:
568
+ streamed = unique_sdk.Integrated.responses_stream(**responses_args)
569
+ except unique_sdk.UniqueError as error:
570
+ _reraise_as_language_model_call_error(error)
571
+ return ResponsesLanguageModelStreamResponse.model_validate(streamed)
547
572
 
548
573
 
549
574
  async def stream_responses_with_references_async(
@@ -608,10 +633,12 @@ async def stream_responses_with_references_async(
608
633
  "Calling responses_stream_async model=%s",
609
634
  responses_params.model_name,
610
635
  )
611
- return ResponsesLanguageModelStreamResponse.model_validate(
612
- await _responses_stream_with_rate_limit_retry(
636
+ try:
637
+ streamed = await _responses_stream_with_rate_limit_retry(
613
638
  responses_args=responses_args,
614
639
  model_name=responses_params.model_name,
615
640
  on_rate_limit_retry=on_rate_limit_retry,
616
641
  )
617
- )
642
+ except unique_sdk.UniqueError as error:
643
+ _reraise_as_language_model_call_error(error)
644
+ return ResponsesLanguageModelStreamResponse.model_validate(streamed)
@@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Any
2
2
 
3
3
  from .constants import DOMAIN_NAME
4
4
  from .default_language_model import DEFAULT_LANGUAGE_MODEL, DEFAULT_GPT_4o
5
+ from .errors import LanguageModelCallError as LanguageModelCallError
5
6
  from .functions import (
6
7
  stream_complete_with_references_openai as stream_complete_with_references_openai,
7
8
  )
@@ -59,6 +60,7 @@ def __getattr__(name: str) -> Any:
59
60
 
60
61
  __all__ = [
61
62
  "LanguageModel",
63
+ "LanguageModelCallError",
62
64
  "LanguageModelName",
63
65
  "Prompt",
64
66
  "LanguageModelAssistantMessage",
@@ -0,0 +1,90 @@
1
+ from collections.abc import Mapping
2
+ from typing import NoReturn
3
+
4
+ import unique_sdk
5
+
6
+
7
+ class LanguageModelCallError(unique_sdk.APIError):
8
+ """LLM call rejected with node-chat's error contract.
9
+
10
+ ``user_message`` is the text the API curated for the user. Display that
11
+ field. ``str(self)`` is the same text, without the SDK's
12
+ ``(Original error)`` suffix.
13
+
14
+ It subclasses ``APIError`` so callers that catch the SDK error around
15
+ toolkit calls keep catching it.
16
+
17
+ Args:
18
+ code (str): ``error.code`` from the API, for example ``content_filter``.
19
+ user_message (str): Text to show the user.
20
+ retryable (bool): Whether the API marked the call safe to retry.
21
+ correlation_id (str | None): Request correlation id, when the API sent one.
22
+ http_status (int | None): HTTP status of the failed call.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ *,
28
+ code: str,
29
+ user_message: str,
30
+ retryable: bool,
31
+ correlation_id: str | None,
32
+ http_status: int | None = None,
33
+ ) -> None:
34
+ super().__init__(message=user_message, http_status=http_status, code=code)
35
+ self.user_message = user_message
36
+ self.retryable = retryable
37
+ self.correlation_id = correlation_id
38
+
39
+
40
+ def _language_model_call_error_from(
41
+ error: unique_sdk.UniqueError,
42
+ ) -> LanguageModelCallError | None:
43
+ """Build a ``LanguageModelCallError`` when ``error`` carries the contract.
44
+
45
+ Returns None for legacy SDK errors that have no ``code`` or no boolean
46
+ ``params.retryable``. Callers re-raise those unchanged.
47
+
48
+ Args:
49
+ error (unique_sdk.UniqueError): Error raised by ``unique_sdk``.
50
+
51
+ Returns:
52
+ LanguageModelCallError | None: The converted error, or None.
53
+ """
54
+ if not isinstance(error.code, str) or error.code == "":
55
+ return None
56
+ params = getattr(error, "params", None)
57
+ if not isinstance(params, Mapping):
58
+ return None
59
+ retryable = params.get("retryable")
60
+ if not isinstance(retryable, bool):
61
+ return None
62
+ user_message = error.args[0] if error.args else None
63
+ if not isinstance(user_message, str) or user_message == "":
64
+ return None
65
+ correlation_id = params.get("correlationId")
66
+ if not isinstance(correlation_id, str):
67
+ correlation_id = None
68
+ return LanguageModelCallError(
69
+ code=error.code,
70
+ user_message=user_message,
71
+ retryable=retryable,
72
+ correlation_id=correlation_id,
73
+ http_status=error.http_status,
74
+ )
75
+
76
+
77
+ def _reraise_as_language_model_call_error(error: BaseException) -> NoReturn:
78
+ """Raise ``LanguageModelCallError`` for a contract error, else re-raise ``error``.
79
+
80
+ The SDK wraps the last error in ``APIError("Failed after N attempts")`` once
81
+ retries run out, so the contract is also read from ``error.__cause__``.
82
+ """
83
+ if isinstance(error, LanguageModelCallError):
84
+ raise error
85
+ for candidate in (error, error.__cause__):
86
+ if isinstance(candidate, unique_sdk.UniqueError):
87
+ converted = _language_model_call_error_from(candidate)
88
+ if converted is not None:
89
+ raise converted from error
90
+ raise error
@@ -16,6 +16,9 @@ from pydantic import BaseModel
16
16
 
17
17
  from unique_toolkit.chat.schemas import ChatMessage, ChatMessageRole
18
18
  from unique_toolkit.content.schemas import ContentChunk, ContentReference
19
+ from unique_toolkit.language_model.errors import (
20
+ _reraise_as_language_model_call_error,
21
+ )
19
22
  from unique_toolkit.language_model.infos import (
20
23
  LanguageModelInfo,
21
24
  LanguageModelName,
@@ -133,7 +136,7 @@ def complete(
133
136
  return LanguageModelResponse(**response)
134
137
  except Exception as e:
135
138
  logger.error(f"Error completing: {e}")
136
- raise e
139
+ _reraise_as_language_model_call_error(e)
137
140
 
138
141
 
139
142
  async def complete_async(
@@ -173,8 +176,8 @@ async def complete_async(
173
176
 
174
177
  Raises:
175
178
  ------
176
- Exception: If an error occurs during the request, an exception is raised
177
- and logged.
179
+ LanguageModelCallError: The API returned the LLM error contract.
180
+ Exception: Any other request failure is logged and re-raised.
178
181
 
179
182
  """
180
183
  if not user_id:
@@ -217,7 +220,7 @@ async def complete_async(
217
220
  return LanguageModelResponse(**response)
218
221
  except Exception as e:
219
222
  logger.exception(f"Error completing: {e}")
220
- raise e
223
+ _reraise_as_language_model_call_error(e)
221
224
 
222
225
 
223
226
  def _add_tools_to_options(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-toolkit
3
- Version: 2026.44.0.dev9
3
+ Version: 2026.46.0.dev0
4
4
  Summary:
5
5
  Author: Cedric Klinkert, Martin Fadler, Sadique Sheik, Fabian Schläpfer, Pascal Hauri, Ejiroghene Oki-Peter, Rami Azouz, Ricardo Visini, Sanzio Monti
6
6
  Author-email: Cedric Klinkert <cedric.klinkert@unique.ch>, Martin Fadler <martin.fadler@unique.ch>, Sadique Sheik <sadique@unique.ch>, Fabian Schläpfer <fabian@unique.ch>, Pascal Hauri <pascal@unique.ch>, Ejiroghene Oki-Peter <oki-peter.ejiroghene@unique.ai>, Rami Azouz <rami.ext@unique.ch>, Ricardo Visini <ricardo.visini@unique.ch>, Sanzio Monti <sanzio.monti@unique.ai>
@@ -16,7 +16,7 @@ Requires-Dist: sseclient>=0.0.27,<0.0.28
16
16
  Requires-Dist: openai>=1.105.0,<3
17
17
  Requires-Dist: platformdirs>=4.0.0,<5
18
18
  Requires-Dist: pillow>=12.1.1
19
- Requires-Dist: unique-sdk>=2026.44.0.dev13,<2026.44.0rc0
19
+ Requires-Dist: unique-sdk>=2026.46.0.dev0,<2026.46.0rc0
20
20
  Requires-Dist: jambo>=0.1.2,<0.2
21
21
  Requires-Dist: jinja2>=3.1.6,<4
22
22
  Requires-Dist: docxtpl>=0.20.1,<0.21
@@ -264,9 +264,9 @@ unique_toolkit/chat/cancellation.py,sha256=k5HMjpeBmofIiInwq5TjOR_5k91oq1bWG00lJ
264
264
  unique_toolkit/chat/constants.py,sha256=05kq6zjqUVB2d6_P7s-90nbljpB3ryxwCI-CAz0r2O4,83
265
265
  unique_toolkit/chat/deprecated/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
266
266
  unique_toolkit/chat/deprecated/service.py,sha256=8_XoopUWHT396Z_5gwMmsI6v4NGVgQirSJCYUW4Gx8c,9339
267
- unique_toolkit/chat/functions.py,sha256=-XNlQHO4UAdDwwnZUXFUAlV-lKh-7oqyJO6ZKyr_4Jc,57846
267
+ unique_toolkit/chat/functions.py,sha256=XCAErF-gRBTETx2QW7EpOyGpzI1QUh_7oMKVPgDnaG0,58008
268
268
  unique_toolkit/chat/rendering.py,sha256=c8YiV9oADRrJQ5A_QBJ4_UFc0NZ-2vVaa7tupoMusso,880
269
- unique_toolkit/chat/responses_api.py,sha256=Swp6wi10vCPkBiSSj8NN58gpjTgYVkqRlwdOJRQHVjo,21135
269
+ unique_toolkit/chat/responses_api.py,sha256=8RBErUBtIvjBW2ZP644g7LqtAGiAVZlZ2ahh6tXfrbQ,22179
270
270
  unique_toolkit/chat/schemas.py,sha256=tRU8vcT1SQWtktkHQmDO26CNbOgSynFHfFB9FR6D4OQ,8372
271
271
  unique_toolkit/chat/service.py,sha256=6D00OL4QrGafbOhTaC5zNXaNgg7gS5W_2ePVa4LhqpE,4439
272
272
  unique_toolkit/chat/state.py,sha256=Cjgwv_2vhDFbV69xxsn7SefhaoIAEqLx3ferdVFCnOg,1445
@@ -392,12 +392,13 @@ unique_toolkit/framework_utilities/openai/__init__.py,sha256=CrHYoC7lv2pBscitLer
392
392
  unique_toolkit/framework_utilities/openai/client.py,sha256=Ft-oLiS6BTaMiuCdObdaMLkiEPE3RYjtc9VPnqQk3W8,2449
393
393
  unique_toolkit/framework_utilities/openai/message_builder.py,sha256=RT1pZjxH42TFZlAxQ5zlqdKPvHKVTjc5t3JDUy58I7Q,6887
394
394
  unique_toolkit/framework_utilities/utils.py,sha256=JK7g2yMfEx3eMprug26769xqNpS5WJcizf8n2zWMBng,789
395
- unique_toolkit/language_model/__init__.py,sha256=dr1O-m9irgQi2RxNz5g6sykhu9E2Z8-cs-_MMAD62WU,2711
395
+ unique_toolkit/language_model/__init__.py,sha256=mgfm7S-BK4pZtz1dXykmfKAKtIBIDS2atiXtUIIioUg,2810
396
396
  unique_toolkit/language_model/_responses_api_utils.py,sha256=w2ZyleOQcWLpOWGJNNb2-uBYApUNlqS-ABqXoMFySlg,3084
397
397
  unique_toolkit/language_model/builder.py,sha256=KwBzsD6-fvQnB4p2I7M-5Tk6j4tmQ973MYwU1ulXFb4,4526
398
398
  unique_toolkit/language_model/constants.py,sha256=B-topqW0r83dkC_25DeQfnPk3n53qzIHUCBS7YJ0-1U,119
399
399
  unique_toolkit/language_model/default_language_model.py,sha256=mcyuFs_V6tp52C8xJSHiEHO4kFGj6xed1gvozvCRD7k,1181
400
- unique_toolkit/language_model/functions.py,sha256=sjuzKe6xAq6NFwwgIRvgAuU8t6apq1tcdGSkcYCl8EQ,27905
400
+ unique_toolkit/language_model/errors.py,sha256=0nm4psGCGJM2B7LIyKXy7WuswRzwARIgIK5xoC4xo34,3186
401
+ unique_toolkit/language_model/functions.py,sha256=6Le6dDm9su6xk3sQ8IPiyUn2tf1qvzxHVUhyTTgP8Kw,28109
401
402
  unique_toolkit/language_model/infos.py,sha256=_fbEwGA9AjBm7TF1FTsihtKzi4SybTwVN7QyHRacMrQ,183899
402
403
  unique_toolkit/language_model/invocation_stats.py,sha256=CQbpUz394IsVd8eNTB1gPSMt5f0UXmkN1amNsqtAW6U,5857
403
404
  unique_toolkit/language_model/model_costs.py,sha256=ShuDAzMew1fUkJ3tzPgU5Zqjp6ivUZaG97BFh93tEn4,6358
@@ -428,6 +429,6 @@ unique_toolkit/short_term_memory/service.py,sha256=2JMBEBn6GoyK-a0xvdcWpPft-3C74
428
429
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
429
430
  unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
430
431
  unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
431
- unique_toolkit-2026.44.0.dev9.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
432
- unique_toolkit-2026.44.0.dev9.dist-info/METADATA,sha256=g0ZlAxgIaNzapDN-UIvQYQ8S16AJWjJlofHlc5yzSm8,7822
433
- unique_toolkit-2026.44.0.dev9.dist-info/RECORD,,
432
+ unique_toolkit-2026.46.0.dev0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
433
+ unique_toolkit-2026.46.0.dev0.dist-info/METADATA,sha256=yT51EF64Rcbqx5fwB5-Is09Or6rCvCMLHmT3OswAObo,7821
434
+ unique_toolkit-2026.46.0.dev0.dist-info/RECORD,,