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.