Skip to content

Commit bc3aa20

Browse files
Copilotfriggeri
andcommitted
Add caching for list_models to prevent rate limiting
Co-authored-by: friggeri <106686+friggeri@users.noreply.github.com>
1 parent 407217f commit bc3aa20

2 files changed

Lines changed: 59 additions & 1 deletion

File tree

python/copilot/client.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ def __init__(self, options: Optional[CopilotClientOptions] = None):
157157
self._state: ConnectionState = "disconnected"
158158
self._sessions: dict[str, CopilotSession] = {}
159159
self._sessions_lock = threading.Lock()
160+
self._models_cache: Optional[list["ModelInfo"]] = None
161+
self._models_cache_lock = threading.Lock()
160162

161163
def _parse_cli_url(self, url: str) -> tuple[str, int]:
162164
"""
@@ -281,6 +283,10 @@ async def stop(self) -> list["StopError"]:
281283
await self._client.stop()
282284
self._client = None
283285

286+
# Clear models cache
287+
with self._models_cache_lock:
288+
self._models_cache = None
289+
284290
# Kill CLI process
285291
# Kill CLI process (only if we spawned it)
286292
if self._process and not self._is_external_server:
@@ -325,6 +331,10 @@ async def force_stop(self) -> None:
325331
pass # Ignore errors during force stop
326332
self._client = None
327333

334+
# Clear models cache
335+
with self._models_cache_lock:
336+
self._models_cache = None
337+
328338
# Kill CLI process immediately
329339
if self._process and not self._is_external_server:
330340
self._process.kill()
@@ -705,6 +715,9 @@ async def list_models(self) -> list["ModelInfo"]:
705715
"""
706716
List available models with their metadata.
707717
718+
Results are cached after the first successful call to avoid rate limiting.
719+
The cache is cleared when the client disconnects.
720+
708721
Returns:
709722
A list of ModelInfo objects with model details.
710723
@@ -720,9 +733,21 @@ async def list_models(self) -> list["ModelInfo"]:
720733
if not self._client:
721734
raise RuntimeError("Client not connected")
722735

736+
# Check cache first (thread-safe)
737+
with self._models_cache_lock:
738+
if self._models_cache is not None:
739+
return self._models_cache
740+
741+
# Cache miss - fetch from backend
723742
response = await self._client.request("models.list", {})
724743
models_data = response.get("models", [])
725-
return [ModelInfo.from_dict(model) for model in models_data]
744+
models = [ModelInfo.from_dict(model) for model in models_data]
745+
746+
# Update cache (thread-safe)
747+
with self._models_cache_lock:
748+
self._models_cache = models
749+
750+
return models
726751

727752
async def list_sessions(self) -> list["SessionMetadata"]:
728753
"""

python/e2e/test_client.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,36 @@ async def test_should_list_models_when_authenticated(self):
135135
await client.stop()
136136
finally:
137137
await client.force_stop()
138+
139+
async def test_should_cache_models_list(self):
140+
"""Test that list_models caches results to avoid rate limiting"""
141+
client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True})
142+
143+
try:
144+
await client.start()
145+
146+
auth_status = await client.get_auth_status()
147+
if not auth_status.isAuthenticated:
148+
# Skip if not authenticated - models.list requires auth
149+
await client.stop()
150+
return
151+
152+
# First call should fetch from backend
153+
models1 = await client.list_models()
154+
assert isinstance(models1, list)
155+
156+
# Second call should return cached results (same object reference)
157+
models2 = await client.list_models()
158+
assert models2 is models1, "Second call should return cached results"
159+
160+
# After stopping, cache should be cleared
161+
await client.stop()
162+
163+
# Restart and verify cache is empty
164+
await client.start()
165+
models3 = await client.list_models()
166+
assert models3 is not models1, "Cache should be cleared after disconnect"
167+
168+
await client.stop()
169+
finally:
170+
await client.force_stop()

0 commit comments

Comments
 (0)