Pular para conteúdo

OpenAI e Responses API

Implementação e consumidores

src/core/integrations/openai_provider/client.py é a fronteira do SDK. OpenAIClient implementa LLMClient.chat() e chama self._client.responses.create(). O nome chat() é o contrato interno legado; o código atual não usa Chat Completions.

src/core/integrations/factory.py cria esse client e o injeta em src/core/agents/invoker.py (AgentInvokerImpl), no reparo de formato em src/core/application/process_message_use_case.py e em src/core/application/operational_summary.py. O invocador monta prompt, histórico, contexto e seleção de modelo; o client apenas transporta a chamada e converte resultado/erros.

class LLMClient(Protocol):
    def chat(self, *, model: str,
             messages: Sequence[dict[str, Any]]) -> LLMCallResult: ...

Não há um flag que selecione stub OpenAI em build_dependencies(). Testes usam mocks do underlying SDK ou implementações de LLMClient/AgentInvoker injetadas. Desativar CRM, WhatsApp ou eGestor não torna a conversa livre de chamadas OpenAI.

Autenticação e configuração

load_settings() resolve OPENAI_API_KEY primeiro. Se ausente, busca OPENAI_API_KEY_SSM_PARAM com boto3.client("ssm").get_parameter(WithDecryption=True); sem ambos lança RuntimeError no bootstrap. O SDK recebe a chave pelo construtor OpenAI(api_key=..., timeout=...). Nenhum token real é necessário nos exemplos desta documentação.

Configuração Consumidor e default do código
OPENAI_MODEL_TRIAGE Triagem; gpt-4o-mini
OPENAI_MODEL_QUALIFICATION Qualificação; gpt-4o-mini
OPENAI_MODEL_COLLECTION Coleta; gpt-4o-mini
OPENAI_MODEL_SUMMARY Resumo; gpt-4o-mini
OPENAI_REASONING_EFFORT Opcional global, omitido se vazio; enviado como reasoning.effort
LLM_TIMEOUT_SECONDS 30 s por operação do SDK
LLM_RETRY_MAX 1 retry do wrapper, além da tentativa inicial

Divergência de defaults e deploy

.env.example exemplifica gpt-4o para os três agentes e gpt-4o-mini para resumo, com reasoning medium; Settings usa gpt-4o-mini e reasoning ausente. Os parâmetros SAM usam gpt-5.6-luna para todos e reasoning vazio. scripts/deploy_from_env.ps1 repassa os modelos, mas não OPENAI_REASONING_EFFORT para OpenAiReasoningEffort. Defina explicitamente o conjunto desejado e confira o ambiente resolvido. Comentários antigos sobre qualidade/preço de modelos não são avaliação atual.

Settings não valida compatibilidade modelo/reasoning, nem restringe a string aos valores mencionados em seu comentário. Uma combinação incompatível pode falhar como 4xx no provider.

Payload enviado

O request efetivo usa model, input e, opcionalmente, reasoning. Não configura streaming, response_format, JSON Schema, tools, previous_response_id, store ou limite explícito de output. A aplicação exige o formato {JSON}Mensagem no prompt e valida o texto depois.

{
  "model": "<MODELO_CONFIGURADO>",
  "input": [
    {"role": "system", "content": "<PROMPT_MONTADO_PELO_AGENTE>"},
    {"role": "user", "content": "Quero falar sobre um serviço."}
  ],
  "reasoning": {"effort": "medium"}
}

input é a lista de mensagens fornecida pelo invocador, não um histórico mantido pela Responses API. O invocador filtra mensagens pelo session_id, inclui as instruções do agente, packages solicitados e dados progressivos. As regras exatas estão em agentes e pacotes de contexto.

Response e telemetria

O client lê response.output_text, response.model e response.usage. Um sucesso fictício resulta em:

LLMCallResult(
    raw_response='{"s":1}Olá! Como posso ajudar?',
    model="modelo-retornado-pelo-provider",
    tokens_input=420,
    tokens_output=27,
    latency_ms=812,
)

Sem output_text, retorna string vazia; sem usage, tokens viram zero. _log_outbound() em process_message_use_case.py copia modelo, tokens e latência para Message. latency_ms mede apenas a tentativa bem-sucedida do wrapper, e não o tempo total de retries. Os contadores de tokens são da resposta final; não são uma auditoria completa de cobrança de todas as tentativas, reparos e resumos. O client não devolve request ID, usage detalhado de reasoning ou custo em moeda.

Falhas, retries e fallback

Falha observada no SDK Wrapper Efeito no fluxo
APITimeoutError Registra llm_timeout; repete até 1 + LLM_RETRY_MAX; esgotado lança LLMTimeoutError Handoff por llm_timeout
APIStatusError 4xx, inclusive 429 llm_4xx; lança imediatamente LLMProviderError no wrapper Atualmente também registrado como motivo llm_timeout pelo core
APIStatusError fora de 4xx llm_5xx; repete e lança LLMProviderError ao esgotar Handoff no tratamento de IntegrationError
Resposta em formato inválido Client devolve texto; parser e reparo atuam acima dele Uma tentativa de reparo; depois llm_invalid_json
Resumo inválido ou IntegrationError no resumo summary_parse_failed / summary_llm_failed Objeto mínimo de fallback; não bloqueia o handoff para esses erros

Exemplo: autenticação 401 produz LLMProviderError("OpenAI 401: ..."). O log llm_4xx contém modelo, status e tentativa. O motivo de handoff salvo é llm_timeout, porque _handle_llm_failure() agrupa todas as IntegrationError; não interprete esse motivo sozinho como prova de timeout.

Limites efetivos de retry

O construtor não define max_retries no SDK. Assim, a política interna da versão instalada continua ativa além do loop do wrapper; LLM_RETRY_MAX=0 elimina apenas o retry escrito neste módulo. Os testes com SDK mockado validam o loop local, não o número efetivo de requests de rede. Não calcule deadline global como simplesmente timeout × tentativas.

O wrapper captura somente APITimeoutError e APIStatusError. Outras exceções do SDK, como conexão fora dessas classes, não são convertidas por este módulo; podem alcançar o handler SQS e gerar retry do item. A alegação do docstring de que o resumo nunca bloqueia deve ser lida com esse limite: o gerador captura IntegrationError, não qualquer exceção Python.

Não existe chave de idempotência para inferências. O histórico e latest-message-wins impedem que resultados ultrapassados alterem sessão/enviem resposta antes do commit, mas uma chamada já iniciada pode consumir processamento mesmo se seu resultado for descartado.

Segurança, testes e manutenção

O client registra metadados de falha, não prompts ou chave. Entretanto, mensagens, dados cadastrais e packages realmente são enviados ao provider, e raw_response pode ser persistido no Message Log. O request não declara uma política de retenção via store; a auditoria do código não confirma configurações da conta externa. Restrinja acesso ao histórico e siga a observabilidade.

tests/unit/integrations/test_openai_client.py verifica Responses API, telemetria, usage ausente, reasoning, timeout, 5xx e 4xx sem retry local. tests/unit/application/test_process_message_use_case.py cobre reparo/handoff; tests/unit/application/test_operational_summary.py cobre resumo/fallback. Os testes em tests/integration/ chamam OpenAI real quando habilitados e não foram executados nesta tarefa.

python -m pytest tests/unit/integrations/test_openai_client.py -q

Para trocar modelo, configure os quatro usos e reasoning, rode testes de contrato e replays, e depois homologue deliberadamente com provider real. Mudanças podem afetar formato, decisões, latência e tokens; o repositório não oferece garantia comparativa entre modelos. Para trocar provider, implemente LLMClient, preserve LLMCallResult e conversão de exceções, e altere a factory. Mantenha validação e fallback fora do adapter, conforme o contrato de resposta.