From 92c19df6abadba690fb2786e3d8a2c363650bf35 Mon Sep 17 00:00:00 2001 From: mattgodbolt-molty Date: Sat, 21 Feb 2026 12:07:48 -0600 Subject: [PATCH] Switch to AsyncAnthropic client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sync Anthropic client was blocking the event loop inside async FastAPI routes. Switch to AsyncAnthropic so the API call properly yields to the event loop while waiting for the response. Closes #9 🤖 Generated by LLM (Claude, via OpenClaw) --- app/explain.py | 10 +++++----- app/main.py | 4 ++-- app/test_explain.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/explain.py b/app/explain.py index 35fff6c..c352bcb 100644 --- a/app/explain.py +++ b/app/explain.py @@ -1,6 +1,6 @@ import logging -from anthropic import Anthropic +from anthropic import AsyncAnthropic from app.cache import CacheProvider, cache_response, get_cached_response from app.explain_api import CostBreakdown, ExplainRequest, ExplainResponse, TokenUsage @@ -19,7 +19,7 @@ async def process_request( body: ExplainRequest, - client: Anthropic, + client: AsyncAnthropic, prompt: Prompt, metrics_provider: MetricsProvider, cache_provider: CacheProvider | None = None, @@ -31,7 +31,7 @@ async def process_request( Args: body: The request body as a Pydantic model - client: Anthropic client instance + client: AsyncAnthropic client instance prompt: Prompt instance for generating messages metrics_provider: metrics provider for tracking stats cache_provider: cache provider for storing/retrieving responses @@ -69,7 +69,7 @@ async def process_request( async def _call_anthropic_api( body: ExplainRequest, - client: Anthropic, + client: AsyncAnthropic, prompt: Prompt, metrics_provider: MetricsProvider, ) -> ExplainResponse: @@ -92,7 +92,7 @@ async def _call_anthropic_api( # Call Claude API LOGGER.info("Using Anthropic client with model: %s", {prompt_data["model"]}) - message = client.messages.create( + message = await client.messages.create( model=prompt_data["model"], max_tokens=prompt_data["max_tokens"], temperature=prompt_data["temperature"], diff --git a/app/main.py b/app/main.py index 1fa2681..957c066 100644 --- a/app/main.py +++ b/app/main.py @@ -2,7 +2,7 @@ from contextlib import asynccontextmanager from pathlib import Path -from anthropic import Anthropic +from anthropic import AsyncAnthropic from anthropic import __version__ as anthropic_version from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware @@ -42,7 +42,7 @@ async def lifespan(app: FastAPI): # Store shared resources in app.state app.state.settings = settings - app.state.anthropic_client = Anthropic(api_key=settings.anthropic_api_key) + app.state.anthropic_client = AsyncAnthropic(api_key=settings.anthropic_api_key) # Load the prompt configuration prompt_config_path = Path(__file__).parent / "prompt.yaml" diff --git a/app/test_explain.py b/app/test_explain.py index 4b84913..da3125d 100644 --- a/app/test_explain.py +++ b/app/test_explain.py @@ -1,6 +1,6 @@ import json from pathlib import Path -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock import pytest @@ -65,7 +65,7 @@ def mock_anthropic_client(): mock_message.usage.input_tokens = 100 mock_message.usage.output_tokens = 50 - mock_client.messages.create.return_value = mock_message + mock_client.messages.create = AsyncMock(return_value=mock_message) return mock_client