Skip to content

Resposta recusada com número no texto não vira erro de credencial - #203

Merged
bdcdo merged 18 commits into
mainfrom
fix/numero-na-resposta-nao-e-credencial
Oct 14, 2026
Merged

bdcdo merged 18 commits into
mainfrom
fix/numero-na-resposta-nao-e-credencial

Conversation

@bdcdo

@bdcdo bdcdo commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Problema

Quando a resposta do modelo falhava no parse ou na validação e o texto continha 401, 403 ou 404, a falha era classificada como erro de credencial ou de permissão. Em texto jurídico isso é comum: "art. 401 da CLT", "Súmula 403 do STJ".

  • claude_code: o ValidationError ou o ValueError do parse_json saía com o valor recusado na mensagem, e is_recoverable_error procurava os códigos HTTP nesse texto. A linha falhava na primeira tentativa, e a mensagem amigável mandava reconfigurar a autenticação. Com "art. 12" no lugar, a mesma falha era tentada de novo.
  • codex: o ProviderOutputError levava o valor recusado na mensagem, e a mensagem amigável, classificada pelo texto, mandava rodar codex login.

O que muda

  • claude_code: a falha de parsing ou de validação levanta ProviderRejectedOutputError, transitória pela classe, como no caminho LangChain.
  • get_friendly_error_message decide pela classe também para ProviderOutputError, antes de qualquer busca no texto. is_rate_limit_error decide pela classe para todo ProviderError e para o ProviderOutputError, e entre eles só o ProviderOverloadedError conta como rate limit. A mensagem dessas exceções pode trazer texto do modelo: o resumo da validação, no ProviderOutputError do codex, e o campo result do SDK, no ProviderError do claude_code. Um '429' nesse texto reduzia os workers sem haver limite de requisições. O texto da mensagem de ProviderRejectedOutputError deixa de afirmar que o erro foi levado de volta ao modelo, o que só vale para o LangChain.
  • codex: o erro JSON-RPC de código -32001, com que o app-server 0.160.1 recusa o pedido quando a fila de entrada está cheia ("Server overloaded; retry later.", data nulo), levanta ProviderOverloadedError, pelo código e sem ler a mensagem. O is_retryable_error do SDK procura a sobrecarga só no data, e esse erro saía ProviderError: na main, a linha falhava na primeira tentativa e os workers não caíam; agora a linha é repetida e, se falhar em todas as tentativas, os workers caem. O erro do codex, de turno ou JSON-RPC, que traz "429 Too Many Requests" só no texto não reduz os workers: o rate limit é reconhecido na origem, pelo código do erro.
  • validation_error_summary, em errors.py, monta o caminho e a regra de cada erro sem o valor recusado (o input do Pydantic). O LangChain já gravava assim; agora o claude_code e o codex usam a mesma função. Caminho e regra ainda trazem texto da resposta quando o Pydantic o põe ali (chave de dict, tag de união, mensagem de validador próprio), e por isso a classificação decide pela classe.
  • Documentação: a tabela de exceções e o guia de tratamento de erros, em pt e en, descrevem o comportamento de cada provider diante da resposta recusada. O FAQ, o guia de performance, nas duas línguas, e a docstring de dataframeit() deixam de dizer que os workers caem a cada 429: eles caem pela metade quando uma linha falha depois de todas as tentativas com erro de rate limit ou de sobrecarga. O codex reconhece o rate limit pelo código ou pelo status do erro, sem ler a mensagem, e os providers do LangChain também pelo texto da mensagem quando o erro não traz status HTTP.

O pedido de correção com retorno no claude_code e a nova tentativa no codex ficam para a refatoração descrita em #193.

Verificação

  • Testes novos em tests/test_claude_code.py, tests/test_codex.py e tests/test_errors.py, que falham sem a correção (inclusive o ramo do ProviderOutputError com número no resumo da validação e o ProviderError com "429" no texto, montado também como _raise_for_result_error monta o erro do claude_code, que não reduz os workers): resposta com "art. 401" e JSON truncado com "Súmula 403" são tentados de novo no claude_code; no codex, a mensagem não traz o valor e a amigável não fala em credencial.
  • No codex, o erro -32001 com data nulo, montado pelo map_jsonrpc_error do SDK, vira ProviderOverloadedError; de ponta a ponta, com parallel_requests=4, cada linha é repetida e os workers caem de 4 para 2 e de 2 para 1. Os códigos -32000 e -32002 com a mesma mensagem continuam definitivos. A falha HTTP sem status ou 5xx e os códigos transitórios do turno são repetidos sem contar como rate limit. Oito mutações de codex.py derrubam algum teste, entre elas as três que tornavam sobrecarga a falha HTTP sem status, a 5xx e os códigos transitórios, que sobreviviam à suíte anterior.
  • Sobre a main atual (8dc97892): 1244 testes passam em Python 3.13 e em 3.11 no job unit, e 1193 no job do codex, nas duas versões; cobertura de ramos combinada em 100%; 1243 nas versões mínimas com all excel polars search-all.
  • ruff format, ruff check, ty, vulture, deptry e mkdocs build passam.

Closes #192

bdcdo added 2 commits October 3, 2026 16:28
No claude_code, a falha de parsing ou de validação da resposta saía como
ValidationError ou ValueError com o texto da resposta na mensagem, e a
classificação procurava 401, 403 e 404 nesse texto. Com "art. 401 da
CLT" na resposta, a linha falhava sem nova tentativa e a mensagem
mandava reconfigurar a autenticação. Agora a falha levanta
ProviderRejectedOutputError, transitória pela classe.

No codex, a mensagem amigável do ProviderOutputError passa a ser
decidida pela classe, e a mensagem do erro de schema deixa de trazer o
valor recusado.

validation_error_summary, em errors.py, monta o caminho e a regra de
cada erro sem o valor, para o LangChain, o claude_code e o codex.

Closes #192
is_rate_limit_error passa a decidir pela classe para ProviderError,
ProviderConfigurationError e ProviderOutputError. Um '429' no caminho
de um erro de validação, como a chave de um dict na resposta do codex,
contava como rate limit e reduzia os workers.

A mensagem amigável do ProviderOutputError descreve também a resposta
vazia e o turno interrompido, em que nenhuma regra falhou. A docstring
de validation_error_summary, o guia de erros e o CHANGELOG dizem que
caminho e regra ainda trazem texto da resposta quando o Pydantic o põe
ali. O teto de erros listados vira uma constante só, usada pelo resumo
e pelo pedido de correção do LangChain.

Testes novos cobrem o ramo do ProviderOutputError na mensagem amigável
com número no caminho e o rate limit decidido pela classe.
@bdcdo

bdcdo commented Oct 13, 2026

Copy link
Copy Markdown
Owner Author

Revisão independente: correção confirmada, PR ainda não pronto para merge

Um subagente novo revisou c1975786 contra a main atual (871c35e7) em worktrees descartáveis. Os achados abaixo são os do relatório dele e ainda não foram reproduzidos de novo fora do revisor.

A correção da issue 192 funciona. Na reprodução da issue, a main dá ValidationError, 1 tentativa, mensagem de erro de autenticação; o PR dá ProviderRejectedOutputError, 3 tentativas, mensagem de resposta recusada. O mesmo vale para JSON truncado com "Súmula 403", prosa sem JSON com "art. 404" e JSON que não é objeto. Erro real do provider com status 401, 403 e 404 continua não recuperável, igual à main. Os 7 testes novos falham com o código da main, e cada um cai com alguma mutação.

Achados abertos

  1. Não mescla com a main. Conflito só no CHANGELOG.md, na primeira linha depois de ### Corrigido: o PR e a main inseriram entradas no mesmo ponto, e a resolução é manter as três. Com isso resolvido, a árvore mesclada passa: 1162 testes em 3.13, 1157 em 3.10, 1182 no job do codex, cobertura combinada em 100%, ruff, ty, vulture, deptry e mkdocs build limpos, e as duas configurações de versões mínimas.
  2. is_rate_limit_error foi alargada além do que a issue pede (errors.py). O caso descrito só exige ProviderOutputError; a tupla ganhou também ProviderError inteiro e ProviderConfigurationError. Com ProviderError, um turno do codex que falha com "429 Too Many Requests" no texto e sem código classificável deixa de reduzir workers, embora a mensagem amigável continue dizendo que é limite de requisições. ProviderConfigurationError nunca chega a esse ponto, e tirá-la da tupla não derruba teste nenhum.
  3. Dois comentários dizem que a mensagem não traz texto da resposta, e traz (claude_code.py e errors.py): o _error_details gravado inclui a chave de dict recusada ("contagens.art. 401 da CLT") e a mensagem de um validador próprio. A classificação continua certa, porque decide pela classe.
  4. Texto novo sem teste. Voltar a mensagem amigável de ProviderRejectedOutputError ao texto antigo, ou tirar o {error} da mensagem nova de ProviderOutputError, passa na suíte inteira.

Fora do diff, e relevante para o Closes #192

Um 401 real do claude_code é repetido como erro recuperável, na main e neste PR: o SDK levanta ResultError com api_error_status=401 de dentro do query(), e esse atributo não é lido na classificação. A issue 192 termina pedindo que, onde o código HTTP vem do SDK, a classificação use o atributo; o PR não trata disso. Fica a decidir se vira issue própria ou se o Closes sai.

bdcdo added 4 commits October 13, 2026 13:00
Conflito só no CHANGELOG.md, em Corrigido, onde os dois lados inseriram
entradas no mesmo ponto; ficam todas.
A entrada do extra codex citava o pin 0.159.2; o pyproject.toml fixa
openai-codex==0.160.1, que instala o runtime 0.160.1.
A tupla tinha ganhado ProviderError e ProviderConfigurationError além de
ProviderOutputError. Com ProviderError nela, um turno do codex que falha
com "429 Too Many Requests" no texto e sem código classificável deixava
de reduzir os workers, embora a mensagem amigável dissesse limite de
requisições. Tirar ProviderConfigurationError não derrubava teste.

Fica o que a issue pede: a resposta fora do contrato, cujo resumo de
validação traz texto do modelo, decide pela classe, e o ProviderError
definitivo volta a ser lido pelo texto. Um teste prende cada lado.
Dois comentários diziam que a mensagem não traz texto da resposta. Traz:
o resumo da validação leva a chave de dict recusada e a mensagem de
validador próprio, e por isso a classificação decide pela classe.

A mensagem amigável nova não tinha teste. Voltar o texto que prometia o
erro de volta ao modelo, ou tirar o detalhe do erro da mensagem de
ProviderOutputError, passava na suíte; agora cada mudança derruba um
teste.
…o resumo

O codex recusa modelo com dict de chaves dinâmicas antes de processar, e o
exemplo da chave de dict só acontece no claude_code. O CHANGELOG, o guia de
erros e a fixture dos testes passam a usar a mensagem de um validador próprio
para o codex. Os READMEs dizem que a resposta recusada volta ao modelo só nos
providers do LangChain.

Testes novos: o resumo da validação conta todos os erros e lista até o teto, e
a mensagem da resposta fora do contrato diz que a linha não é tentada de novo.
@bdcdo

bdcdo commented Oct 13, 2026

Copy link
Copy Markdown
Owner Author

Segunda revisão independente: sem achado que bloqueie; uma decisão de desenho aberta

Os achados da primeira revisão foram reproduzidos e corrigidos em dfa197f8, com a main integrada. Um subagente novo revisou esse commit em worktree descartável: tabela de 11 classes de exceção por 7 textos comparando main e branch, 27 mutações, ponta a ponta no claude_code e no codex com SDK falso, e todos os jobs do CI reproduzidos.

O que confirmou. A correção da #192 funciona: resposta com "art. 401" na chave de um dict, validador com "Súmula 403" e JSON truncado passam de 1 tentativa com erro de credencial para nova tentativa com ProviderRejectedOutputError; resposta com "429" deixa de reduzir os workers. Na tabela, só as linhas de ProviderOutputError mudam em relação à main. Suíte: 1164 e 1159 testes (3.13 e 3.10), 1184 e 1179 no job do codex, cobertura de ramos combinada em 100%, versões mínimas, ruff, ty, vulture, deptry e mkdocs build.

Quatro achados menores, corrigidos em 7ab9a6ec:

  1. O exemplo "chave de um dict" não acontece no codex, que recusa modelo com chaves dinâmicas antes de processar. O CHANGELOG, o guia de erros e a fixture dos testes passam a usar a mensagem de um validador próprio para o codex.
  2. O teto de 20 regras e a contagem de erros de validation_error_summary não tinham teste. Teste novo; as duas mutações que sobreviviam agora caem.
  3. O rodapé da mensagem de ProviderOutputError ("não é tentada de novo") não era conferido. Asserção nova; a mutação cai.
  4. Os READMEs diziam que a resposta recusada volta ao modelo, o que só vale para os providers do LangChain.

Fora do escopo, com issue própria: o status HTTP que o SDK do Claude Code entrega em ResultError.api_error_status não é lido na classificação (#227).

Aberto, e trava o merge: como is_rate_limit_error trata o ProviderError genérico. O PR original decidia pela classe para todo ProviderError; 80ccf2b4 voltou a ler o texto, como na main, porque um turno do codex que falha só com "429 Too Many Requests" no texto deixava de reduzir os workers. A verificação automática de segurança do push apontou o outro lado: um ProviderError definitivo do claude_code leva o campo result do SDK na mensagem, e texto do modelo com "429" ali reduz os workers sem haver limite de requisições. Nenhum dos dois cenários foi visto com o runtime real. A escolha entre os dois desenhos fica para o dono do repositório.

bdcdo added 3 commits October 13, 2026 15:19
Rate limit do pacote é só ProviderOverloadedError. A mensagem de um
ProviderError do claude_code leva o campo result do SDK, que pode trazer
texto do modelo, e um "429" ali reduzia os workers sem haver limite de
requisições. Os demais ProviderError deixam de ser lidos pelo texto, como
o ProviderOutputError já não era.

Um turno do codex que falha só com "429 Too Many Requests" no texto, sem
código classificável, deixa de reduzir os workers. O teste que prendia o
comportamento anterior troca de lado, e um teste novo monta o erro do
claude_code como _raise_for_result_error o monta.
@bdcdo

bdcdo commented Oct 14, 2026

Copy link
Copy Markdown
Owner Author

Evidência: is_rate_limit_error decide pela classe para todo ProviderError

Decisão do dono do repositório aplicada em 8ebaee8e, sobre a main integrada em b01df0b0 (c0cc0ce8, Python mínimo 3.11, sem conflito). A tupla de is_rate_limit_error passa de (ProviderTransientError, ProviderOutputError) para (ProviderError, ProviderOutputError). ProviderOverloadedError continua retornando True antes dela, e é o único rate limit entre as exceções do pacote. ProviderConfigurationError ficou fora: nenhum teste a exige, e ela não herda de ProviderError.

Caso concreto: ProviderError do claude_code com "art. 429" no result

SDK falso instalado em sys.modules["claude_agent_sdk"], cujo query() devolve só um ResultMessage com is_error=True, subtype="error_max_turns", api_error_status=None e result="Não terminei a análise do fundamento do art. 429 do CPC". Primeiro a exceção é montada por _raise_for_result_error, depois dataframeit() roda 4 linhas com provider="claude_code" e parallel_requests=4, contando os avisos "Rate limit detectado". Nenhuma chamada de rede.

main (c0cc0ce8) branch antes (8c09e3dd) branch agora (8ebaee8e)
exceção ProviderError ProviderError ProviderError
is_rate_limit_error True True False
linhas com erro 4 4 4
avisos de redução de workers 2 (4 para 2, 2 para 1) 2 (4 para 2, 2 para 1) 0
mensagem amigável, primeira linha LIMITE DE REQUISIÇÕES ATINGIDO LIMITE DE REQUISIÇÕES ATINGIDO LIMITE DE REQUISIÇÕES ATINGIDO

Mensagem da exceção nos três: Claude Code SDK retornou erro (error_max_turns, status None): Não terminei a análise do fundamento do art. 429 do CPC.

Tabela de classificação, só as linhas que mudam

11 classes (as 8 exceções de errors.py, mais ValueError, RuntimeError e OutputParserException do LangChain) por 4 textos ("429 Too Many Requests", "art. 429 do CPC", "rate limit exceeded", "401"), 44 linhas por árvore. Colunas: is_rate_limit_error / is_recoverable_error / primeira linha com texto de get_friendly_error_message. A primeira linha é a mesma com provider="codex" e provider="claude_code" nas 44 linhas das três árvores.

Classe Texto main branch antes branch agora
ProviderError 429 Too Many Requests True / False / LIMITE DE REQUISIÇÕES True / False / LIMITE DE REQUISIÇÕES False / False / LIMITE DE REQUISIÇÕES
ProviderError art. 429 do CPC True / False / LIMITE DE REQUISIÇÕES True / False / LIMITE DE REQUISIÇÕES False / False / LIMITE DE REQUISIÇÕES
ProviderAbortError 429 Too Many Requests True / False / LIMITE DE REQUISIÇÕES True / False / LIMITE DE REQUISIÇÕES False / False / LIMITE DE REQUISIÇÕES
ProviderAbortError art. 429 do CPC True / False / LIMITE DE REQUISIÇÕES True / False / LIMITE DE REQUISIÇÕES False / False / LIMITE DE REQUISIÇÕES
ProviderUsageLimitError 429 Too Many Requests True / False / LIMITE DE REQUISIÇÕES True / False / LIMITE DE REQUISIÇÕES False / False / LIMITE DE REQUISIÇÕES
ProviderUsageLimitError art. 429 do CPC True / False / LIMITE DE REQUISIÇÕES True / False / LIMITE DE REQUISIÇÕES False / False / LIMITE DE REQUISIÇÕES
ProviderOutputError 429 Too Many Requests True / False / LIMITE DE REQUISIÇÕES False / False / RESPOSTA FORA DO CONTRATO False / False / RESPOSTA FORA DO CONTRATO
ProviderOutputError art. 429 do CPC True / False / LIMITE DE REQUISIÇÕES False / False / RESPOSTA FORA DO CONTRATO False / False / RESPOSTA FORA DO CONTRATO
ProviderOutputError rate limit exceeded False / False / ERRO NO PROCESSAMENTO False / False / RESPOSTA FORA DO CONTRATO False / False / RESPOSTA FORA DO CONTRATO
ProviderOutputError 401 False / False / ERRO DE AUTENTICAÇÃO False / False / RESPOSTA FORA DO CONTRATO False / False / RESPOSTA FORA DO CONTRATO

Entre o branch antes e agora mudam só as seis primeiras linhas, todas na coluna is_rate_limit_error; is_recoverable_error e a mensagem amigável não mudam em nenhuma das 44. Em core.py, ProviderAbortError e as subclasses são capturadas antes da chamada a is_rate_limit_error, e por isso as quatro linhas delas não mudam o comportamento da execução paralela. ProviderConfigurationError com "429" no texto continua True, igual à main.

A mensagem amigável ainda diz "LIMITE DE REQUISIÇÕES ATINGIDO" para um ProviderError com "429" no texto, inclusive o "art. 429 do CPC" do caso acima, que agora não reduz os workers; get_friendly_error_message não foi alterada, e o ProviderTransientError com "429" no texto já tinha essa combinação na main.

Mutações

Cada mutação trocou a linha da tupla em errors.py, rodou test_errors.py, test_classificacao_de_erro.py, test_parallel_requests.py, test_erros_de_provider.py e test_claude_code.py (145 testes) com PYTHONDONTWRITEBYTECODE=1 e __pycache__ apagado, e restaurou o arquivo, conferido por hash.

Mutação Resultado
tupla anterior, (ProviderTransientError, ProviderOutputError) 3 falham: test_429_no_result_do_claude_code_nao_conta_como_rate_limit e os dois casos de test_provider_error_com_429_na_mensagem_nao_conta_como_rate_limit
(ProviderError,), sem ProviderOutputError 1 falha: test_numero_no_resumo_da_validacao_nao_conta_como_rate_limit
sem a checagem da tupla 4 falham: as três acima e a do ProviderOutputError
tupla com ProviderConfigurationError a mais 145 passam: mutante equivalente para a suíte, e por isso a classe ficou fora

Testes executados e pulados, main ao lado do branch, no mesmo ambiente

A suíte da main rodou da árvore de c0cc0ce8 com PYTHONPATH no src dela, conferido por dataframeit.__file__, no mesmo venv da suíte do branch.

Job main (c0cc0ce8) branch (8ebaee8e)
unit, Python 3.13 1184 passaram, 105 pulados 1195 passaram, 106 pulados
unit, Python 3.11 1184 passaram, 105 pulados 1195 passaram, 106 pulados
codex-provider, Python 3.13 1175 passaram, 135 pulados 1187 passaram, 135 pulados
codex-provider, Python 3.11 1175 passaram, 135 pulados 1187 passaram, 135 pulados
lowest-direct, sem extras 1048 passaram, 241 pulados 1059 passaram, 242 pulados
lowest-direct, all excel polars search-all 1183 passaram, 106 pulados 1194 passaram, 107 pulados

Nenhuma falha. Os motivos de pulo são os mesmos nos dois lados; o pulo a mais no unit e no lowest-direct é o teste novo test_number_in_rejected_response_is_not_a_credential_error, de test_codex.py, que pede openai_codex e roda no job do codex (103 pulos por falta de openai_codex na main, 104 no branch).

  • Cobertura de ramos combinada dos quatro arquivos (unit e codex-provider, 3.11 e 3.13), com o fail_under = 100 do pyproject.toml: 3044 linhas, 1190 ramos, 0 faltando, 100%.
  • Job quality com --extra dev --extra all --extra search-all --extra polars --extra excel --extra codex em 3.13: ruff format --check src tests, ruff check src tests, ty check, vulture e deptry src passam.
  • mkdocs build com --extra docs passa, com os mesmos dois avisos do mkdocs_static_i18n e do Material que a main dá.

SHA revisado: 8ebaee8

bdcdo added 3 commits October 14, 2026 10:24
…adedError

O is_retryable_error do SDK reconhece só server_overloaded, em snake_case, no
data do erro JSON-RPC. Um CodexRpcError com rate_limit_exceeded saía
ProviderError e, com is_rate_limit_error decidindo pela classe, deixava de
reduzir os workers. O data passa a ser lido pela mesma regra do turno que
falha (_is_overload_info), nas duas grafias do código, sem ler a mensagem.
FAQ, guia de performance, nas duas línguas, e a docstring de dataframeit()
diziam que os workers caem a cada 429. Eles caem quando o provider identifica
o rate limit pelo status ou pelo código do erro.
@bdcdo

bdcdo commented Oct 14, 2026

Copy link
Copy Markdown
Owner Author

Evidência: código de rate limit no erro JSON-RPC do codex e documentação da redução de workers

Três commits sobre 8ebaee8e:

O problema

_raise_classified_sdk_error levantava ProviderOverloadedError só quando o is_retryable_error do SDK dizia que o erro é repetível, e o SDK (openai-codex==0.160.1) reconhece só server_overloaded, e só nessa grafia, em snake_case. Ele procura o código no data do erro JSON-RPC, nas chaves codex_error_info, codexErrorInfo e errorInfo e em qualquer dict ou lista dentro dele. Um CodexRpcError(-32000, "429 Too Many Requests", {"codex_error_info": "rate_limit_exceeded"}) saía ProviderError. Na main, o '429' do texto reduzia os workers. Com is_rate_limit_error decidindo pela classe, os workers deixavam de cair.

O data passa a ser lido pela mesma regra do turno que falha. _is_overload_info, extraída de _raise_turn_error, conta como sobrecarga server_overloaded, rate_limit_exceeded, flex_unavailable e o HTTP 429 de uma variante HTTP. O código é aceito nas duas grafias: camelCase, a do protocolo v2, validada pelo CodexErrorInfo do SDK, e snake_case, a que o SDK procura. A mensagem do erro não é lida.

Caso X5, antes e depois

dataframeit() com 6 linhas, parallel_requests=4, max_retries=2, provider="codex" e o backend do codex com SDK falso, montado pelos helpers de tests/test_codex.py. O thread_start levanta, em toda chamada:

CodexRpcError(-32000, "429 Too Many Requests", {"codex_error_info": "rate_limit_exceeded"})

O script conta os avisos "Rate limit detectado" e os avisos "Tentativa". A árvore de cada SHA rodou com PYTHONPATH no próprio src, conferido por dataframeit.__file__, no mesmo venv (Python 3.13, --extra dev --extra codex). Nenhuma chamada de rede.

PYTHONPATH=src python e2e.py B tests "$TMP"
Árvore Reduções de workers Avisos de nova tentativa _error_details
main (c6e99a10) 2 0 [Erro não-recuperável] ProviderError: CodexRpcError: JSON-RPC error -32000: 429 Too Many Requests
8ebaee8e 0 0 [Erro não-recuperável] ProviderError: CodexRpcError: JSON-RPC error -32000: 429 Too Many Requests
fbd05cd9 2 6 [Falhou após 2 tentativa(s)] ProviderOverloadedError: CodexRpcError: JSON-RPC error -32000: 429 Too Many Requests

Na main, os workers caíam pelo texto, mas a linha falhava sem nova tentativa. Agora os workers caem pelo código, e a linha é repetida como no turno que falha com rate_limit_exceeded (X2).

Reduções de workers por caso, nas três árvores

Mesmo script, 18 casos: os do claude_code e do LangChain com o SDK e o modelo falsos no venv com --extra dev --extra all --extra search-all, os do codex no venv do caso X5. Cada célula traz reduções de workers / avisos de nova tentativa. Nas 54 execuções, as 6 linhas terminam com status error.

Caso main 8ebaee8e fbd05cd9
claude_code: C1 is_error max_turns, result 'art. 429 do CPC' 2 / 0 0 / 0 0 / 0
claude_code: C2 is_error api_error_status=429 2 / 6 2 / 6 2 / 6
claude_code: C3 is_error api_error_status=529 2 / 6 2 / 6 2 / 6
claude_code: C4 query() levanta ResultError status 429 2 / 6 2 / 6 2 / 6
claude_code: C5 resposta com confianca='art. 429 do CPC' 2 / 6 0 / 6 0 / 6
claude_code: C6 is_error status 400, result com '429' 2 / 0 0 / 0 0 / 0
claude_code: C7 is_error sem status, result '429 Too Many Requests' 0 / 6 0 / 6 0 / 6
langchain(openai): L1 openai.RateLimitError 429 2 / 6 2 / 6 2 / 6
langchain(openai): L2 ModelRateLimitError 2 / 6 2 / 6 2 / 6
langchain(openai): L3 RuntimeError '429 Too Many Requests' 2 / 6 2 / 6 2 / 6
langchain(openai): L4 parsing_error com 'art. 429' 0 / 6 0 / 6 0 / 6
langchain(openai): L5 OutputParserException com '429' no llm_output 0 / 6 0 / 6 0 / 6
codex: X1 turno failed 'other', mensagem '429 Too Many Requests' 2 / 0 0 / 0 0 / 0
codex: X2 turno failed rate_limit_exceeded 2 / 6 2 / 6 2 / 6
codex: X3 turno failed HTTP 429 2 / 6 2 / 6 2 / 6
codex: X4 resposta com confianca='art. 429 do CPC' 2 / 0 0 / 0 0 / 0
codex: X5 thread_start levanta CodexRpcError com rate_limit_exceeded 2 / 0 0 / 0 2 / 6
codex: X6 turno failed bad_request, mensagem com 'art. 429' 2 / 0 0 / 0 0 / 0

Entre 8ebaee8e e fbd05cd9 muda só o X5. O X1 é a consequência aceita da decisão pela classe: o turno que falha com código other e '429' só no texto não reduz os workers. A tabela de classificação de is_rate_limit_error, is_recoverable_error e get_friendly_error_message (16 classes por 9 textos, 144 linhas) é idêntica em 8ebaee8e e fbd05cd9; em errors.py mudou só o comentário acima da tupla.

Testes novos

Em tests/test_codex.py, 15 casos, todos com o SDK real e sem rede:

  • test_rpc_rate_limit_code_is_overload_and_reduces_workers: o caso X5 no backend.invoke, com nova tentativa, ProviderOverloadedError e is_rate_limit_error verdadeiro.
  • test_rpc_overload_code_in_data_is_overload, 6 formas do data: rate_limit_exceeded em snake_case, rateLimitExceeded em camelCase, flex_unavailable na chave errorInfo, a variante httpConnectionFailed com status 429, o código num dict aninhado e numa lista.
  • test_rpc_error_without_overload_code_is_definitive, 7 formas, todas com "429 Too Many Requests" na mensagem: data nulo, o código como texto solto, o código numa chave que não é de código, badRequest, um código desconhecido, a variante HTTP com status 401 e unauthorized numa lista. Saem ProviderError definitivo, e is_rate_limit_error é falso.
  • test_non_rpc_error_data_is_not_read: uma exceção que não é erro JSON-RPC, com data de rate limit, continua ProviderError.

Com os testes novos sobre o src de 8ebaee8e: 7 falham e 8 passam. Falham o X5 e as 6 formas positivas, inclusive [{"codexErrorInfo": "serverOverloaded"}], porque o SDK só reconhece a grafia em snake_case. Passam os 8 casos que conferem o comportamento definitivo, que 8ebaee8e já tinha.

Mutações

Cada mutante numa cópia limpa da árvore, com PYTHONDONTWRITEBYTECODE=1, -p no:cacheprovider, -o pythonpath no src da cópia e o __file__ de dataframeit.codex conferido. Rodam tests/test_codex.py e tests/test_errors.py, e a base sem mutação passa com 248 testes.

Mutante em codex.py Testes que falham
_raise_classified_sdk_error sem a leitura do data 7
sem a guarda isinstance(error, JsonRpcError) 1 (test_non_rpc_error_data_is_not_read)
lista no data não é percorrida 1
data que não é dict conta como sobrecarga 7
código reconhecido na chave não decide 7
dict aninhado não é percorrido 1
sem a grafia em snake_case 3
código desconhecido vira rate_limit_exceeded 1
rate_limit_exceeded fora do conjunto de sobrecarga 5
HTTP 429 da variante deixa de ser sobrecarga 2
_raise_turn_error não consulta _is_overload_info 8
variante HTTP não reconhecida 4
sem a chave errorInfo 1
status 4xx da variante HTTP do turno vira transitório 1
variante HTTP do turno cai nos códigos 2

Nenhum mutante sobrevive.

Testes executados e pulados, main ao lado do branch, no mesmo ambiente

A suíte da main rodou da árvore de c6e99a10, com PYTHONPATH no src dela, conferido por dataframeit.__file__, no mesmo venv da suíte do branch. Nas versões mínimas, cada árvore foi instalada em modo editável no venv, como o job faz.

Job main (c6e99a10) branch (fbd05cd9)
unit, Python 3.13 1220 passaram, 105 pulados 1231 passaram, 121 pulados
unit, Python 3.11 1220 passaram, 105 pulados 1231 passaram, 121 pulados
codex-provider, Python 3.13 1175 passaram, 171 pulados 1202 passaram, 171 pulados
codex-provider, Python 3.11 1175 passaram, 171 pulados 1202 passaram, 171 pulados
lowest-direct, sem extras 1048 passaram, 277 pulados 1059 passaram, 293 pulados
lowest-direct, all excel polars search-all 1219 passaram, 106 pulados 1230 passaram, 122 pulados

Nenhuma falha. Os pulos a mais no unit e no lowest-direct estão todos em tests/test_codex.py (104 na main, 120 no branch, no unit): são os 15 testes novos e test_number_in_rejected_response_is_not_a_credential_error, que pedem openai_codex e rodam no job do codex. No job do codex, os pulos são os mesmos, arquivo a arquivo.

  • Cobertura de ramos combinada dos quatro arquivos (unit e codex-provider, 3.11 e 3.13), com o fail_under = 100 do pyproject.toml: 3094 linhas, 1212 ramos, 0 faltando, 100%.
  • Job quality com --extra dev --extra all --extra search-all --extra polars --extra excel --extra codex em 3.13: ruff format --check src tests, ruff check src tests, ty check, vulture e deptry src passam.
  • mkdocs build com --extra docs passa; os avisos são o aviso da equipe do Material sobre o MkDocs 2.0 e os dois do mkdocs_static_i18n sobre o seletor de idioma.

SHA revisado: fbd05cd

bdcdo added 3 commits October 14, 2026 16:10
O app-server 0.160.1 recusa com o código JSON-RPC -32001, mensagem "Server
overloaded; retry later." e data nulo, o pedido que chega com a fila de
entrada cheia. O is_retryable_error do SDK procura a sobrecarga só no data,
e esse erro saía ProviderError definitivo: a linha falhava na primeira
tentativa e os workers não caíam. _raise_classified_sdk_error passa a
levantar ProviderOverloadedError pelo código, sem ler a mensagem.

Sai a leitura de códigos de rate limit no data do erro JSON-RPC, que nenhum
erro do app-server envia, com os helpers que só ela usava; _raise_turn_error
volta ao que era antes dela.

Os testes da falha HTTP sem status ou 5xx e dos códigos transitórios do
turno conferem que o erro não conta como rate limit. A documentação diz
que os workers caem quando a linha falha depois de todas as tentativas,
que o codex reconhece o rate limit pelo código ou pelo status do erro e
que os providers do LangChain também leem o texto da mensagem quando o
erro não traz status HTTP.
@bdcdo

bdcdo commented Oct 14, 2026

Copy link
Copy Markdown
Owner Author

Evidência: erro -32001 do app-server do codex como sobrecarga, sem ler o data

Dois commits sobre fbd05cd9:

O que a revisão achou

A revisão leu o SDK openai-codex==0.160.1 e o app-server da tag rust-v0.160.1.

  1. O formato tratado em f0c3f1f9 não existe. Nenhum erro JSON-RPC do app-server põe código de rate limit ou de sobrecarga no data. O único data com codexErrorInfo é o do turn/steer, que o dataframeit não chama. A leitura do data, com três chaves, duas grafias, dicts aninhados e listas, tratava um caso inventado.
  2. O erro de sobrecarga real não era tratado. Quando a fila de entrada está cheia, o app-server responde ao pedido com o erro JSON-RPC de código -32001, mensagem "Server overloaded; retry later." e data nulo (codex-rs/app-server-transport/src/transport/mod.rs, linhas 245 a 251). O map_jsonrpc_error do SDK entrega esse erro como CodexRpcError com code == -32001, e o is_retryable_error procura a sobrecarga só no data; o erro saía ProviderError definitivo, na main e em todos os commits do PR.
  3. A documentação dizia mais do que o código faz. Os workers não caem a cada erro de rate limit, só quando a linha falha depois de todas as tentativas; e o claude_code não reconhece o rate limit só pelo status: quando o SDK levanta ResultError, a classificação lê o texto da mensagem (ver adiante).

O que mudou

  • _raise_classified_sdk_error levanta ProviderOverloadedError para o JsonRpcError de código -32001, pelo código, sem ler a mensagem. Um comentário junto da constante diz de onde ele vem.
  • Sai a leitura do data e os helpers que só ela usava (_is_overload_info, _http_variant_status, _parse_error_info, _rpc_error_is_overload); _raise_turn_error volta ao que era em 8ebaee8e.
  • Testes: o -32001 com data nulo, montado pelo map_jsonrpc_error do SDK, vira ProviderOverloadedError; os códigos -32000 e -32002 com a mesma mensagem continuam definitivos; uma exceção que não é JSON-RPC com atributo code = -32001 continua definitiva; e, de ponta a ponta, dataframeit() com parallel_requests=4 repete cada linha e reduz os workers de 4 para 2 e de 2 para 1. Os testes da falha HTTP sem status ou 5xx e dos códigos transitórios do turno (internal_server_error, agora também thread_rollback_failed) conferem is_rate_limit_error(...) is False. Saem os 15 casos do formato inventado.
  • Documentação: o FAQ e o guia de performance, nas duas línguas, e a docstring de dataframeit() dizem que os workers caem pela metade quando uma linha falha depois de todas as tentativas com erro de rate limit ou de sobrecarga; que o codex reconhece o rate limit pelo código ou pelo status do erro, sem ler a mensagem; e que os providers do LangChain também leem o texto da mensagem quando o erro não traz status HTTP.
  • CHANGELOG.md e o comentário de is_rate_limit_error, em errors.py, descrevem o comportamento final.

git diff --stat do ajuste, antes (f0c3f1f9 e fbd05cd9 sobre 01294b84) e depois (contra 8ebaee8e, nos arquivos do ajuste; em core.py e no CHANGELOG.md, só as linhas do ajuste, porque a main também mexeu neles):

Arquivo Antes Depois
src/dataframeit/codex.py +97 −29 +12 −2
src/dataframeit/errors.py +3 −2 +4 −3
src/dataframeit/core.py +2 −1 +4 −1
tests/test_codex.py +71 −0 +110 −19
docs (FAQ e performance, pt e en) +4 −4 +8 −4
CHANGELOG.md +1 −1 +1 −1

O -32001 de ponta a ponta

dataframeit() com 6 linhas, parallel_requests=4, max_retries=2, provider="codex" e o backend do codex com SDK falso, montado pelos helpers de tests/test_codex.py. O thread_start levanta, em toda chamada, o erro que o SDK monta para a resposta do app-server:

map_jsonrpc_error(-32001, "Server overloaded; retry later.", None)

Cada árvore rodou com PYTHONPATH no próprio src, conferido por dataframeit.__file__, no mesmo venv (Python 3.13, --extra dev --extra codex e os demais extras). Nenhuma chamada de rede.

Árvore Reduções de workers Avisos de nova tentativa Chamadas a thread_start _error_details
main (8dc97892) 0 0 6 [Erro não-recuperável] ProviderError: CodexRpcError: JSON-RPC error -32001: Server overloaded; retry later.
8ebaee8e 0 0 6 igual à main
fbd05cd9 0 0 6 igual à main
92cbe791 2 6 12 [Falhou após 2 tentativa(s)] ProviderOverloadedError: CodexRpcError: JSON-RPC error -32001: Server overloaded; retry later.

Os mesmos casos de antes, no mesmo script (reduções / avisos de nova tentativa):

Caso main fbd05cd9 92cbe791
CodexRpcError(-32000, "429 Too Many Requests", None), '429' só no texto 2 / 0 0 / 0 0 / 0
CodexRpcError(-32000, "429 Too Many Requests", {"codex_error_info": "rate_limit_exceeded"}), o formato inventado 2 / 0 2 / 6 0 / 0

Tabela de classificação

594 entradas por árvore: os TurnError de todos os códigos do CodexErrorInfoValue e das quatro variantes HTTP com sete status, com mensagem neutra e com "429 Too Many Requests"; os erros JSON-RPC montados pelo map_jsonrpc_error com os códigos -32000, -32001, -32603 e -32600, quatro mensagens e 31 formas de data; e seis exceções avulsas. Cada entrada traz a classe levantada, is_rate_limit_error e is_recoverable_error.

  • 8ebaee8e contra 92cbe791: nenhum dos 92 turnos muda, nem as seis exceções avulsas. Mudam 84 entradas, todas de código -32001, de ProviderError definitivo para ProviderOverloadedError com is_rate_limit_error verdadeiro. As 124 entradas de -32001 saem ProviderOverloadedError; as outras 40 já saíam, porque o SDK as entrega como ServerBusyError.
  • fbd05cd9 contra 92cbe791: além das entradas de -32001, 122 voltam de ProviderOverloadedError para ProviderError definitivo; são as formas do data que só o formato inventado reconhecia.

Documentação conferida rodando

Mesmos scripts, árvore 92cbe791, com o modelo ou o SDK falso:

Frase Caso Reduções / tentativas
Os workers caem só quando a linha falha depois de todas as tentativas codex, -32001 na primeira tentativa de cada linha e sucesso na segunda 0 / 6, as 6 linhas processed
idem codex, -32001 em toda tentativa 2 / 6
O codex não lê a mensagem codex, -32000 com "429 Too Many Requests" e data nulo 0 / 0
O LangChain lê o texto quando o erro não traz status RuntimeError("429 Too Many Requests") 2 / 6
idem, e o status decide quando existe erro com status_code=500 e "429 Too Many Requests" no texto 0 / 6
idem erro com status_code=429 e texto "falhou" 2 / 6

O claude_code ficou fora da frase "pelo código ou pelo status". O ResultMessage com is_error é classificado pelo status, mas o ResultError que o SDK (claude-agent-sdk 0.2.165) levanta de dentro do query() não é ProviderError, e is_rate_limit_error lê o texto dele: com query() levantando ResultError de api_error_status=500 e "art. 429 do CPC" no texto, os workers caem (2 / 6); com api_error_status=529 e "API Error: Overloaded", não caem (0 / 6). É o caso da #227, fora deste PR.

Mutações

Cada mutante numa cópia limpa da árvore, com PYTHONDONTWRITEBYTECODE=1, -p no:cacheprovider, -o pythonpath no src da cópia e o __file__ de dataframeit.codex conferido. Rodam tests/test_codex.py e tests/test_errors.py; a base sem mutação passa com 239 testes.

Mutante em codex.py Testes que falham
sem o ramo do -32001 (o comportamento de 8ebaee8e) 2 (test_fila_cheia_do_app_server_e_sobrecarga, test_fila_cheia_reduz_workers_e_repete_a_linha)
sem a guarda isinstance(error, JsonRpcError) 1 (test_codigo_fora_de_erro_json_rpc_nao_e_lido)
constante -32000 no lugar de -32001 3
error.code <= -32001, faixa no lugar do código 1 (o -32002)
decide pela mensagem, "overloaded" no texto 2 (o -32000 e o -32002)
HTTP sem status vira sobrecarga 1; com os testes de 8ebaee8e, 0
HTTP 5xx vira sobrecarga 1; com os testes de 8ebaee8e, 0
códigos transitórios do turno viram sobrecarga 2; com os testes de 8ebaee8e, 0

Nenhum mutante sobrevive. Os três últimos sobreviviam à suíte anterior, porque os testes da falha HTTP e do código transitório só conferiam a nova tentativa, que a sobrecarga também faz.

Testes executados e pulados, main ao lado do branch, no mesmo ambiente

A suíte da main rodou da árvore de 8dc97892, com PYTHONPATH no src dela, conferido por dataframeit.__file__, no mesmo venv da suíte do branch. Nas versões mínimas, cada árvore foi instalada em modo editável no venv, como o job faz.

Job main (8dc97892) branch (92cbe791)
unit, Python 3.13 1233 passaram, 105 pulados 1244 passaram, 112 pulados
unit, Python 3.11 1233 passaram, 105 pulados 1244 passaram, 112 pulados
codex-provider, Python 3.13 1175 passaram, 184 pulados 1193 passaram, 184 pulados
codex-provider, Python 3.11 1175 passaram, 184 pulados 1193 passaram, 184 pulados
lowest-direct, all excel polars search-all 1232 passaram, 106 pulados 1243 passaram, 113 pulados

Nenhuma falha. O branch tem 18 testes a mais (7 em tests/test_codex.py, 8 em tests/test_errors.py e 3 em tests/test_claude_code.py). Os 7 pulos a mais no unit e no lowest-direct são os de tests/test_codex.py, que pedem openai_codex e rodam no job do codex, onde os pulos são os mesmos nas duas árvores.

  • Cobertura de ramos combinada dos quatro arquivos (unit e codex-provider, 3.11 e 3.13), com o fail_under = 100 do pyproject.toml: 3075 linhas, 1204 ramos, 0 faltando, 100%.
  • Job quality com --extra dev --extra all --extra search-all --extra polars --extra excel --extra codex em 3.13: ruff format --check src tests, ruff check src tests, ty check, vulture e deptry src passam.
  • mkdocs build com --extra docs passa; os avisos são o da equipe do Material sobre o MkDocs 2.0 e os dois do mkdocs_static_i18n sobre o seletor de idioma.

SHA revisado: 92cbe79

@bdcdo

bdcdo commented Oct 14, 2026

Copy link
Copy Markdown
Owner Author

Revisão independente da rodada final (1580f65b): um achado de texto, corrigido

Um subagente novo revisou o ajuste do -32001. O código está certo: no SDK instalado, o -32001 chega como CodexRpcError com o código em .code, e o is_retryable_error não o cobre; no fonte do app-server na tag rust-v0.160.1, todas as origens do -32001 são de capacidade (fila de entrada cheia, limite de assinaturas MCP, uploads de feedback em curso) e mandam data nulo. Ponta a ponta, o -32001 em toda tentativa reduz os workers duas vezes e repete a linha; só na primeira tentativa, não reduz; -32000 e -32002 continuam definitivos. Das 15 mutações, as 12 não equivalentes caem.

O achado: a frase nova "com erro de rate limit ou de sobrecarga" valia só para o codex. Nos providers do LangChain e no ResultError do claude_code, sobrecarga (529, 503) não reduz os workers (medido: 0 reduções nos três casos). Em 2fa6d671, a redução fica descrita como "erro de rate limit", a sobrecarga aparece só no codex, e o LangChain fica descrito pelo status 429, pela classe do erro e, sem nenhum dos dois, pelo texto, no guia de performance, no FAQ (pt e en) e na docstring de dataframeit(). Só texto; o código não muda.

Fora do ajuste e já registrado na #227: o ResultError que o SDK do Claude Code levanta de dentro do query() ainda é classificado pelo texto.

SHA revisado: 2fa6d67

# Conflicts:
#	CHANGELOG.md
#	tests/test_codex.py
@bdcdo
bdcdo merged commit 92f8086 into main Oct 14, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Número como "art. 401" na resposta do modelo é classificado como erro de credencial

1 participant