From 969cf79dba30dcccbd0bc58e7b6f8ba1be364b06 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 10 Jul 2026 20:51:10 -0700 Subject: [PATCH] fix(claude-code): bound concurrent GitHub fetches during marketplace dir-scan Unbounded asyncio.gather fan-out during skills-dir discovery (one HTTP fetch per skill folder) serialized enough blocking socket.getaddrinfo() calls inside the shared SSRF validator to blow through the sync timeout for repos with more than a handful of skills. Import would report sync_status "error" and only succeed after a manual re-sync. Bounding concurrency to 6 in-flight fetches fixes the timeout without touching the shared SSRF utility. --- .../claude_code_marketplace_sync.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace_sync.py b/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace_sync.py index b1ebec2b6bb..00bfe29dd96 100644 --- a/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace_sync.py +++ b/litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace_sync.py @@ -47,6 +47,15 @@ from litellm.types.proxy.claude_code_endpoints import ( DEFAULT_SYNC_TIMEOUT_SECONDS = 10.0 +# validate_url() (litellm_core_utils/url_utils.py) does a blocking +# socket.getaddrinfo() per request; discovery fans out one fetch per skill +# folder via asyncio.gather, and unbounded concurrency there serializes enough +# DNS lookups on the event loop to blow through DEFAULT_SYNC_TIMEOUT_SECONDS +# for repos with more than a handful of skills. Bounding concurrency here +# keeps each individual fetch fast without touching the shared SSRF utility. +_MAX_CONCURRENT_GITHUB_FETCHES = 6 +_github_fetch_semaphore = asyncio.Semaphore(_MAX_CONCURRENT_GITHUB_FETCHES) + SourceHost = Literal["github", "gitlab", "bitbucket", "url"] SyncErrorReason = Literal["unreachable", "http_error", "invalid_json", "invalid_schema"] @@ -163,7 +172,9 @@ async def _http_get(client: AsyncHTTPHandler, url: str, *, timeout: float) -> ht try: # async_safe_get is SSRF-guarded (resolves + validates every redirect # hop) - required here because the target URL is admin-supplied. - return await async_safe_get(client, url, headers={}, timeout=timeout) + # Bounded by _github_fetch_semaphore: see its module-level comment. + async with _github_fetch_semaphore: + return await async_safe_get(client, url, headers={}, timeout=timeout) except SSRFError as exc: raise MarketplaceSyncError(reason="unreachable", detail=str(exc)) from exc except httpx.HTTPError as exc: