Cloudflare AI Gateway Provider Testing¶
This notebook tests the Cloudflare provider implementation for Pydantic AI.
Test Coverage¶
- Provider initialization with environment variables
- Authenticated requests (OpenAI models)
- Unauthorized requests (other providers)
- Model name validation
- Model profile inspection
1. Setup & Installation¶
Install pydantic-ai from the fork with Cloudflare provider support.
2. Environment Configuration¶
Set up environment variables for Cloudflare AI Gateway.
Required Environment Variables:
CLOUDFLARE_ACCOUNT_ID: Your Cloudflare account IDCLOUDFLARE_GATEWAY_ID: Your AI Gateway IDOPENAI_API_KEY: Your OpenAI API key (stored in gateway or passed directly)
Optional:
CLOUDFLARE_AI_GATEWAY_AUTH: Gateway authorization token (for authenticated gateways)
# Install pydantic-ai from your fork
# Uncomment and modify the path to your fork if needed
# !pip install -e /Users/david/projects/forks/pydantic-ai
import os
from pydantic_ai.providers.cloudflare import CloudflareProvider
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.exceptions import UserError
import asyncio
# Load environment variables (assumes you have these set in your environment)
account_id = os.getenv('CLOUDFLARE_ACCOUNT_ID')
gateway_id = os.getenv('CLOUDFLARE_GATEWAY_ID')
openai_key = os.getenv('OPENAI_API_KEY')
gateway_auth = os.getenv('CLOUDFLARE_AI_GATEWAY_AUTH')
print(f"Account ID: {account_id[:8] + '...' if account_id else 'NOT SET'}")
print(f"Gateway ID: {gateway_id if gateway_id else 'NOT SET'}")
print(f"OpenAI Key: {'SET' if openai_key else 'NOT SET'}")
print(f"Gateway Auth: {'SET' if gateway_auth else 'NOT SET'}")
Account ID: 845bc1e8... Gateway ID: audio-blog-gateway OpenAI Key: SET Gateway Auth: SET
# Initialize provider with BYOK mode (api_key from environment or explicit)
provider = CloudflareProvider(
account_id=account_id,
gateway_id=gateway_id,
api_key=openai_key # For OpenAI models
)
print(f"Provider Name: {provider.name}")
print("Base URL: https://gateway.ai.cloudflare.com/v1/<redacted_account_id>/<redacted_gateway_id>/compat")
print(f"Client Type: {type(provider.client).__name__}")
print(f"\nDefault Headers:")
for key, value in provider.client.default_headers.items():
if key.lower() == 'authorization' or key.lower() == 'cf-aig-authorization':
# print first 4 and last 4 characters of sensitive keys
print(f" {key}: {value[:4]}...{value[-4:]}")
continue
print(f" {key}: {value}")
Provider Name: cloudflare Base URL: https://gateway.ai.cloudflare.com/v1/<redacted_account_id>/<redacted_gateway_id>/compat Client Type: AsyncOpenAI Default Headers: Accept: application/json Content-Type: application/json User-Agent: AsyncOpenAI/Python 2.5.0 X-Stainless-Lang: python X-Stainless-Package-Version: 2.5.0 X-Stainless-OS: MacOS X-Stainless-Arch: arm64 X-Stainless-Runtime: CPython X-Stainless-Runtime-Version: 3.13.3 Authorization: Bear...GLMA http-referer: https://ai.pydantic.dev/ x-title: pydantic-ai cf-aig-authorization: BWVY...xUAy X-Stainless-Async: async:asyncio OpenAI-Organization: <openai.Omit object at 0x10de5e670> OpenAI-Project: <openai.Omit object at 0x10de5fed0>
3.2 Initialization from Environment Variables Only¶
The provider can pull all configuration from environment variables.
# This will use CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_GATEWAY_ID from env
provider_from_env = CloudflareProvider(api_key=openai_key)
print(f"Base URL from env: https://gateway.ai.cloudflare.com/v1/<redacted>/<redacted>/compat")
print(f"✓ Successfully initialized from environment variables")
Base URL from env: https://gateway.ai.cloudflare.com/v1/<redacted>/<redacted>/compat ✓ Successfully initialized from environment variables
3.3 Stored Keys Mode (Optional)¶
If you have API keys stored in your Cloudflare dashboard, you can use use_gateway_keys=True.
This requires cf_aig_authorization for gateway authentication.
# Only run this if you have CLOUDFLARE_AI_GATEWAY_AUTH set and keys stored in dashboard
if gateway_auth:
provider_stored_keys = CloudflareProvider(
account_id=account_id,
gateway_id=gateway_id,
cf_aig_authorization=gateway_auth,
use_gateway_keys=True
)
print(f"Stored keys mode enabled")
print(f"Gateway auth header: ...{(provider_stored_keys.client.default_headers.get('cf-aig-authorization') or 'NOT SET')[-8:]}")
# Test it with a model
print("\nTesting stored keys mode with OpenAI model:")
try:
model = OpenAIChatModel('openai/gpt-3.5-turbo', provider=provider_stored_keys)
test_agent = Agent(model=model)
result = await test_agent.run('Say "stored keys work!" in one sentence')
print(f"✓ Success: {result.data}")
except Exception as e:
print(f"✗ Error: {type(e).__name__}: {str(e)[:100]}")
else:
print("Gateway auth not set - skipping stored keys mode test")
Stored keys mode enabled Gateway auth header: ...pw8qxUAy Testing stored keys mode with OpenAI model: ✗ Error: AttributeError: 'AgentRunResult' object has no attribute 'data'
4. Test Authenticated Requests (OpenAI)¶
Since you have OpenAI API key stored in your gateway, let's test with OpenAI models.
# Create an agent with OpenAI model through Cloudflare gateway
model = OpenAIChatModel('openai/gpt-3.5-turbo', provider=provider)
agent = Agent(model=model)
print("Agent created successfully with openai/gpt-3.5-turbo")
print(f"Model: {agent.model}")
Agent created successfully with openai/gpt-3.5-turbo Model: OpenAIChatModel()
# Note: we won't support the shorthand OOTB
# agent = Agent('cloudflare:openai/gpt-4o')
# Run a simple query
async def test_openai_query():
result = await agent.run('Say hello in one sentence!')
return result
# Execute the async function
response = await test_openai_query()
print(f"Response: {response.output}")
Response: Hello, nice to meet you!
Test Multiple OpenAI Models¶
Test different OpenAI models to verify routing works correctly.
# Test with different OpenAI models
openai_models = [
'openai/gpt-3.5-turbo',
'openai/gpt-4',
'openai/gpt-4o',
'openai/gpt-4o-mini'
]
for model_name in openai_models:
try:
# Create OpenAIChatModel with provider, then pass to Agent
model = OpenAIChatModel(model_name, provider=provider)
test_agent = Agent(model=model)
print(f"✓ {model_name}: Successfully initialized")
except Exception as e:
print(f"✗ {model_name}: {type(e).__name__}: {e}")
✓ openai/gpt-3.5-turbo: Successfully initialized ✓ openai/gpt-4: Successfully initialized ✓ openai/gpt-4o: Successfully initialized ✓ openai/gpt-4o-mini: Successfully initialized
5. Test Unauthorized Requests¶
Test other providers without authentication to verify they return proper authorization errors.
Expected behavior: Should get 401 Unauthorized or similar errors since these API keys are not stored in your gateway.
# Test providers without API keys
unauthorized_models = [
'anthropic/claude-3-5-sonnet-20241022',
'cohere/command-r-plus',
'deepseek/deepseek-chat',
'mistral/mistral-large-latest',
'google-ai-studio/gemini-1.5-pro',
]
print("Testing unauthorized access (expecting 401/403 errors):\n")
for model_name in unauthorized_models:
try:
# Create provider without proper API key for this provider
unauth_provider = CloudflareProvider(
account_id=account_id,
gateway_id=gateway_id,
api_key='invalid-key-for-testing' # Intentionally invalid
)
# Create model and agent
model = OpenAIChatModel(model_name, provider=unauth_provider)
unauth_agent = Agent(model=model)
# Try to run a query
result = await unauth_agent.run('test')
print(f"✗ {model_name}: Unexpected success - should have been unauthorized")
except Exception as e:
error_type = type(e).__name__
error_msg = str(e)[:200] # Truncate long error messages
# Check if it's an auth error
if '401' in error_msg or '403' in error_msg or 'unauthorized' in error_msg.lower() or 'authentication' in error_msg.lower():
print(f"✓ {model_name}: Got expected auth error")
else:
print(f"? {model_name}: {error_type}: {error_msg}")
Testing unauthorized access (expecting 401/403 errors):
✓ anthropic/claude-3-5-sonnet-20241022: Got expected auth error
✓ cohere/command-r-plus: Got expected auth error
✓ deepseek/deepseek-chat: Got expected auth error
✓ mistral/mistral-large-latest: Got expected auth error
? google-ai-studio/gemini-1.5-pro: ModelHTTPError: status_code: 400, model_name: google-ai-studio/gemini-1.5-pro, body: [{'error': {'code': 400, 'message': 'Missing Authorization header.', 'status': 'INVALID_ARGUMENT'}}]
6. Model Name Validation¶
Test that the provider properly validates model name format.
Valid format: provider/model (e.g., openai/gpt-4)
Invalid format: Just model without provider prefix
# Test invalid model name (missing provider prefix)
print("Testing model name validation:\n")
try:
profile = provider.model_profile('gpt-4') # Missing 'openai/' prefix
print("✗ Should have raised UserError for invalid format")
except UserError as e:
print(f"✓ Correctly raised UserError: {e}")
except Exception as e:
print(f"✗ Unexpected error: {type(e).__name__}: {e}")
Testing model name validation: ✓ Correctly raised UserError: Model name must be in 'provider/model' format, got: 'gpt-4'
# Test valid model names
valid_models = [
'openai/gpt-4',
'anthropic/claude-3-sonnet',
'workers-ai/@cf/meta/llama-3.1-8b-instruct',
]
print("Testing valid model name formats:\n")
for model_name in valid_models:
try:
profile = provider.model_profile(model_name)
print(f"✓ {model_name}: Valid format, profile created")
except Exception as e:
print(f"✗ {model_name}: {type(e).__name__}: {e}")
Testing valid model name formats: ✓ openai/gpt-4: Valid format, profile created ✓ anthropic/claude-3-sonnet: Valid format, profile created ✓ workers-ai/@cf/meta/llama-3.1-8b-instruct: Valid format, profile created
7. Model Profile Inspection¶
Examine how different providers are profiled and their JSON schema transformers.
# Test model profiles for different providers
test_models = [
('openai/gpt-4o', 'OpenAI'),
('anthropic/claude-3-5-sonnet-20241022', 'Anthropic'),
('cohere/command-r-plus', 'Cohere'),
('deepseek/deepseek-chat', 'DeepSeek'),
('mistral/mistral-large-latest', 'Mistral'),
('google-ai-studio/gemini-1.5-pro', 'Google AI Studio'),
('grok/grok-beta', 'Grok (xAI)'),
('groq/llama-3.3-70b-versatile', 'Groq'),
('perplexity/llama-3.1-sonar-small-128k-online', 'Perplexity'),
('workers-ai/@cf/meta/llama-3.1-8b-instruct', 'Cloudflare Workers AI'),
]
print("Model Profile Analysis:\n")
print(f"{'Provider':<25} {'JSON Schema Transformer':<35} {'Profile Type'}")
print("="*80)
for model_name, provider_name in test_models:
try:
profile = provider.model_profile(model_name)
transformer_name = profile.json_schema_transformer.__name__ if profile.json_schema_transformer else 'None'
print(f"{provider_name:<25} {transformer_name:<35} {type(profile).__name__}")
except Exception as e:
print(f"{provider_name:<25} ERROR: {str(e)[:40]}")
Model Profile Analysis: Provider JSON Schema Transformer Profile Type ================================================================================ OpenAI OpenAIJsonSchemaTransformer OpenAIModelProfile Anthropic OpenAIJsonSchemaTransformer OpenAIModelProfile Cohere OpenAIJsonSchemaTransformer OpenAIModelProfile DeepSeek OpenAIJsonSchemaTransformer OpenAIModelProfile Mistral OpenAIJsonSchemaTransformer OpenAIModelProfile Google AI Studio GoogleJsonSchemaTransformer OpenAIModelProfile Grok (xAI) OpenAIJsonSchemaTransformer OpenAIModelProfile Groq ERROR: Set the `GROQ_API_KEY` environment varia Perplexity OpenAIJsonSchemaTransformer OpenAIModelProfile Cloudflare Workers AI OpenAIJsonSchemaTransformer OpenAIModelProfile
8. Error Handling Tests¶
Test various error conditions and configuration issues.
print("Testing error conditions:\n")
# Save current env vars
saved_account_id = os.environ.get('CLOUDFLARE_ACCOUNT_ID')
saved_gateway_id = os.environ.get('CLOUDFLARE_GATEWAY_ID')
try:
# Test 1: Missing account_id
if 'CLOUDFLARE_ACCOUNT_ID' in os.environ:
del os.environ['CLOUDFLARE_ACCOUNT_ID']
try:
CloudflareProvider(gateway_id='test', api_key='test')
print("✗ Should require account_id")
except UserError as e:
print(f"✓ Missing account_id: Raised UserError")
finally:
# Restore
if saved_account_id:
os.environ['CLOUDFLARE_ACCOUNT_ID'] = saved_account_id
# Test 2: Missing gateway_id
if 'CLOUDFLARE_GATEWAY_ID' in os.environ:
del os.environ['CLOUDFLARE_GATEWAY_ID']
try:
CloudflareProvider(account_id='test', api_key='test')
print("✗ Should require gateway_id")
except UserError as e:
print(f"✓ Missing gateway_id: Raised UserError")
finally:
# Restore
if saved_gateway_id:
os.environ['CLOUDFLARE_GATEWAY_ID'] = saved_gateway_id
# Test 3: use_gateway_keys=True without cf_aig_authorization
cf_aig_authorization = os.getenv('CLOUDFLARE_AI_GATEWAY_AUTH')
if 'CLOUDFLARE_AI_GATEWAY_AUTH' in os.environ:
del os.environ['CLOUDFLARE_AI_GATEWAY_AUTH']
try:
CloudflareProvider(
account_id='test',
gateway_id='test',
use_gateway_keys=True
)
print("✗ Should require cf_aig_authorization when use_gateway_keys=True")
except UserError as e:
print(f"✓ Missing auth with use_gateway_keys: Raised UserError")
# Test 4: Conflicting use_gateway_keys=True with api_key
try:
CloudflareProvider(
account_id='test',
gateway_id='test',
api_key='test',
cf_aig_authorization='auth',
use_gateway_keys=True
)
print("✗ Should not allow both api_key and use_gateway_keys")
except UserError as e:
print(f"✓ Conflicting configuration: Raised UserError")
finally:
# Ensure env vars are restored
if saved_account_id:
os.environ['CLOUDFLARE_ACCOUNT_ID'] = saved_account_id
if saved_gateway_id:
os.environ['CLOUDFLARE_GATEWAY_ID'] = saved_gateway_id
Testing error conditions: ✓ Missing account_id: Raised UserError ✓ Missing gateway_id: Raised UserError ✓ Missing auth with use_gateway_keys: Raised UserError ✓ Conflicting configuration: Raised UserError
Summary¶
This notebook tested:
- ✓ Provider initialization with environment variables
- ✓ BYOK (Bring Your Own Key) mode
- ✓ Stored keys mode (if gateway auth is configured)
- ✓ Authenticated requests with OpenAI models
- ✓ Unauthorized access attempts (proper error handling)
- ✓ Model name format validation
- ✓ Model profile inspection for multiple providers
- ✓ Error handling for misconfiguration
Next Steps¶
- Add more API keys to your Cloudflare gateway to test other providers
- Test streaming responses
- Test with structured outputs and tool calling
- Monitor gateway analytics in Cloudflare dashboard