Módulo 2: Vision + LLMs

5. Comparación: GPT-4 Vision vs Claude Vision vs Gemini Vision

Descripción

En las cápsulas 02, 03 y 04 aprendiste a usar GPT-4 Vision, Claude 3 Vision y Gemini Vision de forma individual. Cada proveedor tiene una API distinta, modelos con diferentes capacidades, y una estructura de costos propia. Pero en producción no eliges un proveedor y te quedas con él para siempre — eliges el proveedor correcto para cada tarea.

Un sistema de clasificación de imágenes de productos a alto volumen necesita un modelo barato y rápido. Un pipeline de OCR sobre contratos legales necesita máxima precisión. Un análisis de video necesita un proveedor que soporte video como input nativo. Son el mismo dominio (visión), pero la decisión óptima cambia en cada caso.

Esta cápsula te da las herramientas para tomar esa decisión: tablas comparativas con datos reales, benchmarks que puedes ejecutar con tus propias imágenes, un árbol de decisión programático, cálculo de costos por escenario, y patrones de fallback para producción.

Lo que vas a construir: Un benchmark multi-proveedor, una función de selección automática de proveedor, un estimador de costos, y un router con fallback.


Tabla Comparativa Detallada

Esta tabla compara los modelos más relevantes para tareas de visión de cada proveedor. Los precios corresponden a las APIs oficiales al momento de escritura — verifica siempre las páginas de pricing antes de estimar costos de producción.

CriterioGPT-4oGPT-4o-miniClaude 3.5 SonnetClaude 3 HaikuGemini 1.5 FlashGemini 1.5 Pro
Calidad OCRExcelenteBuenaExcelenteBuenaMuy buenaExcelente
RazonamientoMuy altoMedioMuy altoMedioAltoMuy alto
Input (1M tokens)$2.50$0.15$3.00$0.25$0.075$1.25
Output (1M tokens)$10.00$0.60$15.00$1.25$0.30$5.00
Context window128K128K200K200K1M1M
Imágenes por request~10~10~20~20~16~16
Latencia típicaMediaBajaMediaBajaBajaMedia
Soporte URLNoNoNoNo
Soporte videoNoNoNoNo

Lectura de la tabla

  • Costo: Gemini Flash es ~33x más barato que GPT-4o en input. Para tareas donde la calidad es "suficiente", la diferencia de costo domina la decisión.
  • Context window: Gemini ofrece 1M tokens. Si necesitas analizar documentos de 50+ páginas en una sola llamada, Gemini es la única opción viable sin chunking.
  • Video: Solo Gemini soporta video como input nativo. Los otros proveedores requieren extraer frames y enviarlos como imágenes individuales.
  • URLs: OpenAI acepta URLs directas de imágenes. Anthropic y Google requieren Base64 o bytes. Esto afecta la arquitectura de tu pipeline.

Benchmark: Misma Imagen, Tres Proveedores

La mejor forma de comparar proveedores es enviar la misma imagen con el mismo prompt a los tres y medir los resultados. Este benchmark mide latencia, tokens y costo real.

import time
import base64
from pathlib import Path
from dataclasses import dataclass
from openai import OpenAI
import anthropic
import google.generativeai as genai


@dataclass
class BenchmarkResult:
    provider: str
    model: str
    response_text: str
    latency_ms: float
    input_tokens: int
    output_tokens: int
    estimated_cost_usd: float


PRICING = {
    "gpt-4o": {"input": 2.50, "output": 10.00},
    "gpt-4o-mini": {"input": 0.15, "output": 0.60},
    "claude-3-5-sonnet-latest": {"input": 3.00, "output": 15.00},
    "claude-3-haiku-20240307": {"input": 0.25, "output": 1.25},
    "gemini-1.5-flash": {"input": 0.075, "output": 0.30},
    "gemini-1.5-pro": {"input": 1.25, "output": 5.00},
}


def estimate_cost(model: str, input_tokens: int, output_tokens: int) -> float:
    prices = PRICING[model]
    return (input_tokens * prices["input"] + output_tokens * prices["output"]) / 1_000_000


def _timed_call(fn):
    start = time.perf_counter()
    result = fn()
    return result, (time.perf_counter() - start) * 1000


def benchmark_openai(image_b64: str, prompt: str, model: str = "gpt-4o") -> BenchmarkResult:
    client = OpenAI()
    response, latency = _timed_call(lambda: client.chat.completions.create(
        model=model, max_tokens=500,
        messages=[{"role": "user", "content": [
            {"type": "text", "text": prompt},
            {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}}
        ]}]
    ))
    u = response.usage
    return BenchmarkResult("OpenAI", model, response.choices[0].message.content,
        round(latency, 1), u.prompt_tokens, u.completion_tokens,
        estimate_cost(model, u.prompt_tokens, u.completion_tokens))


def benchmark_anthropic(image_b64: str, prompt: str, model: str = "claude-3-5-sonnet-latest") -> BenchmarkResult:
    client = anthropic.Anthropic()
    response, latency = _timed_call(lambda: client.messages.create(
        model=model, max_tokens=500,
        messages=[{"role": "user", "content": [
            {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_b64}},
            {"type": "text", "text": prompt}
        ]}]
    ))
    u = response.usage
    return BenchmarkResult("Anthropic", model, response.content[0].text,
        round(latency, 1), u.input_tokens, u.output_tokens,
        estimate_cost(model, u.input_tokens, u.output_tokens))


def benchmark_gemini(image_bytes: bytes, prompt: str, model: str = "gemini-1.5-flash") -> BenchmarkResult:
    gmodel = genai.GenerativeModel(model)
    response, latency = _timed_call(
        lambda: gmodel.generate_content([prompt, {"mime_type": "image/jpeg", "data": image_bytes}])
    )
    u = response.usage_metadata
    return BenchmarkResult("Google", model, response.text,
        round(latency, 1), u.prompt_token_count, u.candidates_token_count,
        estimate_cost(model, u.prompt_token_count, u.candidates_token_count))


def benchmark_vision(image_path: str, prompt: str) -> list[BenchmarkResult]:
    raw_bytes = Path(image_path).read_bytes()
    image_b64 = base64.b64encode(raw_bytes).decode()

    results = [
        benchmark_openai(image_b64, prompt, "gpt-4o"),
        benchmark_anthropic(image_b64, prompt, "claude-3-5-sonnet-latest"),
        benchmark_gemini(raw_bytes, prompt, "gemini-1.5-flash"),
    ]

    print(f"\n{'Proveedor':<12} {'Modelo':<28} {'Latencia':>10} {'Tokens In':>10} {'Tokens Out':>11} {'Costo':>10}")
    print("-" * 85)
    for r in results:
        print(f"{r.provider:<12} {r.model:<28} {r.latency_ms:>8.0f}ms {r.input_tokens:>10,} {r.output_tokens:>11,} ${r.estimated_cost_usd:>8.6f}")
    return results


results = benchmark_vision("factura.jpg", "Extrae todos los datos de esta factura en formato JSON.")

Salida de ejemplo:

Proveedor    Modelo                        Latencia  Tokens In  Tokens Out      Costo
-------------------------------------------------------------------------------------
OpenAI       gpt-4o                          2340ms      1,285         312  $0.006313
Anthropic    claude-3-5-sonnet-latest        2780ms      1,412         298  $0.008706
Google       gemini-1.5-flash                 890ms        980         275  $0.000156

Gemini Flash fue ~3x más rápido y ~40x más barato. Pero la calidad de la respuesta puede variar — el benchmark te da los datos, tú decides qué trade-off acepta tu caso de uso.


Comparación por Tarea

Cada tipo de tarea tiene un proveedor óptimo. Esta tabla resume las recomendaciones basadas en benchmarks y experiencia práctica.

TareaRecomendadoRazónAlternativa
OCR simple (facturas, recibos)Gemini 1.5 FlashMás barato con calidad suficienteGPT-4o-mini
OCR complejo (escritura a mano)Claude 3.5 SonnetSuperior en reconocimiento de handwritingGPT-4o
Clasificación de imágenesGPT-4o-miniCalidad suficiente, bajo costoGemini Flash
Documentos largos (>50 págs)Gemini 1.5 ProContext window de 1M tokensClaude Sonnet (200K)
Razonamiento sobre imágenesClaude 3.5 SonnetMejor razonamiento visual detalladoGPT-4o
Batch alto volumen (>1000 imgs)Gemini 1.5 FlashCosto mínimo, baja latenciaClaude Haiku
Análisis de videoGemini 1.5 ProÚnico con soporte nativo de video
Extracción JSON estructuradoGPT-4oAdherencia consistente a schemas JSONClaude Sonnet
Descripción de imágenesGPT-4o-miniBuena calidad, costo bajoGemini Flash
Detección de contenido sensibleClaude HaikuRápido, enfocado en seguridadGPT-4o-mini

Cuándo la recomendación cambia

Las recomendaciones anteriores asumen condiciones ideales. En la práctica, otros factores pueden cambiar la decisión:

  • Ya tienes integración con un proveedor: Si tu sistema ya usa OpenAI para texto, agregar visión con GPT-4o tiene menor costo de integración que agregar Anthropic desde cero.
  • Regulación y compliance: Algunos sectores requieren que los datos no salgan de ciertas regiones. Revisa las políticas de cada proveedor.
  • Rate limits de tu tier: Si tienes tier gratuito en Google pero tier-2 en OpenAI, los rate limits pueden favorecer un proveedor que en teoría es más caro.
  • Calidad para tu dominio específico: Estos rankings son generales. Siempre ejecuta el benchmark con tus imágenes reales.

Árbol de Decisión

Versión texto

¿Tu tarea involucra video?
├── SÍ → Gemini 1.5 Pro
└── NO → ¿El documento tiene más de 50 páginas?
    ├── SÍ → Gemini 1.5 Pro (1M context)
    └── NO → ¿El costo es la prioridad #1?
        ├── SÍ → Gemini 1.5 Flash
        └── NO → ¿Necesitas máxima calidad de razonamiento?
            ├── SÍ → Claude 3.5 Sonnet
            └── NO → ¿Volumen > 1000 imágenes/día?
                ├── SÍ → GPT-4o-mini o Gemini Flash
                └── NO → GPT-4o (equilibrio general)

Versión código

def select_provider(
    task: str,
    budget: str = "medium",
    context_length: int = 0,
    needs_video: bool = False,
    volume_per_day: int = 0
) -> dict:
    """Selecciona el proveedor óptimo según los parámetros del caso de uso.

    Args:
        task: Tipo de tarea (ocr_simple, ocr_complex, classification, reasoning, description, json_extraction)
        budget: Restricción de presupuesto (low, medium, high)
        context_length: Tokens estimados del documento (0 si es solo imagen)
        needs_video: Si la tarea requiere procesamiento de video
        volume_per_day: Número estimado de requests por día

    Returns:
        dict con provider, model y reason
    """
    if needs_video:
        return {"provider": "Google", "model": "gemini-1.5-pro", "reason": "Único con soporte nativo de video"}

    if context_length > 200_000:
        return {"provider": "Google", "model": "gemini-1.5-pro", "reason": f"Context de {context_length:,} tokens requiere 1M window"}

    if budget == "low":
        if volume_per_day > 1000:
            return {"provider": "Google", "model": "gemini-1.5-flash", "reason": "Mínimo costo para alto volumen"}
        return {"provider": "Google", "model": "gemini-1.5-flash", "reason": "Opción más económica"}

    task_map = {
        "ocr_simple": {"provider": "Google", "model": "gemini-1.5-flash", "reason": "OCR suficiente, costo mínimo"},
        "ocr_complex": {"provider": "Anthropic", "model": "claude-3-5-sonnet-latest", "reason": "Superior en handwriting y documentos complejos"},
        "classification": {"provider": "OpenAI", "model": "gpt-4o-mini", "reason": "Calidad suficiente para clasificación, bajo costo"},
        "reasoning": {"provider": "Anthropic", "model": "claude-3-5-sonnet-latest", "reason": "Mejor razonamiento visual detallado"},
        "description": {"provider": "OpenAI", "model": "gpt-4o-mini", "reason": "Buenas descripciones a bajo costo"},
        "json_extraction": {"provider": "OpenAI", "model": "gpt-4o", "reason": "Mejor adherencia a schemas JSON"},
    }

    if task in task_map:
        return task_map[task]

    if volume_per_day > 1000:
        return {"provider": "OpenAI", "model": "gpt-4o-mini", "reason": "Equilibrio costo/calidad para volumen"}

    return {"provider": "OpenAI", "model": "gpt-4o", "reason": "Default equilibrado para tareas generales"}


print(select_provider("ocr_complex", budget="medium"))
print(select_provider("classification", budget="low", volume_per_day=5000))
print(select_provider("reasoning", needs_video=True))
{'provider': 'Anthropic', 'model': 'claude-3-5-sonnet-latest', 'reason': 'Superior en handwriting y documentos complejos'}
{'provider': 'Google', 'model': 'gemini-1.5-flash', 'reason': 'Mínimo costo para alto volumen'}
{'provider': 'Google', 'model': 'gemini-1.5-pro', 'reason': 'Único con soporte nativo de video'}

Costos Reales: Escenarios Comparados

Los precios por millón de tokens son abstractos. Lo que importa es cuánto cuesta tu caso de uso concreto. Esta función estima el costo de un escenario específico en cada proveedor.

SCENARIOS = [
    {"name": "Análisis de 1 imagen",       "imgs": 1,     "in_tok": 1_000,  "out_tok": 300},
    {"name": "100 imágenes de productos",   "imgs": 100,   "in_tok": 800,    "out_tok": 200},
    {"name": "Documento de 30 páginas",     "imgs": 1,     "in_tok": 30_000, "out_tok": 2_000},
    {"name": "1,000 screenshots",           "imgs": 1_000, "in_tok": 1_200,  "out_tok": 150},
]

MODELS = ["gpt-4o", "gpt-4o-mini", "claude-3-5-sonnet-latest",
          "claude-3-haiku-20240307", "gemini-1.5-flash", "gemini-1.5-pro"]

def calculate_scenario_costs(scenarios: list[dict]) -> None:
    header = f"{'Escenario':<30}" + "".join(f"{m:>16}" for m in MODELS)
    print(header)
    print("-" * len(header))
    for s in scenarios:
        total_in, total_out = s["imgs"] * s["in_tok"], s["imgs"] * s["out_tok"]
        costs = [(total_in * PRICING[m]["input"] + total_out * PRICING[m]["output"]) / 1_000_000 for m in MODELS]
        min_cost = min(costs)
        row = f"{s['name']:<30}" + "".join(
            f"${c:>13.4f}{' ★' if c == min_cost else '  '}" for c in costs
        )
        print(row)

calculate_scenario_costs(SCENARIOS)

Salida:

Escenario                            GPT-4o      GPT-4o-mini   Claude Sonnet    Claude Haiku   Gemini Flash      Gemini Pro
---------------------------------------------------------------------------------------------------------------------------
Análisis de 1 imagen             $  0.0055       $  0.0003       $  0.0075       $  0.0006       $  0.0002 ★     $  0.0028
100 imágenes de productos        $  0.4000       $  0.0240       $  0.5400       $  0.0450       $  0.0120 ★     $  0.2000
Documento de 30 páginas          $  0.0950       $  0.0057       $  0.1200       $  0.0100       $  0.0028 ★     $  0.0475
1,000 screenshots                $  4.5000       $  0.2700       $  5.4750       $  0.4875       $  0.1350 ★     $  2.2500

Interpretando los números

  • Gemini Flash gana en costo en todos los escenarios. Si tu tarea tolera calidad "buena" (no "excelente"), la decisión económica es obvia.
  • GPT-4o-mini es la segunda opción más barata y ofrece mejor calidad que Gemini Flash en muchas tareas.
  • Claude Sonnet y GPT-4o tienen costos similares. La elección entre ellos depende de la tarea, no del precio.
  • 1,000 screenshots con GPT-4o cuestan $4.50. Con Gemini Flash, $0.14. En producción a escala, esa diferencia es de miles de dólares al mes.

Estrategia de Fallback

En producción, las APIs fallan. Rate limits, timeouts, errores internos del proveedor, mantenimiento programado. Si tu sistema depende de un solo proveedor, un error 503 de OpenAI detiene todo. Un VisionRouter con fallback mantiene tu sistema operativo.

Principios

  1. Define un orden de prioridad según tu tarea (calidad primero, o costo primero).
  2. Si el proveedor primario falla, intenta el siguiente automáticamente.
  3. Registra cada intento para monitorear salud de proveedores.
import time
import logging
from dataclasses import dataclass, field
from typing import Callable

logger = logging.getLogger(__name__)


@dataclass
class ProviderAttempt:
    provider: str
    success: bool
    latency_ms: float
    error: str | None = None


@dataclass
class VisionRouter:
    providers: list[dict]
    max_retries: int = 2
    timeout_ms: float = 15_000
    attempts_log: list[ProviderAttempt] = field(default_factory=list)
    total_cost_usd: float = 0.0

    def call(self, image_b64: str, prompt: str) -> dict:
        """Intenta cada proveedor en orden hasta obtener una respuesta exitosa."""
        last_error = None

        for provider_config in self.providers:
            name = provider_config["name"]
            call_fn: Callable = provider_config["fn"]

            for attempt in range(self.max_retries):
                try:
                    start = time.perf_counter()
                    result = call_fn(image_b64, prompt)
                    latency = (time.perf_counter() - start) * 1000

                    self.attempts_log.append(ProviderAttempt(name, True, latency))
                    self.total_cost_usd += result.get("cost", 0)

                    logger.info(f"{name} respondió en {latency:.0f}ms (intento {attempt + 1})")
                    return result

                except Exception as e:
                    latency = (time.perf_counter() - start) * 1000
                    self.attempts_log.append(ProviderAttempt(name, False, latency, str(e)))
                    last_error = e
                    logger.warning(f"{name} falló (intento {attempt + 1}): {e}")

        raise RuntimeError(f"Todos los proveedores fallaron. Último error: {last_error}")

    def health_report(self) -> dict:
        """Genera un reporte de éxito/fallo por proveedor."""
        from collections import Counter
        success = Counter()
        failure = Counter()
        for a in self.attempts_log:
            if a.success:
                success[a.provider] += 1
            else:
                failure[a.provider] += 1
        return {
            "success_counts": dict(success),
            "failure_counts": dict(failure),
            "total_cost_usd": round(self.total_cost_usd, 6),
            "total_attempts": len(self.attempts_log),
        }

Uso

Cada provider se envuelve en una función con firma (image_b64: str, prompt: str) -> dict que retorna {"text": ..., "cost": ...}. Reutiliza las funciones de las cápsulas 02-04.

def call_openai(image_b64: str, prompt: str) -> dict:
    client = OpenAI()
    r = client.chat.completions.create(model="gpt-4o", max_tokens=500,
        messages=[{"role": "user", "content": [
            {"type": "text", "text": prompt},
            {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}}
        ]}])
    return {"text": r.choices[0].message.content,
            "cost": estimate_cost("gpt-4o", r.usage.prompt_tokens, r.usage.completion_tokens)}

def call_anthropic(image_b64: str, prompt: str) -> dict:
    client = anthropic.Anthropic()
    r = client.messages.create(model="claude-3-5-sonnet-latest", max_tokens=500,
        messages=[{"role": "user", "content": [
            {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_b64}},
            {"type": "text", "text": prompt}
        ]}])
    return {"text": r.content[0].text,
            "cost": estimate_cost("claude-3-5-sonnet-latest", r.usage.input_tokens, r.usage.output_tokens)}

router = VisionRouter(providers=[
    {"name": "OpenAI", "fn": call_openai},
    {"name": "Anthropic", "fn": call_anthropic},
])
result = router.call(image_b64="...", prompt="Describe esta imagen")
print(result["text"])
print(router.health_report())

Estrategia Multi-Provider en Producción

El fallback resuelve la disponibilidad: si uno falla, usa otro. Pero hay escenarios donde quieres usar múltiples proveedores simultáneamente.

Consensus Voting

Envías la misma imagen a 2-3 proveedores y comparas respuestas. Si dos de tres coinciden, la confianza es alta. Útil para tareas críticas donde un error tiene alto costo (diagnóstico médico, compliance legal).

import json
from concurrent.futures import ThreadPoolExecutor


def consensus_vote(image_b64: str, prompt: str, providers: list[Callable]) -> dict:
    with ThreadPoolExecutor(max_workers=len(providers)) as executor:
        futures = [executor.submit(fn, image_b64, prompt) for fn in providers]
        responses = [f.result() for f in futures]

    texts = [r["text"] for r in responses]
    total_cost = sum(r["cost"] for r in responses)

    return {
        "responses": texts,
        "total_cost": total_cost,
        "agreement": len(set(texts)) == 1,
    }

Cuándo usar consensus

  • Clasificación binaria crítica (ej: "¿Este documento contiene información personal?") — si dos de tres dicen "sí", procede.
  • Extracción de datos de alto valor — si dos proveedores extraen el mismo número de factura, confía en el resultado.
  • No usar para descripciones abiertas — las respuestas siempre serán diferentes en wording aunque sean correctas.

Costo del consensus

Multiplicar por N proveedores no siempre es inviable. Si usas Gemini Flash ($0.075/1M) + GPT-4o-mini ($0.15/1M), el consensus cuesta $0.225/1M — todavía más barato que una sola llamada a GPT-4o ($2.50/1M) o Claude Sonnet ($3.00/1M).


Troubleshooting Multi-Provider

Formatos de respuesta inconsistentes

Cada proveedor estructura el texto de forma diferente. Si pides JSON, GPT-4o tiende a devolver JSON limpio, Claude a veces agrega texto antes/después, y Gemini puede incluir markdown.

import json
import re


def extract_json_from_response(text: str) -> dict:
    """Extrae JSON de una respuesta que puede contener texto adicional."""
    json_match = re.search(r'\{[\s\S]*\}', text)
    if json_match:
        try:
            return json.loads(json_match.group())
        except json.JSONDecodeError:
            pass
    raise ValueError(f"No se encontró JSON válido en la respuesta: {text[:200]}")

Resultados cualitativamente diferentes

El mismo prompt puede producir resultados con diferente nivel de detalle. Mitigación:

  • Usa prompts explícitos con formato exacto esperado.
  • Incluye un schema de ejemplo en el prompt.
  • Agrega "Responde SOLO con el JSON, sin texto adicional" al final.

Gestión de API keys

Con tres proveedores, tienes tres keys que rotar, monitorear y proteger.

import os


REQUIRED_KEYS = {
    "OpenAI": "OPENAI_API_KEY",
    "Anthropic": "ANTHROPIC_API_KEY",
    "Google": "GOOGLE_API_KEY",
}


def validate_api_keys() -> dict[str, bool]:
    """Verifica qué proveedores tienen API key configurada."""
    return {
        provider: bool(os.environ.get(env_var))
        for provider, env_var in REQUIRED_KEYS.items()
    }


print(validate_api_keys())

Rate limits cruzados

Cada proveedor tiene sus propios rate limits. Un sistema multi-provider necesita tracking independiente:

  • OpenAI: headers x-ratelimit-remaining-requests y x-ratelimit-remaining-tokens
  • Anthropic: headers anthropic-ratelimit-requests-remaining
  • Google: límites por modelo y por minuto, varían por tier

Si un proveedor devuelve 429, tu router debe automáticamente intentar el siguiente — que es exactamente lo que hace el VisionRouter de la sección anterior.


Ejercicios

Ejercicio 1 (Fácil): Selector de proveedor mejorado

Implementa select_provider_v2() que reciba estos parámetros y retorne el proveedor óptimo:

  • task: tipo de tarea
  • budget_per_1000_images: presupuesto máximo en USD para 1000 imágenes
  • min_context_tokens: tokens mínimos de context window requeridos
  • requires_video: si necesita soporte de video

La función debe filtrar proveedores que no cumplen los requisitos y del resto elegir el más barato.

Ver solución
PROVIDER_SPECS = {
    "gpt-4o": {"context": 128_000, "video": False, "cost_per_1k_imgs": 2.50},
    "gpt-4o-mini": {"context": 128_000, "video": False, "cost_per_1k_imgs": 0.15},
    "claude-3-5-sonnet-latest": {"context": 200_000, "video": False, "cost_per_1k_imgs": 3.00},
    "claude-3-haiku-20240307": {"context": 200_000, "video": False, "cost_per_1k_imgs": 0.25},
    "gemini-1.5-flash": {"context": 1_000_000, "video": True, "cost_per_1k_imgs": 0.075},
    "gemini-1.5-pro": {"context": 1_000_000, "video": True, "cost_per_1k_imgs": 1.25},
}

QUALITY_RANKING = {
    "ocr_complex": ["claude-3-5-sonnet-latest", "gpt-4o", "gemini-1.5-pro"],
    "reasoning": ["claude-3-5-sonnet-latest", "gpt-4o", "gemini-1.5-pro"],
    "classification": ["gpt-4o-mini", "gemini-1.5-flash", "claude-3-haiku-20240307"],
    "ocr_simple": ["gemini-1.5-flash", "gpt-4o-mini", "claude-3-haiku-20240307"],
}


def select_provider_v2(
    task: str,
    budget_per_1000_images: float = 10.0,
    min_context_tokens: int = 0,
    requires_video: bool = False,
) -> dict:
    candidates = []
    for model, specs in PROVIDER_SPECS.items():
        if requires_video and not specs["video"]:
            continue
        if specs["context"] < min_context_tokens:
            continue
        if specs["cost_per_1k_imgs"] > budget_per_1000_images:
            continue
        candidates.append(model)

    if not candidates:
        return {"model": None, "reason": "Ningún proveedor cumple todos los requisitos"}

    if task in QUALITY_RANKING:
        for preferred in QUALITY_RANKING[task]:
            if preferred in candidates:
                return {"model": preferred, "reason": f"Mejor calidad para {task} dentro del presupuesto"}

    cheapest = min(candidates, key=lambda m: PROVIDER_SPECS[m]["cost_per_1k_imgs"])
    return {"model": cheapest, "reason": "Opción más económica que cumple requisitos"}


print(select_provider_v2("ocr_complex", budget_per_1000_images=5.0))
print(select_provider_v2("classification", requires_video=True))
print(select_provider_v2("reasoning", min_context_tokens=500_000))

Ejercicio 2 (Medio): Generador de tabla de costos

Crea una función generate_cost_report() que reciba una lista de escenarios personalizados y genere una tabla comparativa formateada. Cada escenario tiene: nombre, cantidad de imágenes, tokens promedio de input por imagen, y tokens promedio de output por imagen. La función debe:

  1. Calcular el costo total por escenario para cada modelo.
  2. Marcar el modelo más barato con .
  3. Marcar el modelo más caro con .
  4. Al final, mostrar un resumen con el ahorro total de usar siempre el más barato vs siempre el más caro.
Ver solución
def generate_cost_report(scenarios: list[dict]) -> None:
    models = list(PRICING.keys())
    total_min = 0.0
    total_max = 0.0

    header = f"{'Escenario':<35}" + "".join(f"{m:>22}" for m in models)
    print(header)
    print("=" * len(header))

    for s in scenarios:
        total_in = s["images"] * s["input_tokens"]
        total_out = s["images"] * s["output_tokens"]

        costs = {}
        for model in models:
            p = PRICING[model]
            costs[model] = (total_in * p["input"] + total_out * p["output"]) / 1_000_000

        min_cost = min(costs.values())
        max_cost = max(costs.values())
        total_min += min_cost
        total_max += max_cost

        row = f"{s['name']:<35}"
        for model in models:
            c = costs[model]
            marker = " ★" if c == min_cost else (" ✗" if c == max_cost else "  ")
            row += f"  ${c:>16.4f}{marker}"
        print(row)

    print("=" * len(header))
    savings = total_max - total_min
    pct = (savings / total_max * 100) if total_max > 0 else 0
    print(f"\nAhorro total (siempre el más barato vs siempre el más caro): ${savings:.4f} ({pct:.1f}%)")


my_scenarios = [
    {"name": "Onboarding: 50 documentos ID", "images": 50, "input_tokens": 1500, "output_tokens": 400},
    {"name": "Catálogo: 500 productos", "images": 500, "input_tokens": 800, "output_tokens": 200},
    {"name": "Auditoría: 10 contratos largos", "images": 10, "input_tokens": 50_000, "output_tokens": 5_000},
    {"name": "Monitoring: 2000 screenshots/día", "images": 2000, "input_tokens": 1000, "output_tokens": 100},
]

generate_cost_report(my_scenarios)

Ejercicio 3 (Difícil): VisionRouter completo con métricas

Extiende el VisionRouter para implementar:

  1. Retry con backoff exponencial: El primer retry espera 1s, el segundo 2s, el tercero 4s.
  2. Logging estructurado: Cada intento se registra con timestamp, provider, latencia, éxito/fallo, y código de error si aplica.
  3. Métricas acumuladas: get_metrics() retorna un dict con: total de requests, tasa de éxito por provider, latencia p50 y p95 por provider, costo total acumulado.
  4. Selección dinámica: Si un provider tiene >50% de fallos en las últimas 10 llamadas, se salta automáticamente.
Ver solución
import time, statistics, logging
from datetime import datetime
from dataclasses import dataclass, field
from typing import Callable
from collections import deque

logger = logging.getLogger(__name__)

@dataclass
class AttemptRecord:
    timestamp: str
    provider: str
    success: bool
    latency_ms: float
    cost_usd: float
    error_code: str | None = None

@dataclass
class SmartVisionRouter:
    providers: list[dict]
    max_retries: int = 3
    base_delay_s: float = 1.0
    history: list[AttemptRecord] = field(default_factory=list)
    recent_by_provider: dict[str, deque] = field(default_factory=dict)
    total_cost: float = 0.0

    def __post_init__(self):
        for p in self.providers:
            self.recent_by_provider[p["name"]] = deque(maxlen=10)

    def _is_healthy(self, name: str) -> bool:
        recent = self.recent_by_provider[name]
        if len(recent) < 5:
            return True
        return sum(1 for r in recent if not r.success) / len(recent) <= 0.5

    def _record(self, name, success, latency, cost=0, error=None):
        rec = AttemptRecord(datetime.now().isoformat(), name, success, round(latency, 1), cost, error)
        self.history.append(rec)
        self.recent_by_provider[name].append(rec)
        if success:
            self.total_cost += cost
        return rec

    def call(self, image_b64: str, prompt: str) -> dict:
        last_error = None
        for prov in self.providers:
            name, call_fn = prov["name"], prov["fn"]
            if not self._is_healthy(name):
                logger.warning(f"Saltando {name}: tasa de fallos > 50%")
                continue
            for attempt in range(self.max_retries):
                if attempt > 0:
                    time.sleep(self.base_delay_s * (2 ** attempt))
                start = time.perf_counter()
                try:
                    result = call_fn(image_b64, prompt)
                    latency = (time.perf_counter() - start) * 1000
                    self._record(name, True, latency, result.get("cost", 0))
                    return result
                except Exception as e:
                    latency = (time.perf_counter() - start) * 1000
                    self._record(name, False, latency, error=type(e).__name__)
                    last_error = e
                    logger.warning(f"{name} intento {attempt+1}/{self.max_retries}: {e}")
        raise RuntimeError(f"Todos los proveedores fallaron. Último error: {last_error}")

    def get_metrics(self) -> dict:
        metrics = {}
        for name in self.recent_by_provider:
            recs = [r for r in self.history if r.provider == name]
            if not recs:
                continue
            ok = [r for r in recs if r.success]
            lats = sorted(r.latency_ms for r in ok)
            metrics[name] = {
                "total_calls": len(recs),
                "success_rate": len(ok) / len(recs),
                "latency_p50_ms": round(statistics.median(lats), 1) if lats else None,
                "latency_p95_ms": round(lats[int(len(lats) * 0.95)], 1) if len(lats) >= 2 else (round(lats[0], 1) if lats else None),
            }
        return {"total_requests": len(self.history), "total_cost_usd": round(self.total_cost, 6), "providers": metrics}

Resumen

  • No existe un proveedor universalmente mejor — la elección depende de la tarea, el presupuesto y los requisitos técnicos.
  • Gemini Flash domina en costo. Claude Sonnet domina en razonamiento. GPT-4o es el equilibrio general.
  • Ejecuta benchmarks con tus propias imágenes — los rankings generales no siempre aplican a dominios específicos.
  • En producción, implementa fallback entre proveedores para garantizar disponibilidad.
  • Calcula costos con escenarios reales antes de comprometerte con un proveedor.

Recursos Adicionales

  1. OpenAI Vision Pricing — Costos actualizados de GPT-4o y GPT-4o-mini
  2. Anthropic Pricing — Costos de Claude 3.5 Sonnet, Opus y Haiku
  3. Google AI Pricing — Costos de Gemini Flash y Pro
  4. Artificial Analysis LLM Benchmarks — Benchmarks independientes de calidad y velocidad
  5. LMSYS Chatbot Arena — Rankings de modelos basados en evaluaciones humanas