When building production AI applications, few issues are more frustrating than an API call that returns an empty string. You've sent a perfectly crafted prompt, waited the expected latency, and received... nothing. Understanding why this happens—and how to detect, debug, and prevent it—separates production-grade implementations from hobby projects.
Before we dive into the technical details, let's talk about economics. In 2026, API pricing varies dramatically across providers:
| Model | Output Cost ($/MTok) |
|---|---|
| GPT-4.1 | $8.00 |
| Claude Sonnet 4.5 | $15.00 |
| Gemini 2.5 Flash | $2.50 |
| DeepSeek V3.2 | $0.42 |
For a typical production workload of 10 million tokens/month, the cost difference is substantial:
- OpenAI GPT-4.1: $80/month
- Anthropic Claude Sonnet 4.5: $150/month
- Google Gemini 2.5 Flash: $25/month
- DeepSeek V3.2 via HolySheep AI relay: just $4.20/month
That's an 85%+ savings when routing through HolySheep, which offers rates of ¥1 per dollar equivalent while supporting WeChat and Alipay payments. With sub-50ms relay latency and free credits on signup, HolySheep provides the infrastructure backbone for cost-effective AI integrations.
Why Your API Returns Empty Strings
An empty response isn't random—it carries diagnostic information encoded in two critical fields: finish_reason and content_filter. These fields tell you exactly why the model stopped generating and whether any filtering occurred.
Understanding finish_reason
The finish_reason field indicates why the generation stopped. Common values include:
stop: Normal completion—the model generated a complete responselength: Hit token limit before completingcontent_filter: Content was filtered—response may be empty or truncatedtool_calls: Model generated function/tool calls instead of textrefusal: Model refused to provide the requested content
An empty string with finish_reason: "content_filter" or finish_reason: "refusal" tells you the model intentionally withheld content.
Decoding content_filter
Different providers expose content filtering differently:
{
"choices": [{
"finish_reason": "content_filter",
"content_filter_result": {
"filtered": true,
"finish_reason": "harmful_content"
}
}],
"usage": {
"prompt_tokens": 150,
"completion_tokens": 0,
"total_tokens": 150
}
}
When completion_tokens is zero alongside finish_reason: "content_filter", you have a confirmed filtered response.
Building a Robust Detection & Handling System
Here's a production-ready Python implementation that properly handles empty responses:
import requests
import json
import time
from dataclasses import dataclass
from typing import Optional, Dict, Any
from enum import Enum
class FinishReason(Enum):
STOP = "stop"
LENGTH = "length"
CONTENT_FILTER = "content_filter"
TOOL_CALLS = "tool_calls"
REFUSAL = "refusal"
UNKNOWN = "unknown"
@dataclass
class APIResponse:
content: str
finish_reason: FinishReason
content_filtered: bool
filter_details: Optional[Dict[str, Any]]
raw_response: Dict[str, Any]
@property
def is_empty(self) -> bool:
return len(self.content) == 0
def parse_finish_reason(reason: str) -> FinishReason:
"""Map API finish_reason string to enum."""
reason_mapping = {
"stop": FinishReason.STOP,
"length": FinishReason.LENGTH,
"content_filter": FinishReason.CONTENT_FILTER,
"tool_calls": FinishReason.TOOL_CALLS,
"refusal": FinishReason.REFUSAL,
}
return reason_mapping.get(reason, FinishReason.UNKNOWN)
def make_api_request(
prompt: str,
api_key: str,
model: str = "gpt-4.1",
max_retries: int = 3
) -> APIResponse:
"""
Make API request with comprehensive empty response detection.
Uses HolySheep AI relay for cost optimization.
"""
base_url = "https://api.holysheep.ai/v1"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": 2048,
"temperature": 0.7
}
for attempt in range(max_retries):
try:
response = requests.post(
f"{base_url}/chat/completions",
headers=headers,
json=payload,
timeout=30
)
response.raise_for_status()
data = response.json()
# Extract choice data
if not data.get("choices"):
return APIResponse(
content="",
finish_reason=FinishReason.UNKNOWN,
content_filtered=True,
filter_details={"error": "No choices returned"},
raw_response=data
)
choice = data["choices"][0]
finish_reason_str = choice.get("finish_reason", "unknown")
finish_reason = parse_finish_reason(finish_reason_str)
# Handle content_filter result (OpenAI format)
content_filter_result = choice.get("content_filter_result", {})
content_filtered = content_filter_result.get("filtered", False)
# Handle refusal (Claude format)
refusal = choice.get("refusal", None)
if refusal:
content_filtered = True
# Extract message content
message = choice.get("message", {})
content = message.get("content", "")
return APIResponse(
content=content,
finish_reason=finish_reason,
content_filtered=content_filtered,
filter_details=content_filter_result,
raw_response=data
)
except requests.exceptions.Timeout:
if attempt < max_retries - 1:
time.sleep(2 ** attempt) # Exponential backoff
continue
raise Exception("Request timed out after retries")
except requests.exceptions.RequestException as e:
raise Exception(f"API request failed: {str(e)}")
def handle_response(response: APIResponse) -> str:
"""
Handle API response based on finish_reason and content_filter.
Returns appropriate message or raises informative exception.
"""
if response.is_empty:
if response.finish_reason == FinishReason.CONTENT_FILTER:
details = response.filter_details or {}
filter_reason = details.get("filtered", True)
raise ContentFilteredException(
f"Content was filtered. Details: {details}"
)
elif response.finish_reason == FinishReason.REFUSAL:
raise RefusalException(
"Model refused to generate content for this request"
)
elif response.finish_reason == FinishReason.LENGTH:
raise TokenLimitException(
"Response truncated due to token limit"
)
elif response.finish_reason == FinishReason.TOOL_CALLS:
return "[Tool call detected - handle separately]"
return response.content
class ContentFilteredException(Exception):
"""Raised when content is filtered by safety systems."""
pass
class RefusalException(Exception):
"""Raised when model refuses the request."""
pass
class TokenLimitException(Exception):
"""Raised when response exceeds token limit."""
pass
Advanced Multi-Provider Implementation
Different providers expose filtering through different APIs. Here's a unified handler:
import requests
from typing import Dict, Any, Optional
from abc import ABC, abstractmethod
class ProviderResponseParser(ABC):
"""Abstract base for provider-specific response parsing."""
@abstractmethod
def parse(self, raw_response: Dict[str, Any]) -> Dict[str, Any]:
"""Parse raw response into standardized format."""
pass
@abstractmethod
def is_filtered(self, parsed: Dict[str, Any]) -> bool:
"""Check if response was filtered."""
pass
class OpenAIResponseParser(ProviderResponseParser):
"""Parse OpenAI-compatible responses."""
def parse(self, raw_response: Dict[str, Any]) -> Dict[str, Any]:
choice = raw_response.get("choices", [{}])[0]
message = choice.get("message", {})
filter_result = choice.get("content_filter_result", {})
return {
"content": message.get("content", ""),
"finish_reason": choice.get("finish_reason", "unknown"),
"filtered": filter_result.get("filtered", False),
"filter_reason": filter_result.get("finish_reason", None),
"usage": raw_response.get("usage", {}),
"model": raw_response.get("model", "unknown"),
"id": raw_response.get("id", "")
}
def is_filtered(self, parsed: Dict[str, Any]) -> bool:
return parsed["filtered"] or parsed["finish_reason"] == "content_filter"
class ClaudeResponseParser(ProviderResponseParser):
"""Parse Claude-specific responses."""
def parse(self, raw_response: Dict[str, Any]) -> Dict[str, Any]:
content = raw_response.get("content", [{}])[0] if raw_response.get("content") else {}
# Check for refusal
is_refusal = content.get("type") == "refusal"
return {
"content": content.get("text", "") if not is_refusal else "",
"finish_reason": "refusal" if is_refusal else "stop",
"filtered": is_refusal,
"filter_reason": content.get("reasoning", None) if is_refusal else None,
"usage": raw_response.get("usage", {}),
"model": raw_response.get("model", "unknown"),
"id": raw_response.get("id", "")
}
def is_filtered(self, parsed: Dict[str, Any]) -> bool:
return parsed["filtered"]
class HolySheepRelay:
"""
HolySheep AI relay for unified multi-provider access.
Routes requests to optimal provider based on cost/performance.
Features:
- Single API key for all providers
- Automatic provider fallback
- Built-in content filtering detection
- Sub-50ms relay latency
- 85%+ cost savings vs direct API access
"""
BASE_URL = "https://api.holysheep.ai/v1"
PROVIDER_MODELS = {
"openai": ["gpt-4.1", "gpt-4o", "gpt-4o-mini"],
"anthropic": ["claude-sonnet-4-5", "claude-opus-4"],
"google": ["gemini-2.5-flash", "gemini-2.0-pro"],
"deepseek": ["deepseek-v3.2", "deepseek-coder-v2"]
}
def __init__(self, api_key: str):
self.api_key = api_key
self._parsers = {
"openai": OpenAIResponseParser(),
"anthropic": ClaudeResponseParser(),
"google": OpenAIResponseParser(), # Google uses OpenAI-compatible format
"deepseek": OpenAIResponseParser()
}
def _detect_provider(self, model: str) -> str:
"""Detect provider from model name."""
for provider, models in self.PROVIDER_MODELS.items():
if any(m in model.lower() for m in models):
return provider
return "openai" # Default
def chat_completions(
self,
messages: list,
model: str = "gpt-4.1",
temperature: float = 0.7,
max_tokens: int = 2048
) -> Dict[str, Any]:
"""
Send chat completion request through HolySheep relay.
Args:
messages: List of message dicts with 'role' and 'content'
model: Model identifier (auto-detected provider)
temperature: Sampling temperature
max_tokens: Maximum tokens in response
Returns:
Parsed response with standardized format
"""
provider = self._detect_provider(model)
parser = self._parsers[provider]
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens
}
response = requests.post(
f"{self.BASE_URL}/chat/completions",
headers=headers,
json=payload,
timeout=30
)
response.raise_for_status()
raw = response.json()
parsed = parser.parse(raw)
# Add HolySheep metadata
parsed["_holysheep"] = {
"provider": provider,
"relay_latency_ms": response.elapsed.total_seconds() * 1000,
"cost_optimized": True
}
return parsed
def handle_empty_response(self, response: Dict[str, Any]) -> None:
"""
Diagnose and handle empty responses with detailed logging.
"""
content = response.get("content", "")
if not content:
finish_reason = response.get("finish_reason", "unknown")
filtered = response.get("filtered", False)
filter_reason = response.get("filter_reason", "unspecified")
error_msg = f"Empty response received. "
error_msg += f"Finish reason: {finish_reason}. "
if filtered:
error_msg += f"Content was filtered. Reason: {filter_reason}"
raise ContentFilteredException(error_msg)
else:
error_msg += "No content generated."
raise EmptyResponseException(error_msg)
def example_usage():
"""Demonstrate HolySheep relay usage."""
api_key = "YOUR_HOLYSHEEP_API_KEY"
client = HolySheepRelay(api_key)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain quantum entanglement in simple terms."}
]
try:
# Route to cost-optimal provider (DeepSeek for this use case)
response = client.chat_completions(
messages=messages,
model="deepseek-v3.2", # $0.42/MTok output
max_tokens=500
)
# Check for empty/filtered responses
if not response.get("content"):
client.handle_empty_response(response)
print(f"Response: {response['content']}")
print(f"Provider: {response['_holysheep']['provider']}")
print(f"Relay latency: {response['_holysheep']['relay_latency_ms']:.2f}ms")
except ContentFilteredException as e:
print(f"Content filtered: {e}")
except EmptyResponseException as e:
print(f"Empty response: {e}")
class EmptyResponseException(Exception):
"""Raised when response is unexpectedly empty."""
pass
Common Errors & Fixes
Error 1: Empty Response with finish_reason="stop"
Symptom: API returns an empty string but finish_reason shows "stop" (not "content_filter").
Root Cause: This typically occurs when:
- System prompt conflicts with user request
- Temperature is set to 0 with greedy sampling issues
- Max tokens is set too low (model generates nothing within limit)
- Provider returns an unparseable response format
Fix:
# Increase max_tokens and verify response parsing
response = client.chat_completions(
messages=messages,
model="deepseek-v3.2",
max_tokens=2048, # Ensure sufficient capacity
temperature=0.7 # Non-zero for diverse generation
)
Manual validation
if not response.get("content") and response.get("finish_reason") == "stop":
# Retry with adjusted parameters
response = client.chat_completions(
messages=messages,
model="deepseek-v3.2",
max_tokens=1024,
temperature