mozak.tech Engenharia de IA Corporativa 65%

Parte II — Acesso Programático e Instrução de Modelos

2.2 — Conectando-se a Múltiplos Provedores de Modelos via API (Multi-provider)

Objetivo da Aula

Ao concluir esta aula, você será capaz de:

Comparar Anthropic, OpenAI, Google e Hugging Face em termos de custo, qualidade, latência e casos de uso adequados

Implementar clientes Python para múltiplos provedores com interface unificada e fallback automático

Calcular o custo real por request considerando tokens de input, output, e caching disponível em cada provedor

Escolher o modelo certo para cada caso de uso com base em critérios técnicos objetivos

Debugar erros de integração de API incluindo rate limits, timeouts e respostas malformadas

Por que isso importa

Dependência de um único provedor de IA é um risco técnico e de negócio. Em março de 2024, a API da OpenAI ficou fora por 4 horas durante horário de pico. Empresas que só tinham o OpenAI como provedor ficaram completamente paradas. Empresas com fallback para Anthropic continuaram operando.

Mas há um segundo risco mais sutil: custo. Muitos times escolhem o modelo mais potente (GPT-4o, Claude Opus) para tudo — incluindo tarefas simples que um modelo 10x mais barato resolveria com a mesma qualidade. Um sistema de classificação de sentimentos não precisa de GPT-4o. GPT-4o mini (50x mais barato) resolve igualmente bem.

O impacto em escala é brutal: uma startup processando 10 milhões de tokens por dia com GPT-4o paga $40.000/mês. Com uma seleção inteligente de modelos (GPT-4o só para o top 10% de tarefas complexas, GPT-4o mini para o restante), o mesmo volume custa $4.500/mês. Isso é a diferença entre ser rentável ou não.

Um terceiro fator: qualidade varia por domínio. Claude domina raciocínio de código longo e análise de documentos estruturados. GPT-4o tem melhor desempenho em tarefas de instrução seguida rigorosamente. Gemini 1.5 Pro é imbatível em contextos longos (até 2 milhões de tokens). Conhecer essas características permite alocar tarefas ao modelo mais adequado.

Esta unidade te dá o código e os frameworks para fazer isso de forma sistemática.

Conceitos Fundamentais

A Arquitetura de Um Provedor de LLM

Todos os provedores seguem o mesmo padrão de alto nível:

Cliente → HTTP Request (JSON) → API Gateway → Load Balancer → GPU Cluster → Response

Mas os detalhes de implementação diferem em aspectos que importam para produção:

Fundamento: RPM + TPM e Batch API

Rate limits de LLM têm dois eixos simultâneos: RPM (requests por minuto) e TPM (tokens por minuto). Você pode bater em qualquer um independentemente — 10 RPM com prompts de 50k tokens pode bater no TPM muito antes de bater no RPM. Dimensionar headroom exige calcular nos dois eixos: tokens médios por request × throughput desejado = TPM necessário. HTTP 429 é o sinal de throttling em ambos os casos — implemente backoff exponencial com jitter. Batch API oferece 50% de desconto mas com até 24h de latência: ideal para cargas offline (sumarização de documentos, geração de embeddings em batch, análise de logs históricos) onde você não precisa da resposta imediatamente.

Rate Limits: Cada provedor tem limites por minuto (RPM) e tokens por minuto (TPM) por chave de API. Quando você ultrapassa, recebe HTTP 429. A estratégia correta é backoff exponencial com jitter — não retry imediato.

Latência: GPT-4o tem TTFT (time to first token) em torno de 500ms-1s. Claude Haiku é mais rápido (~200-400ms TTFT). Gemini Flash é o mais rápido (~150-300ms). Para aplicações interativas, TTFT importa mais que throughput total.

Confiabilidade (SLA): OpenAI publica 99.9% de uptime mas na prática tem incidentes mais frequentes. Anthropic tem histórico mais estável em 2024-2025. Google tem a infraestrutura de maior escala mas os modelos de AI ainda têm variabilidade.

Formato de Response: Cada SDK tem suas idiossincrasias. OpenAI: response.choices[0].message.content. Anthropic: response.content[0].text. Google: response.text. Um cliente unificado abstrai essas diferenças.

Comparativo Técnico dos Provedores em 2025

Anthropic (Claude) - Modelos: Claude Haiku 4.5 (fast/cheap), Claude Sonnet 4.6 (balanced), Claude Opus 4.8 (most capable) - Preços: Haiku $0.25/1M input + $1.25/1M output; Sonnet $3/15; Opus $15/75 - Context window: 200k tokens (todos os modelos) - Pontos fortes: raciocínio longo, código, análise de documentos, seguimento de instruções complexas - Pontos fracos: sem geração de imagem nativa, sem TTS - Caching: Prompt Caching disponível — reutiliza tokens de prefixo por 5 minutos (60% de desconto) - Rate limits (tier free): 5 RPM, 25k TPM

OpenAI (GPT) - Modelos: GPT-4o mini (fast/cheap), GPT-4o (balanced/capable), o1/o1-mini (raciocínio extended) - Preços: 4o mini $0.15/0.60; 4o $2.50/10; o1 $15/60 - Context window: 128k tokens - Pontos fortes: function calling maduro, embeddings text-embedding-3-large, DALL-E 3, Whisper - Pontos fracos: context window menor que Claude, consistência variável em respostas longas - Batch API: 50% de desconto para jobs assíncronos com latência de 24h - Rate limits (tier 1): 500 RPM, 200k TPM

Google (Gemini) - Modelos: Gemini 1.5 Flash (ultra rápido), Gemini 1.5 Pro (contexto 1M), Gemini 2.0 Flash (novo) - Preços: Flash grátis até 15 RPM; Pro $3.50/10.50 por 1M tokens - Context window: 1 MILHÃO de tokens (Gemini 1.5 Pro) — único no mercado - Pontos fortes: contexto gigante, velocidade, preço competitivo, grounded search - Pontos fracos: qualidade de raciocínio inferior a Claude/GPT-4o para tarefas complexas - Grounding: pode buscar no Google Search em tempo real como parte da resposta

Hugging Face / Open Source - Modelos: Llama 3.1/3.2, Mistral Large, Qwen2.5, Falcon, Command R+ - Hosting: Inference API (pago), Inference Endpoints (self-host managed), ou seu próprio servidor - Preços: Inference API varia por modelo; self-host tem custo de GPU (A100 80GB ~$2-3/h) - Pontos fortes: privacidade de dados total, sem custo de API em escala, customização - Pontos fracos: infraestrutura mais complexa, qualidade ainda inferior aos top comerciais

Quando Usar Cada Provedor

Use Claude quando: - A tarefa envolve análise de documentos longos (contratos, papers, código longo) - Precisa de raciocínio em múltiplos steps com lógica complexa - O output deve seguir instruções detalhadas com alta fidelidade - Você quer Prompt Caching para prefixos que repetem (system prompts longos, documentos de referência)

Use GPT-4o quando: - Você precisa de function calling/tools com schemas complexos (API mais madura) - A tarefa envolve visão + texto com análise nuançada - Você usa embeddings em escala (text-embedding-3-large é referência) - Você precisa de geração de imagem (DALL-E 3) integrada

Use Gemini quando: - Contexto > 100k tokens (contratos gigantes, codebases inteiros) - Volume alto e custo é crítico (Flash é o mais barato com boa qualidade) - Você precisa de grounded search (resposta fundamentada em busca Google ao vivo)

Use modelo open source quando: - Dados não podem sair da sua infraestrutura (saúde, financeiro, jurídico regulado) - Volume é muito alto e o custo de API tornaria o produto inviável - Você precisa de fine-tuning específico do domínio

A Interface Unificada: Por Que Abstrair

Construir um LLMClient abstrato com implementações concretas por provedor te dá:

Fallback automático: se Claude falhar, tenta GPT-4o sem mudança no código de negócio

Troca de provedor: atualizar de GPT-4 para GPT-4o é uma linha de configuração, não refatoração

A/B testing: servir 50% dos usuários para Anthropic e 50% para OpenAI para comparar qualidade

Testabilidade: mock um único protocolo em vez de dois SDKs diferentes

Monitoramento unificado: métricas de custo/latência/qualidade no mesmo sistema

O padrão é o mesmo que você usa para banco de dados: um Repository abstrato com implementações PostgresRepository e MongoRepository. Mesma ideia aplicada a LLMs.

Aprofundamento Técnico

O Protocolo Comum: O Que Todo LLM Precisa

Todo request de LLM tem o mesmo DNA:

# Estrutura universal de um request LLM

{

    "model": str,           # identificador do modelo

    "messages": [           # histórico de conversa

        {"role": "system", "content": str},   # instrução do sistema (opcional)

        {"role": "user", "content": str},     # input do usuário

        {"role": "assistant", "content": str}, # resposta anterior (para multi-turn)

    ],

    "max_tokens": int,      # limite de tokens de output

    "temperature": float,   # 0.0 = determinístico, 1.0+ = criativo

    # ...parâmetros opcionais específicos de cada provedor

}

E todo response tem:

{

    "text": str,            # o texto gerado

    "input_tokens": int,    # tokens consumidos no input

    "output_tokens": int,   # tokens gerados no output

    # ...metadados específicos do provedor (stop reason, etc.)

}

A nossa classe Resposta no starter captura exatamente isso — o mínimo viável que toda integração precisa.

Como o SDK da Anthropic Funciona

from anthropic import Anthropic



client = Anthropic(api_key='...')  # usa ANTHROPIC_API_KEY do env se omitido



# Request básico

response = client.messages.create(

    model='claude-haiku-4-5-20251001',

    max_tokens=500,

    system='Você é um assistente técnico.',   # system prompt separado de messages

    messages=[

        {'role': 'user', 'content': 'O que é um tensor?'}

    ]

)



# Extraindo o texto

texto = response.content[0].text        # content é lista (pode ter múltiplos blocos)



# Extraindo uso de tokens

input_tokens = response.usage.input_tokens

output_tokens = response.usage.output_tokens



# Stop reason (importante para tool_use loops)

stop_reason = response.stop_reason     # 'end_turn' | 'max_tokens' | 'tool_use'

Diferença crucial: Anthropic separa system do array messages. OpenAI coloca system dentro de messages como {"role": "system", ...}. Isso importa na hora de construir o adapter.

Como o SDK da OpenAI Funciona

from openai import OpenAI



client = OpenAI(api_key='...')  # usa OPENAI_API_KEY do env



response = client.chat.completions.create(

    model='gpt-4o-mini',

    max_tokens=500,

    messages=[

        {'role': 'system', 'content': 'Você é um assistente técnico.'},  # system dentro de messages

        {'role': 'user', 'content': 'O que é um tensor?'}

    ]

)



texto = response.choices[0].message.content   # choices[0] (pode ter N completions)

input_tokens = response.usage.prompt_tokens

output_tokens = response.usage.completion_tokens

Como o SDK da Google Funciona

import google.generativeai as genai



genai.configure(api_key='...')  # usa GOOGLE_API_KEY do env



model = genai.GenerativeModel('gemini-1.5-flash')



# Gemini tem API diferente — não usa messages como lista diretamente

response = model.generate_content('O que é um tensor?')

texto = response.text



# Para multi-turn (chat)

chat = model.start_chat()

response = chat.send_message('O que é um tensor?')

texto = response.text



# Tokens de uso

input_tokens = response.usage_metadata.prompt_token_count

output_tokens = response.usage_metadata.candidates_token_count

Gemini tem a API mais diferente das três. O GenerativeModel é instanciado separadamente e o sistema de chat é stateful no objeto chat.

Tratamento de Erros por Provedor

Cada provedor tem suas exceções específicas, mas o padrão de tratamento é o mesmo:

# Anthropic

from anthropic import RateLimitError, APIError, APIConnectionError

# anthropic.RateLimitError → 429

# anthropic.APIError → 4xx/5xx genérico

# anthropic.APIConnectionError → timeout/rede



# OpenAI

from openai import RateLimitError, APIError, APIConnectionError

# mesmos nomes, implementações diferentes



# Google

from google.api_core.exceptions import ResourceExhausted, GoogleAPIError

# ResourceExhausted → 429 (rate limit)

# GoogleAPIError → erros genéricos da API



# Pattern unificado de retry

import time

import random



def com_retry(fn, max_tentativas=3, base_delay=1.0):

    for tentativa in range(max_tentativas):

        try:

            return fn()

        except (RateLimitError, ResourceExhausted):

            if tentativa == max_tentativas - 1:

                raise

            # Backoff exponencial com jitter (aleatoriedade previne "thundering herd")

            delay = base_delay * (2 ** tentativa) + random.uniform(0, 1)

            time.sleep(delay)

        except (APIConnectionError,) as e:

            if tentativa == max_tentativas - 1:

                raise

            time.sleep(base_delay)

O jitter (aleatoriedade) é crítico: se 100 clients falham ao mesmo tempo e todos tentam de novo no mesmo segundo, você causa outro rate limit. O jitter espalha as tentativas no tempo.

Exemplos Anotados

Exemplo 1: Implementação Completa dos Clientes

import os

import time

import random

from abc import ABC, abstractmethod

from dataclasses import dataclass

from typing import Optional





@dataclass

class Resposta:

    texto: str

    tokens_input: int

    tokens_output: int

    modelo: str

    provedor: str



    @property

    def custo_estimado_usd(self) -> float:

        """Estimativa de custo baseada em preços públicos de 2025."""

        precos = {

            # (input_per_1M, output_per_1M)

            'anthropic/claude-haiku-4-5-20251001': (0.25, 1.25),

            'anthropic/claude-sonnet-4-6': (3.0, 15.0),

            'openai/gpt-4o-mini': (0.15, 0.60),

            'openai/gpt-4o': (2.50, 10.0),

            'google/gemini-1.5-flash': (0.075, 0.30),

        }

        key = f'{self.provedor}/{self.modelo}'

        if key not in precos:

            return 0.0

        inp, out = precos[key]

        return (self.tokens_input / 1_000_000) * inp + (self.tokens_output / 1_000_000) * out





class LLMClient(ABC):

    """Interface abstrata — todos os clientes implementam esta."""



    @abstractmethod

    def completar(

        self,

        mensagens: list[dict],

        max_tokens: int = 500,

        temperature: float = 0.7,

        system: Optional[str] = None,

    ) -> Resposta:

        pass



    def completar_com_retry(self, *args, max_tentativas=3, **kwargs) -> Resposta:

        """Retry com backoff exponencial + jitter."""

        for tentativa in range(max_tentativas):

            try:

                return self.completar(*args, **kwargs)

            except Exception as e:

                nome_erro = type(e).__name__

                # Só retenta em erros recuperáveis

                if 'RateLimit' in nome_erro or 'ResourceExhausted' in nome_erro or 'Timeout' in nome_erro:

                    if tentativa < max_tentativas - 1:

                        delay = (2 ** tentativa) + random.uniform(0, 0.5)

                        print(f"[retry] {nome_erro} — aguardando {delay:.1f}s (tentativa {tentativa+1}/{max_tentativas})")

                        time.sleep(delay)

                        continue

                raise  # não recuperável — propaga imediatamente





class ClienteAnthropic(LLMClient):

    """Cliente para API da Anthropic (Claude)."""



    def __init__(self, modelo: str = 'claude-haiku-4-5-20251001'):

        # TODO 1: Inicialize o cliente Anthropic

        # from anthropic import Anthropic

        # self.client = Anthropic(api_key=os.environ.get('ANTHROPIC_API_KEY'))

        # self.modelo = modelo

        self.modelo = modelo

        self._client = None  # substituir por cliente real no TODO



    def completar(self, mensagens, max_tokens=500, temperature=0.7, system=None) -> Resposta:

        # TODO 2: Use self.client.messages.create para chamar a API

        # - model=self.modelo

        # - max_tokens=max_tokens

        # - messages=mensagens

        # - system=system (se fornecido)

        # Retorne Resposta(texto, tokens_input, tokens_output, self.modelo, 'anthropic')

        raise NotImplementedError("TODO: implemente ClienteAnthropic.completar()")





class ClienteOpenAI(LLMClient):

    """Cliente para API da OpenAI (GPT)."""



    def __init__(self, modelo: str = 'gpt-4o-mini'):

        # TODO 3: Inicialize o cliente OpenAI

        # from openai import OpenAI

        # self.client = OpenAI(api_key=os.environ.get('OPENAI_API_KEY'))

        # self.modelo = modelo

        self.modelo = modelo



    def completar(self, mensagens, max_tokens=500, temperature=0.7, system=None) -> Resposta:

        # TODO 4: Use self.client.chat.completions.create

        # ATENÇÃO: OpenAI coloca system DENTRO de messages, não separado

        # Se system for fornecido, adicione {'role': 'system', 'content': system} no INÍCIO de mensagens

        # response.choices[0].message.content para o texto

        # response.usage.prompt_tokens / completion_tokens para tokens

        raise NotImplementedError("TODO: implemente ClienteOpenAI.completar()")





class ClienteMultiProvider(LLMClient):

    """Orquestra múltiplos clientes com fallback automático."""



    def __init__(self, clientes: list[LLMClient]):

        # Ordem de preferência: primeiro cliente é o primário

        self.clientes = clientes



    def completar(self, mensagens, max_tokens=500, temperature=0.7, system=None) -> Resposta:

        erros = []

        for cliente in self.clientes:

            try:

                return cliente.completar_com_retry(

                    mensagens, max_tokens=max_tokens,

                    temperature=temperature, system=system

                )

            except Exception as e:

                erros.append(f"{type(cliente).__name__}: {e}")

                print(f"[fallback] {type(cliente).__name__} falhou: {e}")

                continue



        raise RuntimeError(

            f"Todos os provedores falharam:\n" + "\n".join(erros)

        )
Como o arquiteto lê este código

O código define uma interface de provedor de LLM. class LLMClient(ABC) declara uma interface abstrata — como interface em Java/TypeScript: qualquer classe que herdar de LLMClient é obrigada a implementar os métodos com @abstractmethod. @dataclass em Resposta é açúcar sintático: gera automaticamente o construtor __init__ com todos os campos declarados. @property define um método que se acessa como atributo — resposta.custo_estimado_usd chama a função sem parênteses. getattr(usage, 'cache_creation_input_tokens', 0) lê o campo se existir, retorna 0 caso contrário — leitura defensiva para campos que podem estar ausentes dependendo do provedor.

Exemplo 2: Implementações Reais com Comparação de Qualidade

# Implementação completa (depois de completar os TODOs)



from anthropic import Anthropic as AnthropicSDK

from openai import OpenAI as OpenAISDK

import google.generativeai as genai



class ClienteAnthropic(LLMClient):

    def __init__(self, modelo='claude-haiku-4-5-20251001'):

        self.client = AnthropicSDK(api_key=os.environ['ANTHROPIC_API_KEY'])

        self.modelo = modelo



    def completar(self, mensagens, max_tokens=500, temperature=0.7, system=None):

        kwargs = {

            'model': self.modelo,

            'max_tokens': max_tokens,

            'messages': mensagens,

        }

        if system:

            kwargs['system'] = system



        response = self.client.messages.create(**kwargs)



        return Resposta(

            texto=response.content[0].text,

            tokens_input=response.usage.input_tokens,

            tokens_output=response.usage.output_tokens,

            modelo=self.modelo,

            provedor='anthropic',

        )





class ClienteOpenAI(LLMClient):

    def __init__(self, modelo='gpt-4o-mini'):

        self.client = OpenAISDK(api_key=os.environ['OPENAI_API_KEY'])

        self.modelo = modelo



    def completar(self, mensagens, max_tokens=500, temperature=0.7, system=None):

        # OpenAI: system vai DENTRO de messages, não separado

        msgs = list(mensagens)  # cópia para não mutar o original

        if system:

            msgs = [{'role': 'system', 'content': system}] + msgs



        response = self.client.chat.completions.create(

            model=self.modelo,

            max_tokens=max_tokens,

            messages=msgs,

        )



        return Resposta(

            texto=response.choices[0].message.content,

            tokens_input=response.usage.prompt_tokens,

            tokens_output=response.usage.completion_tokens,

            modelo=self.modelo,

            provedor='openai',

        )





# Comparação de qualidade entre provedores

def comparar_provedores(pergunta: str):

    clientes = {

        'Claude Haiku': ClienteAnthropic('claude-haiku-4-5-20251001'),

        'GPT-4o mini': ClienteOpenAI('gpt-4o-mini'),

    }



    mensagem = [{'role': 'user', 'content': pergunta}]

    system = 'Responda de forma técnica e concisa em 2-3 frases.'



    print(f"\nPergunta: {pergunta}\n" + "="*50)



    for nome, cliente in clientes.items():

        start = time.time()

        resposta = cliente.completar(mensagem, max_tokens=200, system=system)

        latencia = time.time() - start



        print(f"\n{nome}:")

        print(f"  Resposta: {resposta.texto}")

        print(f"  Tokens: {resposta.tokens_input} in / {resposta.tokens_output} out")

        print(f"  Custo: ${resposta.custo_estimado_usd:.6f}")

        print(f"  Latência: {latencia:.2f}s")





comparar_provedores("Explique o mecanismo de atenção em Transformers em 2 frases.")

Padrões e Armadilhas

Padrões Corretos

Padrão 1: Env vars para credenciais, nunca hardcode

# CORRETO: lê do ambiente

client = Anthropic(api_key=os.environ.get('ANTHROPIC_API_KEY'))



# ERRADO: expõe a chave no código (vai parar no git)

client = Anthropic(api_key='sk-ant-...')

Se a chave hardcoded for ao git, mesmo em branch privada, considere-a comprometida. Rotate imediatamente.

Padrão 2: Backoff com jitter, não sleep fixo Rate limits são compartilhados. Se você tem 10 workers fazendo retry simultâneo com sleep de 2s fixo, todos tentam de novo ao mesmo tempo e causam outro rate limit. Jitter (aleatoriedade de ±0.5s) espalha as tentativas.

Padrão 3: Interface abstrata para testabilidade

# Em produção

cliente = ClienteAnthropic()



# Em testes

class ClienteMock(LLMClient):

    def completar(self, mensagens, **kwargs):

        return Resposta(texto="resposta mock", tokens_input=10, tokens_output=5, ...)



# Testes não fazem chamada de API real — rápidos e baratos

Padrão 4: Logar tokens e custo em produção

# Após cada request:

logger.info("llm_call", extra={

    "provedor": resposta.provedor,

    "modelo": resposta.modelo,

    "tokens_in": resposta.tokens_input,

    "tokens_out": resposta.tokens_output,

    "custo_usd": resposta.custo_estimado_usd,

    "latencia_ms": latencia_ms,

})

Sem esses logs, você não sabe quando seus custos explodiram até ver a fatura do mês.

Armadilhas

⚠️ Armadilha 1: Usar o mesmo modelo para tudo

# ERRADO: usa Sonnet (caro) até para classificação simples

modelo = 'claude-sonnet-4-6'  # $3/$15 por 1M



# CORRETO: Haiku para classificação, Sonnet para geração complexa

modelo_classificacao = 'claude-haiku-4-5-20251001'   # $0.25/$1.25 — 12x mais barato

modelo_geracao = 'claude-sonnet-4-6'

⚠️ Armadilha 2: Não tratar choices[0] null no OpenAI

# Pode retornar None se stop reason for 'content_filter'

texto = response.choices[0].message.content

if texto is None:  # ← precisa checar!

    raise ValueError(f"Conteúdo filtrado. Stop reason: {response.choices[0].finish_reason}")

⚠️ Armadilha 3: Mutar a lista de mensagens original

# ERRADO: modifica o original (bug sutil em loops multi-turn)

if system:

    mensagens.insert(0, {'role': 'system', 'content': system})



# CORRETO: trabalha em cópia

msgs = [{'role': 'system', 'content': system}] + list(mensagens)

⚠️ Armadilha 4: Comparar == em stop_reason do Anthropic

# ERRADO: comparação de string pode falhar se houver versão nova

if response.stop_reason == 'end_turn':  # ok hoje



# MELHOR: checar o que importa (não é tool_use)

if response.stop_reason != 'tool_use':

    # terminamos
⚗ Laboratório prático — mozak.tech
Se não for realizar o laboratório, pule para o próximo capítulo.

Ponte para o Lab

O starter tem 4 TODOs que implementam os dois clientes:

TODO 1 — Em ClienteAnthropic.__init__(): importe Anthropic do SDK, instancie com a chave de API do ambiente, e atribua a self.client. A linha é: self.client = Anthropic(api_key=os.environ.get('ANTHROPIC_API_KEY')). Seção de referência: Aprofundamento Técnico → “Como o SDK da Anthropic Funciona”.

TODO 2 — Em ClienteAnthropic.completar(): use self.client.messages.create() com os parâmetros corretos. Extraia response.content[0].text para texto e response.usage.input_tokens/output_tokens para tokens. Retorne um Resposta(...). Seção de referência: Exemplos Anotados → Exemplo 2.

TODO 3 — Em ClienteOpenAI.__init__(): importe OpenAI do SDK, instancie e atribua a self.client. Análogo ao TODO 1 mas para OpenAI.

TODO 4 — Em ClienteOpenAI.completar(): atenção à diferença chave — OpenAI coloca o system DENTRO de messages (não como parâmetro separado). Se system for fornecido, adicione {'role': 'system', 'content': system} no início da lista de mensagens. Use response.choices[0].message.content para o texto e response.usage.prompt_tokens/completion_tokens para tokens. Seção de referência: Aprofundamento Técnico → “Como o SDK da OpenAI Funciona”.

Após implementar, a função comparar_provedores() no main() do starter chama os dois clientes com a mesma pergunta e compara custo e latência. Você verá na prática que Haiku e GPT-4o mini dão respostas de qualidade similar para perguntas factual simples — e a diferença de custo é mínima nessa escala, mas explode com volume.

Agora você está pronto para o lab.