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[]: only user and assistant (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: user and model (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

AspectOpenAIAnthropicGoogle
System prompt locationmessages[0] with role: systemA separate system= parametersystem_instruction= at model init
max_tokens requiredNo (it has a default)Yes (no default, it raises an error)No (it has a default)
Native JSON moderesponse_formatPartial (with instructions)response_mime_type
XML tags for structureWorksExcellent (optimized for it)Works
Respecting "only X words"HighHighMedium-High
Very long system prompt (5k+ tokens)Well supportedExcellent (200k context)Fine
Temperature range0.0-2.00.0-1.00.0-2.0
Roles in messagessystem/user/assistantuser/assistantuser/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:

PracticeOpenAIAnthropicGooglePortable?
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_tokens optional but recommended to control cost

For Anthropic (Claude 3.5 Sonnet / Claude 3 Haiku):

  • System prompt as a separate system= parameter
  • max_tokens is 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_tokens optional
  • Anthropic: system prompt as a separate parameter, max_tokens required, 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

  1. OpenAI API Reference — Messages — Complete documentation of the parameters, including response_format and temperature
  2. Anthropic Messages API — Design differences from OpenAI, with an emphasis on system and max_tokens
  3. Google Gemini API — generativeai — Python documentation for Gemini with system_instruction and generation config
  4. LiteLLM — An open-source library that unifies the interface of 100+ LLMs; an alternative to implementing adapters by hand
  5. OpenAI Structured Outputs — Advanced JSON mode with JSON Schema schemas (a preview of Module 3)
  6. Anthropic Model Comparison — An up-to-date table of Claude models with capabilities, prices, and recommended use cases