Robust error handling OpenAI function calls Python requires more than wrapping the SDK call in a bare try/except. The model can emit malformed arguments, the API can throttle or drop connections, and your local tool implementation can raise—each failure mode demands a distinct recovery path.
Step 1: Build a minimal function-calling loop
Start with the official openai package and a single tool. The loop sends messages, gets a response, and if the model requests a function, executes it and feeds the result back. Keep the initial version dumb: no retries, no validation. You need a known-good baseline before you can reason about failures.
from openai import OpenAI
client = OpenAI() # reads OPENAI_API_KEY from env
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current temperature for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
def get_weather(city: str) -> str:
# stub: in reality this hits a weather API
return f"22C in {city}"
messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
)
msg = resp.choices[0].message
If msg.tool_calls is populated, you must append a tool role message with the matching tool_call_id before the next request. Effective error handling OpenAI function calls Python starts by classifying what actually breaks in this loop: the network, the API server, the model output, or your local function.
Step 2: Catch transport and API errors explicitly
The SDK raises a hierarchy of exceptions. Catch the specific ones you can recover from: RateLimitError, APITimeoutError, APIConnectionError. A APIStatusError with a 5xx status indicates server-side issues; 4xx (except 429) usually means your request is malformed or unauthorized.
from openai import (
OpenAI,
RateLimitError,
APITimeoutError,
APIConnectionError,
APIStatusError,
)
def safe_create(client, **kwargs):
try:
return client.chat.completions.create(**kwargs)
except RateLimitError as e:
# inspect e.response.headers.get("retry-after")
raise
except (APITimeoutError, APIConnectionError) as e:
# transient network issue, safe to retry
raise
except APIStatusError as e:
if e.status_code >= 500:
raise # retry with backoff
else:
# 400/401/403/404: do not retry blindly
print(f"Fatal API error {e.status_code}: {e.response}")
raise
Never catch Exception broadly; it hides bugs in your own tool code. For error handling OpenAI function calls Python, surface the right exception to a retry layer and let fatal errors crash fast.
Step 3: Validate function arguments with a schema
Models hallucinate parameter shapes. A string field becomes a number, a required key vanishes. Use pydantic to parse the arguments JSON string. If validation fails, return a tool message describing the error so the model can self-correct on the next turn.
from pydantic import BaseModel, ValidationError
class WeatherArgs(BaseModel):
city: str
def execute_tool_call(name: str, arguments: str) -> str:
if name == "get_weather":
try:
args = WeatherArgs.model_validate_json(arguments)
except ValidationError as e:
return f"Invalid arguments: {e.errors()}. Provide a JSON object with a 'city' string."
return get_weather(args.city)
return f"Unknown function: {name}"
Feed the result back:
if msg.tool_calls:
for call in msg.tool_calls:
result = execute_tool_call(call.function.name, call.function.arguments)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result,
})
This converts a parse failure from a crash into a second model turn. That loop is the core of error handling OpenAI function calls Python: let the model fix its own mistakes when the schema is violated.
Step 4: Handle unknown functions and refusals
If the model calls a function you didn’t register, do not invent a result. Return an explicit error string. Also watch finish_reason. A value of content_filter means the provider blocked the response; log it and return a safe fallback.
if msg.tool_calls:
for call in msg.tool_calls:
if call.function.name not in {"get_weather"}:
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": f"Function {call.function.name} is not available.",
})
continue
# normal execution path
elif resp.choices[0].finish_reason == "content_filter":
messages.append({"role": "assistant", "content": "I can't answer that."})
Unknown-function handling prevents the common production bug where a stale prompt schema drifts from your registered tools.
Step 5: Retry local tool execution with idempotency
The code inside your tool can fail independently of the LLM. Use tenacity to retry, but only if the tool is idempotent. For non-idempotent actions (payments, emails), require an idempotency_key. Generate one from tool_call_id so repeated calls with the same ID reuse the cached outcome.
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10))
def get_weather_with_retry(city: str) -> str:
# external call that may flap
return get_weather(city)
def execute_idempotent(name, arguments, tool_call_id):
if name == "get_weather":
args = WeatherArgs.model_validate_json(arguments)
cache_key = f"{name}:{tool_call_id}"
if cache_key in _cache:
return _cache[cache_key]
out = get_weather_with_retry(args.city)
_cache[cache_key] = out
return out
That discipline separates robust error handling OpenAI function calls Python from naive scripts that double-charge a credit card on the third retry.
Step 6: Use a gateway for automatic provider fallback
A single provider outage should not 500 your app. An OpenAI-compatible endpoint like n4n.ai addresses 240+ models and performs automatic fallback when a provider is rate-limited or degraded, while honoring your routing directives and forwarding provider cache-control hints. Swap the base_url and keep the same code.
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.n4n.ai/v1",
api_key=os.environ["N4N_API_KEY"],
)
Because the interface is identical, the exception handling and validation above still apply; you gain resilience without rewriting retries. Per-token usage metering also lets you cap spend per request.
Step 7: Verify the full flow with an integration test
Write a pytest that exercises the happy path and a validation failure. Mock the API or point at a cheap model with a deterministic prompt. The test must prove that malformed arguments produce a recoverable tool message rather than an unhandled exception.
def test_validation_recovery(monkeypatch):
import my_module
bad = '{"city": 123}' # pydantic rejects int
good = '{"city": "Paris"}'
# direct unit check on the executor
r1 = my_module.execute_tool_call("get_weather", bad)
assert "Invalid arguments" in r1
r2 = my_module.execute_tool_call("get_weather", good)
assert "Paris" in r2
# simulate unknown function
r3 = my_module.execute_tool_call("delete_db", "{}")
assert "Unknown function" in r3
Run pytest -q. Success means: malformed arguments trigger a model-correctable error, unknown functions are reported, and API errors propagate to your retry layer instead of crashing the loop.
Verification checklist
- Model returns valid JSON: tool executes, user gets answer.
- Model returns wrong types: pydantic catches, model gets error, retries.
- API returns 429/500: exception bubbles, caller backs off.
- Provider down: gateway fails over, request succeeds.
- Test suite passes locally and in CI.
That is the complete pattern for error handling OpenAI function calls Python in production. The key is to treat the model as a fallible client and your tools as untrusted input, then encode each failure mode as a typed branch instead of a generic catch-all.