Módulo 2: Vision + LLMs
3. Claude 3 Vision (Anthropic)
Descripción
Claude 3 es la familia de modelos multimodales de Anthropic que compite directamente con GPT-4o de OpenAI. Soporta imágenes como input junto con texto, lo que permite realizar las mismas tareas que vimos en la cápsula anterior: descripción, OCR, clasificación y extracción estructurada.
¿Por qué importa Anthropic como alternativa? Tres razones: una ventana de contexto de 200K tokens (vs 128K de GPT-4o), rendimiento superior en OCR y análisis de documentos según benchmarks independientes, y una API con diferencias estructurales que todo AI Engineer debe dominar para no depender de un solo proveedor.
Esta cápsula se enfoca en las diferencias con OpenAI. Si algo funciona igual, no lo repetiremos — consulta la cápsula 02 para los fundamentos.
Modelos con Vision
Anthropic ofrece tres modelos con capacidades de visión, cada uno optimizado para un balance diferente entre calidad, velocidad y costo:
| Modelo | Visión | Contexto | Costo Input (aprox) | Costo Output (aprox) | Mejor para |
|---|---|---|---|---|---|
claude-3-5-sonnet-latest | Excelente | 200K tokens | $3.00 / 1M tokens | $15.00 / 1M tokens | Default recomendado. Balance óptimo calidad/costo |
claude-3-opus-latest | Superior | 200K tokens | $15.00 / 1M tokens | $75.00 / 1M tokens | Análisis complejos, razonamiento profundo |
claude-3-haiku-20240307 | Buena | 200K tokens | $0.25 / 1M tokens | $1.25 / 1M tokens | Tareas simples, alto volumen, baja latencia |
Recomendación: Usa claude-3-5-sonnet-latest como default. Tiene la mejor relación calidad-precio y su rendimiento en visión es comparable a Opus en la mayoría de tareas. Reserva Opus para análisis que requieran razonamiento multi-paso complejo. Usa Haiku cuando necesites velocidad y el análisis sea simple (clasificación binaria, detección de contenido).
Comparado con OpenAI:
| Aspecto | OpenAI | Anthropic |
|---|---|---|
| Modelo económico | gpt-4o-mini ($0.15/1M in) | claude-3-haiku ($0.25/1M in) |
| Modelo balance | gpt-4o ($2.50/1M in) | claude-3-5-sonnet ($3.00/1M in) |
| Contexto máximo | 128K tokens | 200K tokens |
Estructura del Request — Diferencias con OpenAI
Esta es la diferencia más importante entre ambas APIs. Aunque el concepto es el mismo (enviar un array de content blocks), la estructura JSON es completamente distinta.
En la cápsula 02 vimos que OpenAI usa {"type": "image_url", "image_url": {"url": "data:mime;base64,..."}}. Anthropic tiene una estructura distinta:
Anthropic
import anthropic
import base64
client = anthropic.Anthropic()
with open("imagen.jpg", "rb") as f:
image_data = base64.b64encode(f.read()).decode()
response = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": image_data
}
},
{"type": "text", "text": "Describe esta imagen."}
]
}]
)
result = response.content[0].text
Tabla comparativa de estructuras
| Elemento | OpenAI | Anthropic |
|---|---|---|
| Tipo de bloque imagen | "type": "image_url" | "type": "image" |
| Datos de imagen | "image_url": {"url": "data:mime;base64,DATA"} | "source": {"type": "base64", "media_type": "...", "data": "..."} |
| Media type | Embebido en el data URI | Campo media_type explícito y obligatorio |
| URLs directas | Sí, acepta URLs públicas | No soportado |
| Método de la API | client.chat.completions.create() | client.messages.create() |
| Acceso a respuesta | response.choices[0].message.content | response.content[0].text |
| Parámetro tokens | max_tokens (opcional) | max_tokens (obligatorio) |
El punto clave: en OpenAI el Base64 va dentro de un data URI (data:image/jpeg;base64,XXXX), mientras que en Anthropic el Base64 va en un campo data separado y el media type en su propio campo. Esto hace que Anthropic sea más explícito pero también más estricto — un media type incorrecto generará un error.
Solo Base64 — No URLs
Esta es la limitación más impactante de Anthropic vs OpenAI. Mientras que OpenAI acepta tanto Base64 como URLs públicas, Anthropic solo acepta Base64. No puedes pasar una URL directamente.
Esto significa que si tu imagen está en un servidor remoto, debes descargarla primero y convertirla a Base64 antes de enviarla a Claude.
import anthropic
import base64
import httpx
from pathlib import Path
client = anthropic.Anthropic()
def download_and_encode(url: str) -> tuple[str, str]:
"""Descarga una imagen desde URL y retorna (base64_data, media_type)."""
response = httpx.get(url, follow_redirects=True, timeout=30)
response.raise_for_status()
content_type = response.headers.get("content-type", "image/jpeg")
media_type = content_type.split(";")[0].strip()
allowed = {"image/jpeg", "image/png", "image/gif", "image/webp"}
if media_type not in allowed:
raise ValueError(f"Formato no soportado: {media_type}")
data = response.content
if len(data) > 5 * 1024 * 1024:
raise ValueError(f"Imagen excede 5MB: {len(data) / 1024 / 1024:.1f}MB")
return base64.b64encode(data).decode(), media_type
Límite de 5MB
Anthropic impone un límite de 5MB por imagen en Base64. En la práctica, como Base64 aumenta el tamaño ~33%, esto significa que tu imagen original no debe superar ~3.75MB. Si necesitas enviar imágenes más grandes, redimensiona con PIL antes de codificar.
Media Types
Anthropic soporta cuatro formatos de imagen:
| Formato | Media Type | Notas |
|---|---|---|
| JPEG | image/jpeg | El más común. Buena compresión para fotos |
| PNG | image/png | Ideal para capturas de pantalla, texto, diagramas |
| GIF | image/gif | Solo el primer frame si es animado |
| WebP | image/webp | Formato moderno, buena compresión |
A diferencia de OpenAI donde el media type va embebido en el data URI y puede inferirse, en Anthropic es un campo obligatorio separado. Enviar un media type incorrecto produce un error.
Función de auto-detección:
from pathlib import Path
MEDIA_TYPES = {
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
".webp": "image/webp",
}
def detect_media_type(path: str) -> str:
ext = Path(path).suffix.lower()
if ext not in MEDIA_TYPES:
raise ValueError(f"Extensión no soportada: {ext}. Usa: {list(MEDIA_TYPES.keys())}")
return MEDIA_TYPES[ext]
Ejemplo 1: Descripción de Imagen
Ejemplo completo que carga una imagen local y obtiene una descripción:
import anthropic
import base64
from pathlib import Path
client = anthropic.Anthropic()
def describe_image(image_path: str, language: str = "español") -> str:
ext = Path(image_path).suffix.lower()
media_types = {".jpg": "image/jpeg", ".jpeg": "image/jpeg",
".png": "image/png", ".gif": "image/gif", ".webp": "image/webp"}
media_type = media_types.get(ext, "image/jpeg")
with open(image_path, "rb") as f:
image_data = base64.b64encode(f.read()).decode()
response = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=512,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {"type": "base64", "media_type": media_type, "data": image_data}
},
{
"type": "text",
"text": f"Describe esta imagen en detalle en {language}. "
f"Incluye: elementos principales, colores, composición y contexto."
}
]
}]
)
return response.content[0].text
description = describe_image("foto_producto.jpg")
print(description)
Diferencia práctica con OpenAI: el prompt y la estructura son equivalentes en funcionalidad, pero nota cómo aquí debemos especificar media_type explícitamente y max_tokens es obligatorio (en OpenAI es opcional).
Ejemplo 2: OCR con Claude
Claude destaca particularmente en OCR — en benchmarks independientes supera a GPT-4o en extracción de texto de documentos escaneados, recibos y formularios. Esto lo convierte en la opción preferida para pipelines de procesamiento documental.
import anthropic
import base64
client = anthropic.Anthropic()
def ocr_claude(image_path: str, structured: bool = False) -> str:
with open(image_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
if structured:
prompt = (
"Extrae todo el texto visible en esta imagen. "
"Organiza el resultado respetando la estructura visual: "
"encabezados, párrafos, listas, tablas. "
"Usa formato Markdown para representar la estructura."
)
else:
prompt = (
"Extrae todo el texto visible en esta imagen, "
"línea por línea, exactamente como aparece."
)
response = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=4096,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {"type": "base64", "media_type": "image/png", "data": b64}
},
{"type": "text", "text": prompt}
]
}]
)
return response.content[0].text
text_raw = ocr_claude("recibo.png")
text_structured = ocr_claude("recibo.png", structured=True)
print(text_structured)
Para OCR con Claude, usa max_tokens alto (4096+) porque documentos escaneados pueden generar mucho texto. Con OpenAI el default de max_tokens es 4096, pero en Anthropic debes especificarlo explícitamente.
Ejemplo 3: Análisis de Documentos
Claude sobresale en análisis de documentos complejos: contratos, facturas, reportes. Su ventana de 200K tokens permite procesar documentos extensos con contexto adicional.
import anthropic
import base64
client = anthropic.Anthropic()
def analyze_document(image_path: str, document_type: str, fields: list[str]) -> str:
with open(image_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
fields_list = "\n".join(f"- {field}" for field in fields)
response = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=2048,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {"type": "base64", "media_type": "image/png", "data": b64}
},
{
"type": "text",
"text": f"Este es un documento de tipo: {document_type}.\n\n"
f"Extrae los siguientes campos:\n{fields_list}\n\n"
f"Para cada campo, indica el valor encontrado. "
f"Si un campo no es visible, indica 'No encontrado'."
}
]
}]
)
return response.content[0].text
result = analyze_document(
"factura.png",
document_type="factura comercial",
fields=["Número de factura", "Fecha", "Proveedor", "Total", "IVA", "Método de pago"]
)
print(result)
Claude soporta múltiples imágenes en el mismo mensaje — simplemente agrega varios bloques {"type": "image", ...} al array content antes del bloque de texto.
Ejemplo 4: Extracción JSON
En OpenAI puedes usar response_format={"type": "json_object"} para garantizar output JSON. Anthropic no tiene este parámetro. En su lugar, debes usar prompt engineering para obtener JSON válido.
import anthropic
import base64
import json
client = anthropic.Anthropic()
def extract_json_from_image(image_path: str, schema_description: str) -> dict:
with open(image_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
response = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=2048,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {"type": "base64", "media_type": "image/jpeg", "data": b64}
},
{
"type": "text",
"text": f"Analiza esta imagen y extrae la información en JSON.\n\n"
f"Schema esperado:\n{schema_description}\n\n"
f"Responde ÚNICAMENTE con el JSON válido, sin texto adicional, "
f"sin bloques de código markdown, sin explicaciones."
}
]
}]
)
raw = response.content[0].text.strip()
if raw.startswith("```"):
raw = raw.split("\n", 1)[1].rsplit("```", 1)[0].strip()
return json.loads(raw)
product_data = extract_json_from_image(
"producto.jpg",
'{"nombre": "string", "precio": number, "categoria": "string", "color": "string"}'
)
print(json.dumps(product_data, indent=2, ensure_ascii=False))
Comparación de estrategias para JSON:
| Aspecto | OpenAI | Anthropic |
|---|---|---|
| Forzar JSON | response_format={"type": "json_object"} | No disponible |
| Estrategia | Parámetro nativo | Prompt engineering |
| Fiabilidad | ~99% con response_format | ~95% con buen prompt |
| Post-procesado | Directo json.loads() | Puede requerir limpieza de markdown |
El bloque de limpieza (if raw.startswith("```")) es necesario porque Claude a veces envuelve el JSON en bloques de código markdown, incluso cuando se le pide no hacerlo.
System Prompt en Anthropic
Otra diferencia estructural importante: en OpenAI el system prompt es un mensaje con "role": "system" dentro del array messages. En Anthropic, el system prompt es un parámetro top-level separado.
import anthropic
import base64
client = anthropic.Anthropic()
with open("imagen.jpg", "rb") as f:
b64 = base64.b64encode(f.read()).decode()
response = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=1024,
system="Eres un experto en análisis visual de productos. "
"Siempre respondes en español con formato estructurado.",
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {"type": "base64", "media_type": "image/jpeg", "data": b64}
},
{"type": "text", "text": "Analiza este producto."}
]
}]
)
Si incluyes {"role": "system", ...} dentro de messages en Anthropic, obtendrás un error. Es un error frecuente al migrar código de OpenAI a Anthropic.
Tokens y Costos
Anthropic incluye información de uso en cada respuesta. Es esencial para monitorear costos en producción.
import anthropic
import base64
client = anthropic.Anthropic()
with open("imagen.jpg", "rb") as f:
b64 = base64.b64encode(f.read()).decode()
response = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {"type": "base64", "media_type": "image/jpeg", "data": b64}
},
{"type": "text", "text": "Describe esta imagen."}
]
}]
)
input_tokens = response.usage.input_tokens
output_tokens = response.usage.output_tokens
COST_PER_M_INPUT = 3.00
COST_PER_M_OUTPUT = 15.00
cost_input = (input_tokens / 1_000_000) * COST_PER_M_INPUT
cost_output = (output_tokens / 1_000_000) * COST_PER_M_OUTPUT
total_cost = cost_input + cost_output
print(f"Input: {input_tokens:,} tokens (${cost_input:.4f})")
print(f"Output: {output_tokens:,} tokens (${cost_output:.4f})")
print(f"Total: ${total_cost:.4f}")
Las imágenes consumen tokens de input. Una imagen típica de 1024x1024 consume aproximadamente 1,600 tokens. Imágenes más grandes consumen más. Anthropic no documenta la fórmula exacta, pero puedes usar response.usage.input_tokens para medir el consumo real.
Comparación del objeto de uso:
| Campo | OpenAI | Anthropic |
|---|---|---|
| Tokens de entrada | response.usage.prompt_tokens | response.usage.input_tokens |
| Tokens de salida | response.usage.completion_tokens | response.usage.output_tokens |
| Total | response.usage.total_tokens | Calcular manualmente |
Troubleshooting
1. Error: Imagen excede 5MB
anthropic.BadRequestError: Image exceeds maximum size of 5MB
Causa: La imagen codificada en Base64 supera 5MB.
Solución: Redimensiona o comprime la imagen antes de codificar. Usa la función resize_for_claude mostrada en la sección "Solo Base64".
2. Error: URL no soportada
anthropic.BadRequestError: Invalid image source type
Causa: Intentas pasar una URL directa como hacías en OpenAI.
Solución: Descarga la imagen primero y envíala como Base64. Usa download_and_encode() de la sección anterior.
3. Error: Media type incorrecto
anthropic.BadRequestError: Invalid media type
Causa: El campo media_type no coincide con el formato real de la imagen, o usaste un formato no soportado.
Solución: Verifica que la extensión del archivo corresponda al media_type. Usa la función detect_media_type() para automatizar.
4. Error: System prompt como mensaje
anthropic.BadRequestError: Messages must not contain "system" role
Causa: Migraste código de OpenAI sin cambiar el system prompt de mensaje a parámetro top-level.
Solución: Mueve el contenido de {"role": "system", "content": "..."} al parámetro system= de messages.create().
5. Error: max_tokens faltante
anthropic.BadRequestError: max_tokens is required
Causa: En OpenAI max_tokens es opcional (tiene default). En Anthropic es obligatorio.
Solución: Siempre incluye max_tokens en la llamada. Valores comunes: 512 para descripciones cortas, 1024 para análisis, 4096 para OCR extenso.
Ejercicios
Ejercicio 1 (Fácil): Envío universal de imagen a Claude
Crea una función send_image_to_claude(image_path, prompt) que:
- Detecte automáticamente el
media_typesegún la extensión del archivo - Valide que el archivo no exceda 5MB
- Envíe la imagen a Claude y retorne la respuesta
- Lance
ValueErrorsi el formato no es soportado o el archivo es muy grande
Ver solución
import anthropic
import base64
from pathlib import Path
client = anthropic.Anthropic()
MEDIA_TYPES = {
".jpg": "image/jpeg", ".jpeg": "image/jpeg",
".png": "image/png", ".gif": "image/gif", ".webp": "image/webp",
}
MAX_SIZE_BYTES = 5 * 1024 * 1024
def send_image_to_claude(image_path: str, prompt: str) -> str:
path = Path(image_path)
ext = path.suffix.lower()
if ext not in MEDIA_TYPES:
raise ValueError(f"Formato no soportado: {ext}")
media_type = MEDIA_TYPES[ext]
file_size = path.stat().st_size
if file_size > MAX_SIZE_BYTES:
raise ValueError(f"Archivo excede 5MB: {file_size / 1024 / 1024:.1f}MB")
with open(path, "rb") as f:
b64_data = base64.b64encode(f.read()).decode()
response = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {"type": "base64", "media_type": media_type, "data": b64_data}
},
{"type": "text", "text": prompt}
]
}]
)
return response.content[0].text
try:
result = send_image_to_claude("foto.jpg", "¿Qué ves en esta imagen?")
print(result)
except ValueError as e:
print(f"Error de validación: {e}")
Ejercicio 2 (Medio): Descargar URL y enviar a Claude
Crea una función claude_from_url(url, prompt) que:
- Descargue la imagen desde una URL usando
httpx - Detecte el media_type desde el header
Content-Type - Valide que sea un formato soportado y no exceda 5MB
- Envíe a Claude y retorne la respuesta
- Maneje errores de red (timeout, 404) con mensajes claros
Ver solución
import anthropic
import base64
import httpx
client = anthropic.Anthropic()
ALLOWED_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp"}
def claude_from_url(url: str, prompt: str, timeout: int = 30) -> str:
try:
http_response = httpx.get(url, follow_redirects=True, timeout=timeout)
http_response.raise_for_status()
except httpx.TimeoutException:
raise ConnectionError(f"Timeout descargando imagen: {url}")
except httpx.HTTPStatusError as e:
raise ConnectionError(f"Error HTTP {e.response.status_code}: {url}")
content_type = http_response.headers.get("content-type", "")
media_type = content_type.split(";")[0].strip()
if media_type not in ALLOWED_TYPES:
raise ValueError(f"Tipo no soportado: {media_type}")
image_bytes = http_response.content
if len(image_bytes) > 5 * 1024 * 1024:
raise ValueError(f"Imagen excede 5MB: {len(image_bytes) / 1024 / 1024:.1f}MB")
b64_data = base64.b64encode(image_bytes).decode()
response = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {"type": "base64", "media_type": media_type, "data": b64_data}
},
{"type": "text", "text": prompt}
]
}]
)
return response.content[0].text
try:
result = claude_from_url(
"https://example.com/producto.jpg",
"Describe este producto en español."
)
print(result)
except (ConnectionError, ValueError) as e:
print(f"Error: {e}")
Ejercicio 3 (Medio): Comparador OpenAI vs Claude
Crea una función compare_vision(image_path, prompt) que:
- Envíe la misma imagen y prompt a GPT-4o Y a Claude 3.5 Sonnet
- Retorne un diccionario con ambas respuestas y los tokens usados por cada uno
- Calcule el costo de cada llamada
Necesitarás openai y anthropic instalados.
Ver solución
from openai import OpenAI
import anthropic
import base64
from pathlib import Path
openai_client = OpenAI()
anthropic_client = anthropic.Anthropic()
MEDIA_TYPES = {
".jpg": "image/jpeg", ".jpeg": "image/jpeg",
".png": "image/png", ".gif": "image/gif", ".webp": "image/webp",
}
def compare_vision(image_path: str, prompt: str) -> dict:
ext = Path(image_path).suffix.lower()
media_type = MEDIA_TYPES.get(ext, "image/jpeg")
with open(image_path, "rb") as f:
b64_data = base64.b64encode(f.read()).decode()
openai_response = openai_client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": {"url": f"data:{media_type};base64,{b64_data}"}
}
]
}],
max_tokens=1024
)
claude_response = anthropic_client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {"type": "base64", "media_type": media_type, "data": b64_data}
},
{"type": "text", "text": prompt}
]
}]
)
return {
"openai": {
"response": openai_response.choices[0].message.content,
"input_tokens": openai_response.usage.prompt_tokens,
"output_tokens": openai_response.usage.completion_tokens,
"cost": (openai_response.usage.prompt_tokens / 1e6 * 2.50 +
openai_response.usage.completion_tokens / 1e6 * 10.00),
},
"anthropic": {
"response": claude_response.content[0].text,
"input_tokens": claude_response.usage.input_tokens,
"output_tokens": claude_response.usage.output_tokens,
"cost": (claude_response.usage.input_tokens / 1e6 * 3.00 +
claude_response.usage.output_tokens / 1e6 * 15.00),
},
}
results = compare_vision("foto.jpg", "Describe esta imagen en 2 oraciones.")
for provider, data in results.items():
print(f"\n{'='*40}")
print(f"{provider.upper()}")
print(f"Respuesta: {data['response']}")
print(f"Tokens: {data['input_tokens']} in / {data['output_tokens']} out")
print(f"Costo: ${data['cost']:.4f}")
Resumen
- Anthropic usa
{"type": "image", "source": {"type": "base64", "media_type": "...", "data": "..."}}— distinto alimage_urlde OpenAI - Modelos: Sonnet (default recomendado), Opus (máxima calidad), Haiku (económico)
- Solo Base64 — no acepta URLs directas (necesitas descargar primero)
media_typees explícito y obligatoriomax_tokenses obligatorio (en OpenAI es opcional)systemprompt va como parámetro top-level, no como mensaje- No tiene
response_formatnativo para JSON — usa prompt engineering - Límite de 5MB por imagen
- Respuesta en
response.content[0].text(vsresponse.choices[0].message.content)
Recursos adicionales
- Anthropic Vision — Documentación oficial
- Anthropic API Reference — Messages
- Claude 3 Model Card
- Anthropic Python SDK