chopratejas commited on
Commit
3b0b075
·
1 Parent(s): 419f040

Fix streaming crashes under concurrent sessions

Browse files

- Cache LiteLLM model resolution to avoid synchronous cost_per_token()
calls blocking the async event loop on every request (3.28ms → 0.12ms)
- Add proper error handling in streaming generate() for httpx connection
errors, timeouts, and pool exhaustion — previously crashed ASGI app
- Bump version to 0.3.3

headroom/__init__.py CHANGED
@@ -142,7 +142,7 @@ from .transforms import (
142
  TransformPipeline,
143
  )
144
 
145
- __version__ = "0.3.2"
146
 
147
  __all__ = [
148
  # Main client
 
142
  TransformPipeline,
143
  )
144
 
145
+ __version__ = "0.3.3"
146
 
147
  __all__ = [
148
  # Main client
headroom/proxy/server.py CHANGED
@@ -539,9 +539,28 @@ class CostTracker:
539
  self._total_savings_usd: float = 0
540
  self._last_prune_time: datetime = datetime.now()
541
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
542
  @staticmethod
543
- def _resolve_litellm_model(model: str) -> str:
544
- """Resolve model name to one LiteLLM recognizes, adding provider prefix if needed."""
545
  if not LITELLM_AVAILABLE:
546
  return model
547
 
@@ -3686,6 +3705,27 @@ class HeadroomProxy:
3686
  # No memory tool calls, yield original buffered chunks
3687
  for chunk in buffered_chunks:
3688
  yield chunk
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3689
  finally:
3690
  # Record metrics after stream completes
3691
  total_latency = (time.time() - start_time) * 1000
 
539
  self._total_savings_usd: float = 0
540
  self._last_prune_time: datetime = datetime.now()
541
 
542
+ # Cache resolved model names to avoid repeated litellm lookups.
543
+ # This is critical: litellm.cost_per_token() is synchronous and can block
544
+ # the async event loop if it triggers I/O (lazy model info download).
545
+ _resolved_model_cache: dict[str, str] = {}
546
+
547
+ @classmethod
548
+ def _resolve_litellm_model(cls, model: str) -> str:
549
+ """Resolve model name to one LiteLLM recognizes, adding provider prefix if needed.
550
+
551
+ Results are cached per model name to avoid blocking the event loop
552
+ with repeated synchronous litellm lookups.
553
+ """
554
+ if model in cls._resolved_model_cache:
555
+ return cls._resolved_model_cache[model]
556
+
557
+ resolved = cls._resolve_litellm_model_uncached(model)
558
+ cls._resolved_model_cache[model] = resolved
559
+ return resolved
560
+
561
  @staticmethod
562
+ def _resolve_litellm_model_uncached(model: str) -> str:
563
+ """Uncached resolution called once per unique model name."""
564
  if not LITELLM_AVAILABLE:
565
  return model
566
 
 
3705
  # No memory tool calls, yield original buffered chunks
3706
  for chunk in buffered_chunks:
3707
  yield chunk
3708
+ except (httpx.ConnectError, httpx.ConnectTimeout, httpx.PoolTimeout) as e:
3709
+ logger.error(f"[{request_id}] Connection error to upstream API: {e}")
3710
+ error_event = {
3711
+ "type": "error",
3712
+ "error": {
3713
+ "type": "connection_error",
3714
+ "message": f"Failed to connect to upstream API: {e}",
3715
+ },
3716
+ }
3717
+ yield f"event: error\ndata: {json.dumps(error_event)}\n\n".encode()
3718
+ except httpx.HTTPStatusError as e:
3719
+ logger.error(f"[{request_id}] HTTP error from upstream API: {e}")
3720
+ # Forward the upstream error response
3721
+ yield e.response.content
3722
+ except Exception as e:
3723
+ logger.error(f"[{request_id}] Unexpected streaming error: {e}")
3724
+ error_event = {
3725
+ "type": "error",
3726
+ "error": {"type": "api_error", "message": str(e)},
3727
+ }
3728
+ yield f"event: error\ndata: {json.dumps(error_event)}\n\n".encode()
3729
  finally:
3730
  # Record metrics after stream completes
3731
  total_latency = (time.time() - start_time) * 1000
pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
 
5
  [project]
6
  name = "headroom-ai"
7
- version = "0.3.2"
8
  description = "The Context Optimization Layer for LLM Applications - Cut costs by 50-90%"
9
  readme = "README.md"
10
  license = "Apache-2.0"
 
4
 
5
  [project]
6
  name = "headroom-ai"
7
+ version = "0.3.3"
8
  description = "The Context Optimization Layer for LLM Applications - Cut costs by 50-90%"
9
  readme = "README.md"
10
  license = "Apache-2.0"