langgraph-api 0.4.1__py3-none-any.whl → 0.7.3__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.
Files changed (135) hide show
  1. langgraph_api/__init__.py +1 -1
  2. langgraph_api/api/__init__.py +111 -51
  3. langgraph_api/api/a2a.py +1610 -0
  4. langgraph_api/api/assistants.py +212 -89
  5. langgraph_api/api/mcp.py +3 -3
  6. langgraph_api/api/meta.py +52 -28
  7. langgraph_api/api/openapi.py +27 -17
  8. langgraph_api/api/profile.py +108 -0
  9. langgraph_api/api/runs.py +342 -195
  10. langgraph_api/api/store.py +19 -2
  11. langgraph_api/api/threads.py +209 -27
  12. langgraph_api/asgi_transport.py +14 -9
  13. langgraph_api/asyncio.py +14 -4
  14. langgraph_api/auth/custom.py +52 -37
  15. langgraph_api/auth/langsmith/backend.py +4 -3
  16. langgraph_api/auth/langsmith/client.py +13 -8
  17. langgraph_api/cli.py +230 -133
  18. langgraph_api/command.py +5 -3
  19. langgraph_api/config/__init__.py +532 -0
  20. langgraph_api/config/_parse.py +58 -0
  21. langgraph_api/config/schemas.py +431 -0
  22. langgraph_api/cron_scheduler.py +17 -1
  23. langgraph_api/encryption/__init__.py +15 -0
  24. langgraph_api/encryption/aes_json.py +158 -0
  25. langgraph_api/encryption/context.py +35 -0
  26. langgraph_api/encryption/custom.py +280 -0
  27. langgraph_api/encryption/middleware.py +632 -0
  28. langgraph_api/encryption/shared.py +63 -0
  29. langgraph_api/errors.py +12 -1
  30. langgraph_api/executor_entrypoint.py +11 -6
  31. langgraph_api/feature_flags.py +29 -0
  32. langgraph_api/graph.py +176 -76
  33. langgraph_api/grpc/client.py +313 -0
  34. langgraph_api/grpc/config_conversion.py +231 -0
  35. langgraph_api/grpc/generated/__init__.py +29 -0
  36. langgraph_api/grpc/generated/checkpointer_pb2.py +63 -0
  37. langgraph_api/grpc/generated/checkpointer_pb2.pyi +99 -0
  38. langgraph_api/grpc/generated/checkpointer_pb2_grpc.py +329 -0
  39. langgraph_api/grpc/generated/core_api_pb2.py +216 -0
  40. langgraph_api/grpc/generated/core_api_pb2.pyi +905 -0
  41. langgraph_api/grpc/generated/core_api_pb2_grpc.py +1621 -0
  42. langgraph_api/grpc/generated/engine_common_pb2.py +219 -0
  43. langgraph_api/grpc/generated/engine_common_pb2.pyi +722 -0
  44. langgraph_api/grpc/generated/engine_common_pb2_grpc.py +24 -0
  45. langgraph_api/grpc/generated/enum_cancel_run_action_pb2.py +37 -0
  46. langgraph_api/grpc/generated/enum_cancel_run_action_pb2.pyi +12 -0
  47. langgraph_api/grpc/generated/enum_cancel_run_action_pb2_grpc.py +24 -0
  48. langgraph_api/grpc/generated/enum_control_signal_pb2.py +37 -0
  49. langgraph_api/grpc/generated/enum_control_signal_pb2.pyi +16 -0
  50. langgraph_api/grpc/generated/enum_control_signal_pb2_grpc.py +24 -0
  51. langgraph_api/grpc/generated/enum_durability_pb2.py +37 -0
  52. langgraph_api/grpc/generated/enum_durability_pb2.pyi +16 -0
  53. langgraph_api/grpc/generated/enum_durability_pb2_grpc.py +24 -0
  54. langgraph_api/grpc/generated/enum_multitask_strategy_pb2.py +37 -0
  55. langgraph_api/grpc/generated/enum_multitask_strategy_pb2.pyi +16 -0
  56. langgraph_api/grpc/generated/enum_multitask_strategy_pb2_grpc.py +24 -0
  57. langgraph_api/grpc/generated/enum_run_status_pb2.py +37 -0
  58. langgraph_api/grpc/generated/enum_run_status_pb2.pyi +22 -0
  59. langgraph_api/grpc/generated/enum_run_status_pb2_grpc.py +24 -0
  60. langgraph_api/grpc/generated/enum_stream_mode_pb2.py +37 -0
  61. langgraph_api/grpc/generated/enum_stream_mode_pb2.pyi +28 -0
  62. langgraph_api/grpc/generated/enum_stream_mode_pb2_grpc.py +24 -0
  63. langgraph_api/grpc/generated/enum_thread_status_pb2.py +37 -0
  64. langgraph_api/grpc/generated/enum_thread_status_pb2.pyi +16 -0
  65. langgraph_api/grpc/generated/enum_thread_status_pb2_grpc.py +24 -0
  66. langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.py +37 -0
  67. langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.pyi +16 -0
  68. langgraph_api/grpc/generated/enum_thread_stream_mode_pb2_grpc.py +24 -0
  69. langgraph_api/grpc/generated/errors_pb2.py +39 -0
  70. langgraph_api/grpc/generated/errors_pb2.pyi +21 -0
  71. langgraph_api/grpc/generated/errors_pb2_grpc.py +24 -0
  72. langgraph_api/grpc/ops/__init__.py +370 -0
  73. langgraph_api/grpc/ops/assistants.py +424 -0
  74. langgraph_api/grpc/ops/runs.py +792 -0
  75. langgraph_api/grpc/ops/threads.py +1013 -0
  76. langgraph_api/http.py +16 -5
  77. langgraph_api/http_metrics.py +15 -35
  78. langgraph_api/http_metrics_utils.py +38 -0
  79. langgraph_api/js/build.mts +1 -1
  80. langgraph_api/js/client.http.mts +13 -7
  81. langgraph_api/js/client.mts +2 -5
  82. langgraph_api/js/package.json +29 -28
  83. langgraph_api/js/remote.py +56 -30
  84. langgraph_api/js/src/graph.mts +20 -0
  85. langgraph_api/js/sse.py +2 -2
  86. langgraph_api/js/ui.py +1 -1
  87. langgraph_api/js/yarn.lock +1204 -1006
  88. langgraph_api/logging.py +29 -2
  89. langgraph_api/metadata.py +99 -28
  90. langgraph_api/middleware/http_logger.py +7 -2
  91. langgraph_api/middleware/private_network.py +7 -7
  92. langgraph_api/models/run.py +54 -93
  93. langgraph_api/otel_context.py +205 -0
  94. langgraph_api/patch.py +5 -3
  95. langgraph_api/queue_entrypoint.py +154 -65
  96. langgraph_api/route.py +47 -5
  97. langgraph_api/schema.py +88 -10
  98. langgraph_api/self_hosted_logs.py +124 -0
  99. langgraph_api/self_hosted_metrics.py +450 -0
  100. langgraph_api/serde.py +79 -37
  101. langgraph_api/server.py +138 -60
  102. langgraph_api/state.py +4 -3
  103. langgraph_api/store.py +25 -16
  104. langgraph_api/stream.py +80 -29
  105. langgraph_api/thread_ttl.py +31 -13
  106. langgraph_api/timing/__init__.py +25 -0
  107. langgraph_api/timing/profiler.py +200 -0
  108. langgraph_api/timing/timer.py +318 -0
  109. langgraph_api/utils/__init__.py +53 -8
  110. langgraph_api/utils/cache.py +47 -10
  111. langgraph_api/utils/config.py +2 -1
  112. langgraph_api/utils/errors.py +77 -0
  113. langgraph_api/utils/future.py +10 -6
  114. langgraph_api/utils/headers.py +76 -2
  115. langgraph_api/utils/retriable_client.py +74 -0
  116. langgraph_api/utils/stream_codec.py +315 -0
  117. langgraph_api/utils/uuids.py +29 -62
  118. langgraph_api/validation.py +9 -0
  119. langgraph_api/webhook.py +120 -6
  120. langgraph_api/worker.py +55 -24
  121. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/METADATA +16 -8
  122. langgraph_api-0.7.3.dist-info/RECORD +168 -0
  123. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/WHEEL +1 -1
  124. langgraph_runtime/__init__.py +1 -0
  125. langgraph_runtime/routes.py +11 -0
  126. logging.json +1 -3
  127. openapi.json +839 -478
  128. langgraph_api/config.py +0 -387
  129. langgraph_api/js/isolate-0x130008000-46649-46649-v8.log +0 -4430
  130. langgraph_api/js/isolate-0x138008000-44681-44681-v8.log +0 -4430
  131. langgraph_api/js/package-lock.json +0 -3308
  132. langgraph_api-0.4.1.dist-info/RECORD +0 -107
  133. /langgraph_api/{utils.py → grpc/__init__.py} +0 -0
  134. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/entry_points.txt +0 -0
  135. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/licenses/LICENSE +0 -0
@@ -7,6 +7,7 @@ from datetime import datetime
7
7
  from typing import Any, Protocol, TypeAlias, TypeVar, cast
8
8
 
9
9
  import structlog
10
+ from langchain_core.runnables import RunnableConfig
10
11
  from langgraph_sdk import Auth
11
12
  from starlette.authentication import AuthCredentials, BaseUser
12
13
  from starlette.exceptions import HTTPException
@@ -36,7 +37,7 @@ async def with_user(
36
37
  if current is None:
37
38
  return
38
39
  set_auth_ctx(
39
- cast(BaseUser, current.user), AuthCredentials(scopes=current.permissions)
40
+ cast("BaseUser", current.user), AuthCredentials(scopes=current.permissions)
40
41
  )
41
42
 
42
43
 
@@ -60,6 +61,42 @@ def get_auth_ctx() -> Auth.types.BaseAuthContext | None:
60
61
  return AuthContext.get()
61
62
 
62
63
 
64
+ def get_user_id(user: BaseUser | None) -> str | None:
65
+ if user is None:
66
+ return None
67
+ try:
68
+ return user.identity
69
+ except NotImplementedError:
70
+ try:
71
+ return user.display_name
72
+ except NotImplementedError:
73
+ pass
74
+
75
+
76
+ def merge_auth(
77
+ config: RunnableConfig,
78
+ ctx: Auth.types.BaseAuthContext | None = None,
79
+ ) -> RunnableConfig:
80
+ """Inject auth context into config's configurable dict.
81
+
82
+ If ctx is not provided, attempts to get it from the current context.
83
+ """
84
+ if ctx is None:
85
+ ctx = get_auth_ctx()
86
+ if ctx is None:
87
+ return config
88
+
89
+ configurable = config.setdefault("configurable", {})
90
+ return config | {
91
+ "configurable": configurable
92
+ | {
93
+ "langgraph_auth_user": cast("BaseUser | None", ctx.user),
94
+ "langgraph_auth_user_id": get_user_id(cast("BaseUser | None", ctx.user)),
95
+ "langgraph_auth_permissions": ctx.permissions,
96
+ }
97
+ }
98
+
99
+
63
100
  class AsyncCursorProto(Protocol):
64
101
  async def fetchone(self) -> Row: ...
65
102
 
@@ -139,9 +176,17 @@ class SchemaGenerator(BaseSchemaGenerator):
139
176
  for endpoint in endpoints_info:
140
177
  try:
141
178
  parsed = self.parse_docstring(endpoint.func)
142
- except AssertionError:
143
- logger.warning("Could not parse docstrings for route %s", endpoint.path)
144
- parsed = {}
179
+ except Exception as exc:
180
+ docstring = getattr(endpoint.func, "__doc__", None) or ""
181
+ logger.warning(
182
+ "Unable to parse docstring from OpenAPI schema for route %s (%s): %s\n\nUsing as description",
183
+ endpoint.path,
184
+ endpoint.func.__qualname__,
185
+ exc,
186
+ exc_info=exc,
187
+ docstring=docstring,
188
+ )
189
+ parsed = {"description": docstring}
145
190
 
146
191
  if endpoint.path not in schema["paths"]:
147
192
  schema["paths"][endpoint.path] = {}
@@ -186,14 +231,14 @@ def validate_select_columns(
186
231
 
187
232
 
188
233
  __all__ = [
234
+ "AsyncConnectionProto",
189
235
  "AsyncCursorProto",
190
236
  "AsyncPipelineProto",
191
- "AsyncConnectionProto",
192
- "fetchone",
193
- "validate_uuid",
194
- "next_cron_date",
195
237
  "SchemaGenerator",
238
+ "fetchone",
196
239
  "get_pagination_headers",
240
+ "next_cron_date",
197
241
  "uuid7",
198
242
  "validate_select_columns",
243
+ "validate_uuid",
199
244
  ]
@@ -1,18 +1,27 @@
1
1
  import asyncio
2
2
  import time
3
3
  from collections import OrderedDict
4
+ from collections.abc import Awaitable, Callable
4
5
  from typing import Generic, TypeVar
5
6
 
6
7
  T = TypeVar("T")
7
8
 
8
9
 
9
10
  class LRUCache(Generic[T]):
10
- """LRU cache with TTL support."""
11
+ """LRU cache with TTL and proactive refresh support."""
11
12
 
12
- def __init__(self, max_size: int = 1000, ttl: float = 60):
13
- self._cache: OrderedDict[str, tuple[T, float]] = OrderedDict()
13
+ def __init__(
14
+ self,
15
+ max_size: int = 1000,
16
+ ttl: float = 60,
17
+ refresh_window: float = 30,
18
+ refresh_callback: Callable[[str], Awaitable[T | None]] | None = None,
19
+ ):
20
+ self._cache: OrderedDict[str, tuple[T, float, bool]] = OrderedDict()
14
21
  self._max_size = max_size if max_size > 0 else 1000
15
22
  self._ttl = ttl
23
+ self._refresh_window = refresh_window if refresh_window > 0 else 30
24
+ self._refresh_callback = refresh_callback
16
25
 
17
26
  def _get_time(self) -> float:
18
27
  """Get current time, using loop.time() if available for better performance."""
@@ -21,17 +30,45 @@ class LRUCache(Generic[T]):
21
30
  except RuntimeError:
22
31
  return time.monotonic()
23
32
 
24
- def get(self, key: str) -> T | None:
25
- """Get item from cache, returning None if expired or not found."""
33
+ async def get(self, key: str) -> T | None:
34
+ """Get item from cache, attempting refresh if within refresh window."""
26
35
  if key not in self._cache:
27
36
  return None
28
37
 
29
- value, timestamp = self._cache[key]
30
- if self._get_time() - timestamp >= self._ttl:
31
- # Expired, remove and return None
38
+ value, timestamp, is_refreshing = self._cache[key]
39
+ current_time = self._get_time()
40
+ time_until_expiry = self._ttl - (current_time - timestamp)
41
+
42
+ # Check if expired
43
+ if time_until_expiry <= 0:
32
44
  del self._cache[key]
33
45
  return None
34
46
 
47
+ # Check if we should attempt refresh (within refresh window and not already refreshing)
48
+ if (
49
+ time_until_expiry <= self._refresh_window
50
+ and not is_refreshing
51
+ and self._refresh_callback
52
+ ):
53
+ # Mark as refreshing to prevent multiple simultaneous refresh attempts
54
+ self._cache[key] = (value, timestamp, True)
55
+
56
+ try:
57
+ # Attempt refresh
58
+ refreshed_value = await self._refresh_callback(key)
59
+ if refreshed_value is not None:
60
+ # Refresh successful, update cache with new value
61
+ self._cache[key] = (refreshed_value, current_time, False)
62
+ # Move to end (most recently used)
63
+ self._cache.move_to_end(key)
64
+ return refreshed_value
65
+ else:
66
+ # Refresh failed, fallback to cached value
67
+ self._cache[key] = (value, timestamp, False)
68
+ except Exception:
69
+ # Refresh failed with exception, fallback to cached value
70
+ self._cache[key] = (value, timestamp, False)
71
+
35
72
  # Move to end (most recently used)
36
73
  self._cache.move_to_end(key)
37
74
  return value
@@ -46,8 +83,8 @@ class LRUCache(Generic[T]):
46
83
  while len(self._cache) >= self._max_size:
47
84
  self._cache.popitem(last=False) # Remove oldest (FIFO)
48
85
 
49
- # Add new entry
50
- self._cache[key] = (value, self._get_time())
86
+ # Add new entry (not refreshing initially)
87
+ self._cache[key] = (value, self._get_time(), False)
51
88
 
52
89
  def size(self) -> int:
53
90
  """Return current cache size."""
@@ -4,7 +4,6 @@ import asyncio
4
4
  import functools
5
5
  import typing
6
6
  from collections import ChainMap
7
- from concurrent.futures import Executor
8
7
  from contextvars import copy_context
9
8
  from os import getenv
10
9
  from typing import Any, ParamSpec, TypeVar
@@ -13,6 +12,8 @@ from langgraph.constants import CONF
13
12
  from typing_extensions import TypedDict
14
13
 
15
14
  if typing.TYPE_CHECKING:
15
+ from concurrent.futures import Executor
16
+
16
17
  from langchain_core.runnables import RunnableConfig
17
18
 
18
19
  try:
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import traceback
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ if TYPE_CHECKING:
7
+ from langgraph_api.graph import GraphSpec
8
+
9
+
10
+ class GraphLoadError(RuntimeError):
11
+ """Raised when a user provided graph fails to load."""
12
+
13
+ def __init__(self, spec: GraphSpec, cause: Exception):
14
+ self.spec = spec
15
+ self.cause = cause
16
+ self.location = spec.module or spec.path or "<unknown>"
17
+ self.notes = tuple(getattr(cause, "__notes__", ()) or ())
18
+ self._traceback = traceback.TracebackException.from_exception(
19
+ cause, capture_locals=False
20
+ )
21
+ self._exception_only = list(self._traceback.format_exception_only())
22
+ message = f"Failed to load graph '{spec.id}' from {self.location}: {cause}"
23
+ super().__init__(message)
24
+
25
+ @property
26
+ def hint(self) -> str | None:
27
+ if isinstance(self.cause, ImportError):
28
+ return "Check that your project dependencies are installed and imports are correct."
29
+ return None
30
+
31
+ def log_fields(self) -> dict[str, Any]:
32
+ fields: dict[str, Any] = {
33
+ "graph_id": self.spec.id,
34
+ "location": self.location,
35
+ "error_type": type(self.cause).__name__,
36
+ "error_message": self.cause_message,
37
+ "error_boundary": "user_graph",
38
+ "summary": self.summary,
39
+ }
40
+ if self.hint:
41
+ fields["hint"] = self.hint
42
+ if self.notes:
43
+ fields["notes"] = "\n".join(self.notes)
44
+ fields["user_traceback"] = self.user_traceback()
45
+ return fields
46
+
47
+ @property
48
+ def cause_message(self) -> str:
49
+ if self._exception_only:
50
+ return self._exception_only[0].strip()
51
+ return str(self.cause)
52
+
53
+ @property
54
+ def summary(self) -> str:
55
+ return f"{type(self.cause).__name__}: {self.cause_message}"
56
+
57
+ def user_traceback(self) -> str:
58
+ """Return the full traceback without filtering."""
59
+ return "".join(self._traceback.format())
60
+
61
+
62
+ class HealthServerStartupError(RuntimeError):
63
+ def __init__(self, host: str, port: int, cause: BaseException):
64
+ self.host = host
65
+ self.port = port
66
+ self.cause = cause
67
+ port_desc = (
68
+ f"{host}:{port}" if host not in {"0.0.0.0", "::"} else f"port {port}"
69
+ )
70
+ if isinstance(cause, OSError) and cause.errno in {48, 98}:
71
+ message = (
72
+ f"Health/metrics server could not bind to {port_desc}: "
73
+ "address already in use. Stop the other process or set PORT to a free port."
74
+ )
75
+ else:
76
+ message = f"Health/metrics server failed to start on {port_desc}: {cause}"
77
+ super().__init__(message)
@@ -2,12 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import concurrent.futures
5
- import contextvars
6
5
  import inspect
7
6
  import sys
8
7
  import types
9
- from collections.abc import Awaitable, Coroutine, Generator
10
- from typing import TypeVar, cast
8
+ from typing import TYPE_CHECKING, TypeVar, cast
9
+
10
+ if TYPE_CHECKING:
11
+ import contextvars
12
+ from collections.abc import Awaitable, Coroutine, Generator
11
13
 
12
14
  T = TypeVar("T")
13
15
  AnyFuture = asyncio.Future | concurrent.futures.Future
@@ -45,7 +47,8 @@ def _set_concurrent_future_state(
45
47
  source: AnyFuture,
46
48
  ) -> None:
47
49
  """Copy state from a future to a concurrent.futures.Future."""
48
- assert source.done()
50
+ if not source.done():
51
+ raise ValueError("Future is not done")
49
52
  if source.cancelled():
50
53
  concurrent.cancel()
51
54
  if not concurrent.set_running_or_notify_cancel():
@@ -65,7 +68,8 @@ def _copy_future_state(source: AnyFuture, dest: asyncio.Future) -> None:
65
68
  """
66
69
  if dest.done():
67
70
  return
68
- assert source.done()
71
+ if not source.done():
72
+ raise ValueError("Future is not done")
69
73
  if dest.cancelled():
70
74
  return
71
75
  if source.cancelled():
@@ -152,7 +156,7 @@ def _ensure_future(
152
156
  if not asyncio.iscoroutine(coro_or_future):
153
157
  if inspect.isawaitable(coro_or_future):
154
158
  coro_or_future = cast(
155
- Coroutine[None, None, T], _wrap_awaitable(coro_or_future)
159
+ "Coroutine[None, None, T]", _wrap_awaitable(coro_or_future)
156
160
  )
157
161
  called_wrap_awaitable = True
158
162
  else:
@@ -2,6 +2,16 @@
2
2
 
3
3
  import functools
4
4
  import re
5
+ import urllib.parse
6
+ from collections.abc import Mapping
7
+
8
+ import orjson
9
+
10
+ LANGSMITH_METADATA = "langsmith-metadata"
11
+ LANGSMITH_TAGS = "langsmith-tags"
12
+ LANGSMITH_PROJECT = "langsmith-project"
13
+ # For security, don't include these in configuration
14
+ DEFAULT_RUN_HEADERS_EXCLUDE = {"x-api-key", "x-tenant-id", "x-service-key"}
5
15
 
6
16
 
7
17
  def translate_pattern(pat: str) -> re.Pattern[str]:
@@ -23,6 +33,62 @@ def translate_pattern(pat: str) -> re.Pattern[str]:
23
33
  return re.compile(rf"(?s:{pattern})\Z")
24
34
 
25
35
 
36
+ def get_configurable_headers(headers: Mapping[str, str]) -> dict[str, str]:
37
+ """Extract headers that should be added to run configuration.
38
+
39
+ This function handles special cases like langsmith-trace and baggage headers,
40
+ while respecting the configurable header patterns.
41
+ """
42
+ configurable = {}
43
+ if not headers:
44
+ return configurable
45
+
46
+ for key, value in headers.items():
47
+ # First handle tracing stuff - always included regardless of patterns
48
+ if key == "langsmith-trace":
49
+ configurable[key] = value
50
+ if baggage := headers.get("baggage"):
51
+ for item in baggage.split(","):
52
+ baggage_key, baggage_value = item.split("=")
53
+ if (
54
+ baggage_key == LANGSMITH_METADATA
55
+ and baggage_key not in configurable
56
+ ):
57
+ configurable[baggage_key] = orjson.loads(
58
+ urllib.parse.unquote(baggage_value)
59
+ )
60
+ elif baggage_key == LANGSMITH_TAGS:
61
+ configurable[baggage_key] = urllib.parse.unquote(
62
+ baggage_value
63
+ ).split(",")
64
+ elif baggage_key == LANGSMITH_PROJECT:
65
+ configurable[baggage_key] = urllib.parse.unquote(baggage_value)
66
+ continue
67
+
68
+ # Check if header should be included based on patterns
69
+ # For run configuration, we have specific default behavior for x-* headers
70
+ if key.startswith("x-"):
71
+ # Check against default excludes for x-* headers
72
+ if key in DEFAULT_RUN_HEADERS_EXCLUDE:
73
+ # Check if explicitly included via patterns
74
+ if should_include_header(key):
75
+ configurable[key] = value
76
+ continue
77
+ # Other x-* headers are included by default unless patterns exclude them
78
+ if should_include_header(key):
79
+ configurable[key] = value
80
+ elif key == "user-agent":
81
+ # user-agent is included by default unless excluded by patterns
82
+ if should_include_header(key):
83
+ configurable[key] = value
84
+ else:
85
+ # All other headers only included if patterns allow
86
+ if should_include_header(key):
87
+ configurable[key] = value
88
+
89
+ return configurable
90
+
91
+
26
92
  @functools.lru_cache(maxsize=1)
27
93
  def get_header_patterns(
28
94
  key: str,
@@ -59,6 +125,14 @@ def should_include_header(key: str) -> bool:
59
125
  Returns:
60
126
  True if the header should be included, False otherwise
61
127
  """
128
+ if (
129
+ key == "x-api-key"
130
+ or key == "x-service-key"
131
+ or key == "x-tenant-id"
132
+ or key == "authorization"
133
+ ):
134
+ return False
135
+
62
136
  include_patterns, exclude_patterns = get_header_patterns("configurable_headers")
63
137
 
64
138
  return pattern_matches(key, include_patterns, exclude_patterns)
@@ -85,5 +159,5 @@ def pattern_matches(
85
159
  # If include patterns are specified, only include headers matching them
86
160
  return any(pattern.match(key) for pattern in include_patterns)
87
161
 
88
- # Default behavior - include if not excluded
89
- return True
162
+ # Default behavior - exclude
163
+ return False
@@ -0,0 +1,74 @@
1
+ import asyncio
2
+
3
+ import httpx
4
+ import structlog
5
+
6
+ logger = structlog.stdlib.get_logger(__name__)
7
+
8
+
9
+ async def _make_http_request_with_retries(
10
+ url: str,
11
+ headers: dict,
12
+ method: str = "GET",
13
+ json_data: dict | None = None,
14
+ max_retries: int = 3,
15
+ base_delay: float = 1.0,
16
+ ) -> httpx.Response | None:
17
+ """
18
+ Make an HTTP request with exponential backoff retries.
19
+
20
+ Args:
21
+ url: The URL to request
22
+ headers: Headers to include in the request
23
+ method: HTTP method ("GET" or "POST")
24
+ json_data: JSON data for POST requests
25
+ max_retries: Maximum number of retry attempts
26
+ base_delay: Base delay in seconds for exponential backoff
27
+
28
+ Returns:
29
+ httpx.Response: The successful response
30
+
31
+ Raises:
32
+ httpx.HTTPStatusError: If the request fails after all retries
33
+ httpx.RequestError: If the request fails after all retries
34
+ """
35
+ for attempt in range(max_retries + 1):
36
+ try:
37
+ async with httpx.AsyncClient(timeout=10.0) as client:
38
+ response = await client.request(
39
+ method, url, headers=headers, json=json_data
40
+ )
41
+ response.raise_for_status()
42
+ return response
43
+
44
+ except (
45
+ httpx.TimeoutException,
46
+ httpx.NetworkError,
47
+ httpx.RequestError,
48
+ httpx.HTTPStatusError,
49
+ ) as e:
50
+ if isinstance(e, httpx.HTTPStatusError) and e.response.status_code < 500:
51
+ # Don't retry on 4xx errors, but do on 5xxs
52
+ raise e
53
+
54
+ # Back off and retry if we haven't reached the max retries
55
+ if attempt < max_retries:
56
+ delay = base_delay * (2**attempt) # Exponential backoff
57
+ logger.warning(
58
+ "HTTP %s request attempt %d to %s failed: %s. Retrying in %.1f seconds...",
59
+ method,
60
+ attempt + 1,
61
+ url,
62
+ e,
63
+ delay,
64
+ )
65
+ await asyncio.sleep(delay)
66
+ else:
67
+ logger.exception(
68
+ "HTTP %s request to %s failed after %d attempts. Last error: %s",
69
+ method,
70
+ url,
71
+ max_retries + 1,
72
+ e,
73
+ )
74
+ raise e