From 23ba6561902fad7560ecf485df3a618cb054ac1c Mon Sep 17 00:00:00 2001 From: vegu-ai-tools <152010387+vegu-ai-tools@users.noreply.github.com> Date: Fri, 1 May 2026 12:56:08 +0300 Subject: [PATCH] fix: resolve redundant fetches in OpenRouter client with asyncio locks --- CHANGELOG.yaml | 1 + src/talemate/client/openrouter.py | 125 +++++++++++++++++------------- 2 files changed, 70 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.yaml b/CHANGELOG.yaml index 5c63095a..9be4a22c 100644 --- a/CHANGELOG.yaml +++ b/CHANGELOG.yaml @@ -47,6 +47,7 @@ - "Character Visuals: Fixed an infinite loop in the cover image and portrait tabs that spammed `scene_assets/search` requests at the backend whenever a character had no scene assets to fall back on as references. The reference-asset search now runs at most once per character, breaking the watcher feedback loop that previously kept firing throughout the time the Generate dialog was open." - "macOS Modifiers: Ctrl+click affordances throughout the UI now also accept Cmd (Meta) on macOS, where the OS reserves Ctrl+click for the system context menu. Tooltips, hints, and docs label the modifier as Cmd on Mac and Ctrl elsewhere. Affects regenerate, scene tools (narrator/director/actor/creative), world state manager, contextual generate, message asset image, autocomplete (Ctrl+Enter), and history navigation. (#261)" - "Game Loop Actor Iter: Fixed a bug where the `game_loop_actor_iter` event was only firing for player turns, so agents that listen for actor iteration (like passive narration) silently skipped AI turns." + - "OpenRouter: Fixed redundant provider/model list fetches when multiple OpenRouter clients are configured. Concurrent `status()` calls all saw the fetched-flag as still false and each kicked off its own HTTP request; the fetches are now serialized with an asyncio lock and double-checked locking so only one request runs." improvements: - "System Prompt Override Indicators: The system prompt override list now shows a pencil icon next to entries that have an active override, making it easy to see which prompts have been customized." - "Search Strictness: The distance_mod embedding preset value is now a float (was int) with a range of 0.1–2.0, allowing both tighter and looser similarity matching. A 'Search Strictness' slider is now available in the Context Database UI, letting users tune search sensitivity on the fly. Changes persist to the active embedding preset immediately." diff --git a/src/talemate/client/openrouter.py b/src/talemate/client/openrouter.py index 08b96847..5679e46d 100644 --- a/src/talemate/client/openrouter.py +++ b/src/talemate/client/openrouter.py @@ -50,6 +50,9 @@ DEFAULT_MODEL = "google/gemini-3-flash-preview" MODELS_FETCHED = False PROVIDERS_FETCHED = False +_MODELS_LOCK = asyncio.Lock() +_PROVIDERS_LOCK = asyncio.Lock() + async def fetch_available_models(api_key: str = None): """Fetch available models from OpenRouter API""" @@ -58,30 +61,36 @@ async def fetch_available_models(api_key: str = None): if MODELS_FETCHED: return AVAILABLE_MODELS - try: - log.debug("Fetching models from OpenRouter") - async with httpx.AsyncClient() as client: - response = await client.get( - "https://openrouter.ai/api/v1/models", timeout=10.0 - ) - if response.status_code == 200: - data = response.json() - models = [] - for model in data.get("data", []): - model_id = model.get("id") - if model_id: - models.append(model_id) - AVAILABLE_MODELS = sorted(models) - log.debug(f"Fetched {len(AVAILABLE_MODELS)} models from OpenRouter") - else: - log.warning( - f"Failed to fetch models from OpenRouter: {response.status_code}" - ) - except Exception as e: - log.error(f"Error fetching models from OpenRouter: {e}") + async with _MODELS_LOCK: + if MODELS_FETCHED: + return AVAILABLE_MODELS - MODELS_FETCHED = True - return AVAILABLE_MODELS + try: + log.debug("Fetching models from OpenRouter") + async with httpx.AsyncClient() as client: + response = await client.get( + "https://openrouter.ai/api/v1/models", timeout=10.0 + ) + if response.status_code == 200: + data = response.json() + models = [] + for model in data.get("data", []): + model_id = model.get("id") + if model_id: + models.append(model_id) + AVAILABLE_MODELS = sorted(models) + log.debug( + f"Fetched {len(AVAILABLE_MODELS)} models from OpenRouter" + ) + else: + log.warning( + f"Failed to fetch models from OpenRouter: {response.status_code}" + ) + except Exception as e: + log.error(f"Error fetching models from OpenRouter: {e}") + + MODELS_FETCHED = True + return AVAILABLE_MODELS async def fetch_available_providers(api_key: str = None): @@ -91,43 +100,47 @@ async def fetch_available_providers(api_key: str = None): if PROVIDERS_FETCHED: return AVAILABLE_PROVIDERS - if not api_key: - api_key = get_config().openrouter.api_key + async with _PROVIDERS_LOCK: + if PROVIDERS_FETCHED: + return AVAILABLE_PROVIDERS + + if not api_key: + api_key = get_config().openrouter.api_key + + if not api_key: + log.warning("No OpenRouter API key available, cannot fetch providers") + PROVIDERS_FETCHED = True + return AVAILABLE_PROVIDERS + + try: + log.debug("Fetching providers from OpenRouter") + async with httpx.AsyncClient() as client: + response = await client.get( + "https://openrouter.ai/api/v1/providers", + headers={"Authorization": f"Bearer {api_key}"}, + timeout=10.0, + ) + if response.status_code == 200: + data = response.json() + providers = [] + for provider in data.get("data", []): + provider_name = provider.get("name") + if provider_name: + providers.append(provider_name) + AVAILABLE_PROVIDERS = sorted(providers) + log.info( + f"Fetched {len(AVAILABLE_PROVIDERS)} providers from OpenRouter" + ) + else: + log.error( + f"Failed to fetch providers from OpenRouter: HTTP {response.status_code}" + ) + except Exception as e: + log.error(f"Error fetching providers from OpenRouter: {e}") - if not api_key: - log.warning("No OpenRouter API key available, cannot fetch providers") PROVIDERS_FETCHED = True return AVAILABLE_PROVIDERS - try: - log.debug("Fetching providers from OpenRouter") - async with httpx.AsyncClient() as client: - response = await client.get( - "https://openrouter.ai/api/v1/providers", - headers={"Authorization": f"Bearer {api_key}"}, - timeout=10.0, - ) - if response.status_code == 200: - data = response.json() - providers = [] - for provider in data.get("data", []): - provider_name = provider.get("name") - if provider_name: - providers.append(provider_name) - AVAILABLE_PROVIDERS = sorted(providers) - log.info( - f"Fetched {len(AVAILABLE_PROVIDERS)} providers from OpenRouter" - ) - else: - log.error( - f"Failed to fetch providers from OpenRouter: HTTP {response.status_code}" - ) - except Exception as e: - log.error(f"Error fetching providers from OpenRouter: {e}") - - PROVIDERS_FETCHED = True - return AVAILABLE_PROVIDERS - def on_talemate_started(event): """Spawn background tasks to fetch models and providers"""