Spaces:
Build error
Build error
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 +1 -1
- headroom/proxy/server.py +42 -2
- pyproject.toml +1 -1
headroom/__init__.py
CHANGED
|
@@ -142,7 +142,7 @@ from .transforms import (
|
|
| 142 |
TransformPipeline,
|
| 143 |
)
|
| 144 |
|
| 145 |
-
__version__ = "0.3.
|
| 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
|
| 544 |
-
"""
|
| 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.
|
| 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"
|