""" Utilities for making API calls with retry logic and error handling. """ import time import random import traceback from loguru import logger from litellm import completion def make_api_call_with_retry(model: str, prompt: str) -> str: """ Makes an API call with a retry mechanism for error handling. Args: model: The model identifier to use. prompt: The prompt text to send to the model. Returns: The response from the model as a string. """ max_attempts = 20 base_delay = 10 max_delay = 60 attempt = 0 last_exception = None while attempt < max_attempts: try: # Add a small random delay to prevent simultaneous requests jitter = random.uniform(0.1, 1.0) time.sleep(jitter) # If this is a retry attempt, add exponential backoff delay if attempt > 0: delay = min(base_delay * (2 ** (attempt - 1)), max_delay) time.sleep(delay) response = completion( model=model, messages=[{"role": "user", "content": prompt}], num_retries=2, # Built-in retry mechanism of LiteLLM response_format={ "type": "json_object", "schema": { "type": "object", "properties": { "summary": {"type": "string", "description": "Overall analysis of the song vibes, meaning and mood"}, "main_themes": {"type": "array", "items": {"type": "string"}, "description": "Main themes identified in the song"}, "mood": {"type": "string", "description": "The overall mood/emotion of the song"}, "sections_analysis": { "type": "array", "items": { "type": "object", "properties": { "section_type": {"type": "string", "description": "verse/chorus/bridge/etc."}, "section_number": {"type": "integer", "description": "Sequential number of this section type"}, "lines": {"type": "array", "items": {"type": "string"}, "description": "Lyrics of this section"}, "analysis": {"type": "string", "description": "Analysis of this section with respect to the overall theme"} }, "required": ["section_type", "section_number", "lines", "analysis"] } }, "conclusion": {"type": "string", "description": "The song vibes and concepts of the underlying meaning, including ideas author may have intended to express"} }, "required": ["summary", "main_themes", "mood", "sections_analysis", "conclusion"] } } ) # Try to extract the content from the response try: analysis_result = response.choices[0].message.content.strip() return analysis_result except (AttributeError, KeyError, IndexError): try: analysis_result = response["choices"][0]["message"]["content"].strip() return analysis_result except (AttributeError, KeyError, IndexError): # If we couldn't extract the content, return an error raise ValueError("Failed to extract content from response") except (ConnectionError, TimeoutError) as e: last_exception = e logger.warning("API call failed (attempt {}/{}) for model {}: {}. Retrying...", attempt+1, max_attempts, model, str(e)) attempt += 1 continue except Exception as e: logger.error("Unexpected error: {}", str(e)) logger.error(traceback.format_exc()) raise # For other exceptions, we don't retry # If all attempts failed, re-raise the last exception if last_exception: logger.error("All {} attempts failed. Last error: {}", max_attempts, str(last_exception)) raise last_exception