Module 1: Fundamentals of Prompt Engineering
7. Providers and Behavioral Differences
Capsule overview
OpenAI, Anthropic and Google respond differently to the same prompt. The system prompt that produces perfect results with GPT-4o may need adjustments on Claude 3.5. In this capsule you'll see how each provider processes prompts, their measurable behavioral differences, and how to build portable prompts that work on all three.
You'll also learn adapter patterns: abstractions that normalize the differences between providers without having to duplicate your prompting logic. This is especially relevant in production, where a fallback strategy (if OpenAI has an incident, reroute to Anthropic) requires your prompts to be portable.
Why it matters: In production you may need to fall back between providers for availability or cost, or work with clients who demand a specific provider. If your prompts assume OpenAI only, a migration is a full redesign. If your prompts are portable by design, the migration is configuration.
API architecture by provider
Before comparing behavior, understand how each API structures messages:
OpenAI (GPT-4o, GPT-4o-mini)
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
client = OpenAI() # Reads OPENAI_API_KEY from .env
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
# System prompt as a message with the "system" role
{"role": "system", "content": "You are a sentiment classifier."},
# Conversation history interleaved
{"role": "user", "content": "What do you think of this text?"},
{"role": "assistant", "content": "I need to see the text first."},
# The user's current message
{"role": "user", "content": "The service was excellent."}
],
temperature=0,
max_tokens=10,
# Force valid JSON (native in OpenAI)
# response_format={"type": "json_object"}
)
# Accessing the output
text = response.choices[0].message.content
tokens_used = response.usage.total_tokens
print(f"Output: {text}")
print(f"Tokens: {tokens_used}")
Characteristics:
- Roles:
system,user,assistant - System prompt: a message of type
"role": "system"— well respected - JSON mode: native with
response_format={"type": "json_object"}or{"type": "json_schema", ...} - Temperature: 0.0-2.0 (default 1.0 in the API, many clients use 0.7)
- Respecting "only X": Very consistent, especially with temperature=0
Anthropic (Claude 3.5 Sonnet, Claude 3 Haiku)
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv()
client = Anthropic() # Reads ANTHROPIC_API_KEY from .env
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=10, # REQUIRED in Anthropic (there is no default)
# System prompt as a separate parameter, NOT as a message
system="You are a sentiment classifier. Respond only with POSITIVE or NEGATIVE.",
messages=[
# Only "user" and "assistant" — there is no "system" in messages[]
{"role": "user", "content": "The service was excellent."}
],
temperature=0
)
# Accessing the output — a different structure than OpenAI
text = response.content[0].text
input_tokens = response.usage.input_tokens
output_tokens = response.usage.output_tokens
print(f"Output: {text}")
print(f"Tokens input/output: {input_tokens}/{output_tokens}")
Characteristics:
- Roles in
messages[]: onlyuserandassistant(no "system") - System prompt: a separate
system=parameter in the request - JSON mode: through instructions in the prompt (not native like OpenAI)
max_tokens: required (no default)- XML tags: Claude follows them especially well (
<output>...</output>) - Behavior: excellent at following long instructions; sometimes more "conversational" on short answers
Google (Gemini 1.5 Flash, Gemini 1.5 Pro)
import google.generativeai as genai
import os
from dotenv import load_dotenv
load_dotenv()
genai.configure(api_key=os.environ["GOOGLE_API_KEY"])
# Configure the model with a system instruction
model = genai.GenerativeModel(
model_name="gemini-1.5-flash",
system_instruction="You are a sentiment classifier. Respond only with POSITIVE or NEGATIVE."
)
response = model.generate_content(
"The service was excellent.",
generation_config=genai.types.GenerationConfig(
temperature=0,
max_output_tokens=10,
# JSON mode available:
# response_mime_type="application/json"
)
)
text = response.text
print(f"Output: {text}")
Characteristics:
- Roles:
userandmodel(equivalent to "assistant") - System instruction: a separate parameter at model initialization
- JSON mode:
response_mime_type="application/json"in generation_config - Behavior: good at technical tasks; can be more verbose by default on open-ended answers
Behavioral differences: a controlled experiment
The same prompt, the three providers:
from openai import OpenAI
from anthropic import Anthropic
import google.generativeai as genai
import os
from dotenv import load_dotenv
load_dotenv()
# Initialize the clients
oai_client = OpenAI()
ant_client = Anthropic()
genai.configure(api_key=os.environ["GOOGLE_API_KEY"])
gem_model = genai.GenerativeModel(
"gemini-1.5-flash",
system_instruction="Classify sentiment. Respond ONLY with POSITIVE, NEGATIVE, or NEUTRAL. One word only."
)
SYSTEM = "Classify sentiment. Respond ONLY with POSITIVE, NEGATIVE, or NEUTRAL. One word only."
test_inputs = [
"I loved the product, excellent quality",
"Terrible service, I'll never buy here again",
"The product arrived, it works fine",
"I'm quite disappointed with the result",
"So-so, nothing special"
]
def classify_openai(text: str) -> str:
r = oai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": text}
],
temperature=0,
max_tokens=10
)
return r.choices[0].message.content.strip()
def classify_anthropic(text: str) -> str:
r = ant_client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=10,
system=SYSTEM,
messages=[{"role": "user", "content": text}]
)
return r.content[0].text.strip()
def classify_google(text: str) -> str:
r = gem_model.generate_content(
text,
generation_config=genai.types.GenerationConfig(temperature=0, max_output_tokens=10)
)
return r.text.strip()
print(f"{'Input':<45} {'OpenAI':<12} {'Anthropic':<12} {'Google':<12}")
print("-" * 85)
for text in test_inputs:
oai = classify_openai(text)
ant = classify_anthropic(text)
goo = classify_google(text)
print(f"{text[:44]:<45} {oai:<12} {ant:<12} {goo:<12}")
Typical output:
Input OpenAI Anthropic Google
-------------------------------------------------------------------------------------
I loved the product, excellent quality POSITIVE POSITIVE POSITIVE
Terrible service, I'll never buy here again NEGATIVE NEGATIVE NEGATIVE
The product arrived, it works fine POSITIVE POSITIVE POSITIVE
I'm quite disappointed with the result NEGATIVE NEGATIVE NEGATIVE
So-so, nothing special NEUTRAL NEUTRAL NEUTRAL
For simple classification with clear instructions, the three are consistent. The differences show up in cases of subtler behavior:
Table of key differences
| Aspect | OpenAI | Anthropic | |
|---|---|---|---|
| System prompt location | messages[0] with role: system | A separate system= parameter | system_instruction= at model init |
max_tokens required | No (it has a default) | Yes (no default, it raises an error) | No (it has a default) |
| Native JSON mode | ✅ response_format | Partial (with instructions) | ✅ response_mime_type |
| XML tags for structure | Works | Excellent (optimized for it) | Works |
| Respecting "only X words" | High | High | Medium-High |
| Very long system prompt (5k+ tokens) | Well supported | Excellent (200k context) | Fine |
| Temperature range | 0.0-2.0 | 0.0-1.0 | 0.0-2.0 |
| Roles in messages | system/user/assistant | user/assistant | user/model |
Behavioral differences in practice:
Claude is more "conversational" by default:
# If the system doesn't impose strict constraints:
# OpenAI → "NEGATIVE"
# Claude → "NEGATIVE" or sometimes "Negative. The text shows frustration."
# Fix for Claude: reinforce it in the Personality
SYSTEM_CLAUDE = """
You are a sentiment classifier.
Respond ONLY with the category. No punctuation. No additional text.
Correct category: POSITIVE
Incorrect category: "The sentiment is POSITIVE because..."
"""
Google can be more verbose on open-ended answers:
# For generation tasks (not classification), Google tends to give more context
# Fix: add an explicit brevity instruction
SYSTEM_GOOGLE = """
Classify the sentiment. One word. No justification.
"""
Adapter patterns for multi-provider
Pattern 1: A unified wrapper with a common interface
from openai import OpenAI
from anthropic import Anthropic
from typing import Literal
ProviderType = Literal["openai", "anthropic"]
class LLMClient:
"""A wrapper that normalizes the interface between providers."""
def __init__(self):
self.openai = OpenAI()
self.anthropic = Anthropic()
def complete(
self,
system: str,
user: str,
provider: ProviderType = "openai",
temperature: float = 0,
max_tokens: int = 100
) -> str:
"""A unified interface for text completion."""
if provider == "openai":
r = self.openai.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user}
],
temperature=temperature,
max_tokens=max_tokens
)
return r.choices[0].message.content.strip()
elif provider == "anthropic":
r = self.anthropic.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=max_tokens, # Required in Anthropic
system=system,
messages=[{"role": "user", "content": user}],
temperature=temperature
)
return r.content[0].text.strip()
raise ValueError(f"Unknown provider: {provider}")
# Usage
llm = LLMClient()
system = "Classify sentiment. Only: POSITIVE, NEGATIVE, or NEUTRAL."
user = "The service was excellent"
# Same code, different providers
for provider in ["openai", "anthropic"]:
result = llm.complete(system=system, user=user, provider=provider)
print(f"{provider}: {result}")
Pattern 2: Normalizing output per provider
Providers can return small variations in format (whitespace, punctuation). Normalize before processing:
def normalize_classification(raw: str, valid_categories: list[str]) -> str:
"""
Normalizes the output of any provider into a valid category.
Handles variations: whitespace, punctuation, inconsistent capitalization.
"""
# Clean up whitespace and trailing punctuation
cleaned = raw.strip().rstrip(".,!?").upper()
# Exact match first
if cleaned in valid_categories:
return cleaned
# Partial match (for outputs like "POSITIVE." or "The text is NEGATIVE")
for cat in valid_categories:
if cat in cleaned:
return cat
return "UNKNOWN"
# Test with real outputs from different providers
outputs_raw = [
"POSITIVE", # OpenAI, ideal
"Positive.", # Claude, occasionally
"The sentiment is NEGATIVE", # Claude being conversational
"NEUTRAL\n", # With a newline
" POSITIVE ", # With spaces
]
categories = ["POSITIVE", "NEGATIVE", "NEUTRAL"]
for raw in outputs_raw:
normalized = normalize_classification(raw, categories)
print(f"'{raw}' → '{normalized}'")
Output:
'POSITIVE' → 'POSITIVE'
'Positive.' → 'POSITIVE'
'The sentiment is NEGATIVE' → 'NEGATIVE'
'NEUTRAL\n' → 'NEUTRAL'
' POSITIVE ' → 'POSITIVE'
Pattern 3: Automatic fallback between providers
import time
def complete_with_fallback(
system: str,
user: str,
primary: ProviderType = "openai",
fallback: ProviderType = "anthropic",
max_retries: int = 2
) -> tuple[str, str]:
"""
Tries primary, falls back if it fails.
Returns: (result, provider_used)
"""
llm = LLMClient()
# Try with primary
for attempt in range(max_retries):
try:
result = llm.complete(system=system, user=user, provider=primary)
return result, primary
except Exception as e:
print(f"Primary ({primary}) failed (attempt {attempt + 1}): {e}")
if attempt < max_retries - 1:
time.sleep(1)
# Fallback
print(f"Using fallback: {fallback}")
result = llm.complete(system=system, user=user, provider=fallback)
return result, fallback
# Usage
result, used_provider = complete_with_fallback(
system="Classify sentiment. Only: POSITIVE or NEGATIVE.",
user="I loved the product"
)
print(f"Result: {result} (using: {used_provider})")
System Prompt Compatibility Matrix
To make a prompt portable, use these practices:
| Practice | OpenAI | Anthropic | Portable? | |
|---|---|---|---|---|
| A clear system prompt, up front | ✅ | ✅ | ✅ | ✅ Yes |
response_format for JSON | ✅ Native | ❌ Doesn't exist | ✅ Different syntax | ❌ No |
| XML tags for structure | ✅ Works | ✅ Excellent | ✅ Works | ✅ Yes |
| Examples in the Experiment | ✅ | ✅ | ✅ | ✅ Yes |
| "Respond ONLY with X" | ✅ | ✅ | ✅ | ✅ Yes |
| Non-English instructions | ✅ | ✅ | ✅ | ✅ Yes |
The golden rule for portability: Use explicit instructions in text instead of provider-specific features. If you depend on response_format, implement parsing with retry for the providers that don't support it.
# Portable: The format instruction lives in the text
SYSTEM_PORTABLE = """
Classify sentiment.
Respond in JSON: {"sentiment": "POSITIVE|NEGATIVE|NEUTRAL"}
Example: {"sentiment": "POSITIVE"}
No additional text. Only the JSON.
"""
# Not portable: It depends on an OpenAI feature
# response_format={"type": "json_object"} # OpenAI only
Differences in JSON output
This is the most important practical difference for systems that need structured output:
import json
# OpenAI: native JSON mode guarantees valid JSON
def extract_json_openai(text: str, schema_description: str) -> dict:
r = oai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": f"Extract: {schema_description}. Respond in JSON."},
{"role": "user", "content": text}
],
temperature=0,
response_format={"type": "json_object"} # Guarantees valid JSON
)
return json.loads(r.choices[0].message.content) # Always parseable
# Anthropic: no native JSON mode — you need explicit instructions + retry
def extract_json_anthropic(text: str, schema_description: str, max_retries: int = 2) -> dict:
system = f"""
Extract: {schema_description}
IMPORTANT: Respond ONLY with valid JSON.
No text before or after the JSON.
Example of the correct format: {{"key": "value"}}
"""
for attempt in range(max_retries + 1):
r = ant_client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=200,
system=system,
messages=[{"role": "user", "content": text}],
temperature=0
)
raw = r.content[0].text.strip()
# Clean up markdown if it comes wrapped in ```json ... ```
if raw.startswith("```"):
lines = raw.split("\n")
raw = "\n".join(lines[1:-1] if lines[-1] == "```" else lines[1:])
try:
return json.loads(raw)
except json.JSONDecodeError:
if attempt < max_retries:
# Specific feedback on the retry
system += f"\nThe previous output was not valid JSON: '{raw[:100]}'. Fix the format."
raise ValueError("Could not get valid JSON from Anthropic")
# Comparative test
text = "Contact Ana Martinez at ana.martinez@company.com"
try:
result_oai = extract_json_openai(text, "name and email as {name: str, email: str}")
print(f"OpenAI: {result_oai}")
except Exception as e:
print(f"OpenAI error: {e}")
try:
result_ant = extract_json_anthropic(text, "name and email as {name: str, email: str}")
print(f"Anthropic: {result_ant}")
except Exception as e:
print(f"Anthropic error: {e}")
Recommendations by provider
For OpenAI (GPT-4o-mini / GPT-4o):
- Use
response_format={"type": "json_object"}for guaranteed JSON - System prompt in the first message with
role: "system" - Temperature=0 for determinism
max_tokensoptional but recommended to control cost
For Anthropic (Claude 3.5 Sonnet / Claude 3 Haiku):
- System prompt as a separate
system=parameter max_tokensis required (without it, it raises an error)- For JSON: use explicit instructions + retry logic
- XML tags work excellently for structuring complex responses
- Claude 3 Haiku for simple tasks (cheaper), Claude 3.5 Sonnet for complex reasoning
For Google (Gemini 1.5 Flash / Pro):
- System instruction at model initialization
- For JSON:
response_mime_type="application/json"in generation_config - Add explicit brevity instructions if the output is longer than expected
Connection to the project
In the Prompt Analyzer (capsule 08) you can add portability detection:
- Detect whether the prompt uses provider-specific features (e.g. it mentions
response_format) - Suggest a portable version of the same instructions
- Indicate what changes it would need to work on each provider
In Module 3 (Structured Outputs) you'll go deeper into OpenAI's JSON mode and Anthropic's alternatives — exactly the adapters you saw here, but with Pydantic schemas and validation.
Troubleshooting
Problem 1: Claude returns longer answers than asked for
Cause: Claude tends to be more explanatory by default, especially on short answers where the format constraint isn't 100% strict.
Fix:
# Reinforce it in the Personality with firm language
system = """
Classify the sentiment.
RESPOND ONLY with a single word: POSITIVE, NEGATIVE, or NEUTRAL.
No punctuation. No explanation. No additional text.
Incorrect answer: "The sentiment is POSITIVE because..."
Correct answer: "POSITIVE"
"""
Problem 2: Error "max_tokens is required" on Anthropic
Cause: Anthropic has no default value for max_tokens — it's required.
Fix: Always include max_tokens in calls to Anthropic:
# ❌ Raises an error
r = ant_client.messages.create(model="...", messages=[...])
# ✅ Correct
r = ant_client.messages.create(model="...", max_tokens=100, messages=[...])
A general rule: max_tokens = (expected output tokens) × 2 as a safety buffer.
Problem 3: Anthropic's JSON comes with text before or after
Cause: Without native JSON mode, Claude sometimes adds "Here's the JSON:" or uses json ... .
Fix: Implement a robust parser:
import re
def extract_json(raw: str) -> dict:
"""Extracts JSON from a response that may have text around it."""
# Try a direct parse
try:
return json.loads(raw)
except json.JSONDecodeError:
pass
# Look for a JSON block between ```
match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', raw, re.DOTALL)
if match:
return json.loads(match.group(1))
# Look for the first { ... } in the text
match = re.search(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', raw, re.DOTALL)
if match:
return json.loads(match.group())
raise ValueError(f"No valid JSON found in: {raw[:100]}")
Problem 4: You migrate from OpenAI to Anthropic and the JSON breaks
Cause: You depended on OpenAI's response_format={"type": "json_object"}, which Anthropic doesn't support.
Fix: Adopt the portable pattern: a JSON instruction in text + a robust parser + retry. This works on every provider and is more resilient even on OpenAI.
Problem 5: Environment variables not loaded
Cause: Each provider looks for its own environment variable.
Fix:
# .env
OPENAI_API_KEY=sk-proj-...
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_API_KEY=AI...
# In your code
from dotenv import load_dotenv
load_dotenv() # Load .env before initializing the clients
from openai import OpenAI
from anthropic import Anthropic
# The clients read from the environment automatically
oai = OpenAI()
ant = Anthropic()
Exercises
Exercise 1: A basic unified wrapper (Easy)
Implement a function classify_sentiment(text, provider) that always returns "POSITIVE", "NEGATIVE", or "NEUTRAL" regardless of the provider used. Try it with at least OpenAI and Anthropic.
See solution
from openai import OpenAI
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv()
oai = OpenAI()
ant = Anthropic()
SYSTEM = "Classify sentiment. Respond ONLY with POSITIVE, NEGATIVE, or NEUTRAL. One word only."
def normalize(raw: str) -> str:
cleaned = raw.strip().rstrip(".,!?").upper()
for cat in ["POSITIVE", "NEGATIVE", "NEUTRAL"]:
if cat in cleaned:
return cat
return "UNKNOWN"
def classify_sentiment(text: str, provider: str = "openai") -> str:
if provider == "openai":
r = oai.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": text}
],
temperature=0,
max_tokens=10
)
raw = r.choices[0].message.content
elif provider == "anthropic":
r = ant.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=10,
system=SYSTEM,
messages=[{"role": "user", "content": text}]
)
raw = r.content[0].text
else:
raise ValueError(f"Unknown provider: {provider}")
return normalize(raw)
# Test
texts = ["I loved the product", "Terrible service", "Fine, nothing to write home about"]
for t in texts:
for provider in ["openai", "anthropic"]:
print(f"[{provider}] {t}: {classify_sentiment(t, provider)}")
Check: Do both providers return the same category for the same inputs?
Exercise 2: Compare outputs on reasoning tasks (Medium)
Pick a more complex task (e.g. summarizing a text in 3 points) and run the same prompt on OpenAI and Anthropic. Compare: response length, format, and whether they follow the brevity instructions.
See solution
SYSTEM_SUMMARY = """
Summarize the following text in exactly 3 points.
Format:
1. [First point — 15 words maximum]
2. [Second point — 15 words maximum]
3. [Third point — 15 words maximum]
Only the 3 numbered points. No introduction, no conclusion.
"""
text = """
Machine learning has transformed how companies process data.
Supervised algorithms learn from labeled examples to make predictions.
Deep neural networks have surpassed classical methods in computer vision.
Natural language processing lets machines understand human text.
AI ethics is a growing field that seeks to mitigate bias and guarantee fairness.
"""
# OpenAI
r_oai = oai.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SYSTEM_SUMMARY},
{"role": "user", "content": text}
],
temperature=0
)
out_oai = r_oai.choices[0].message.content
# Anthropic
r_ant = ant.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=200,
system=SYSTEM_SUMMARY,
messages=[{"role": "user", "content": text}]
)
out_ant = r_ant.content[0].text
print("=== OpenAI ===")
print(out_oai)
print(f"(Words: {len(out_oai.split())})\n")
print("=== Anthropic ===")
print(out_ant)
print(f"(Words: {len(out_ant.split())})")
Notice: Which one sticks more strictly to the 15-word limit per point? Which one adds more context nobody asked for?
Exercise 3: A robust multi-provider JSON parser (Hard)
Implement a function parse_json_universal(raw: str) -> dict that handles every JSON output format the different providers can return: pure JSON, json..., with introductory text, with a trailing comma, etc.
See solution
import json
import re
def parse_json_universal(raw: str) -> dict:
"""
Parses JSON from the output of any LLM provider.
Handles: pure JSON, ```json...```, text+JSON, trailing commas.
"""
# 1. Try a direct parse
try:
return json.loads(raw.strip())
except json.JSONDecodeError:
pass
# 2. Extract from a markdown block ```json ... ```
match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', raw, re.DOTALL)
if match:
try:
return json.loads(match.group(1).strip())
except json.JSONDecodeError:
pass
# 3. Find the JSON in the text (the first balanced { ... })
# Searches from the first { to the matching balanced }
start = raw.find('{')
if start == -1:
raise ValueError("No JSON found in the output")
depth = 0
for i, char in enumerate(raw[start:], start):
if char == '{':
depth += 1
elif char == '}':
depth -= 1
if depth == 0:
candidate = raw[start:i+1]
# 4. Clean up trailing commas (a common error)
candidate = re.sub(r',\s*}', '}', candidate)
candidate = re.sub(r',\s*]', ']', candidate)
try:
return json.loads(candidate)
except json.JSONDecodeError:
break
raise ValueError(f"Could not parse JSON from: {raw[:200]}")
# Test with real problematic formats
test_cases = [
'{"key": "value"}', # Pure JSON
'```json\n{"key": "value"}\n```', # Markdown
'Here is the JSON:\n{"key": "value"}', # With text
'{"key": "value",}', # Trailing comma
'The result is: ```\n{"key": "value"}\n```\nDone!', # Everything mixed
]
for case in test_cases:
try:
result = parse_json_universal(case)
print(f"✅ '{case[:40]}...' → {result}")
except ValueError as e:
print(f"❌ Error: {e}")
Summary
In this capsule you learned:
- OpenAI:
role: "system"in messages[], native JSON mode,max_tokensoptional - Anthropic: system prompt as a separate parameter,
max_tokensrequired, no native JSON mode, XML tags work excellently - Google: system_instruction at model init, JSON via
response_mime_type, can be more verbose - Adapter pattern: A unified wrapper normalizes API differences; output normalization handles format variations
- Portability: Use instructions in text instead of specific features; implement retry for JSON without a native mode
- Fallback: Design the flow so that, if the main provider fails, the fallback works with the same prompt
Next capsule: The module project — you'll build the Prompt Analyzer that integrates anatomy, CRISPE, the casual vs engineered comparison, and multi-provider portability detection.
Additional resources
- OpenAI API Reference — Messages — Complete documentation of the parameters, including
response_formatandtemperature - Anthropic Messages API — Design differences from OpenAI, with an emphasis on
systemandmax_tokens - Google Gemini API — generativeai — Python documentation for Gemini with
system_instructionand generation config - LiteLLM — An open-source library that unifies the interface of 100+ LLMs; an alternative to implementing adapters by hand
- OpenAI Structured Outputs — Advanced JSON mode with JSON Schema schemas (a preview of Module 3)
- Anthropic Model Comparison — An up-to-date table of Claude models with capabilities, prices, and recommended use cases