Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c6e075d
Resposta recusada com número no texto não vira erro de credencial
bdcdo Oct 3, 2026
c197578
Rate limit e mensagens decididos pela classe na resposta recusada
bdcdo Oct 3, 2026
3481d77
Integra a main no branch da resposta recusada
bdcdo Oct 13, 2026
97e43f8
CHANGELOG com a versão atual do openai-codex
bdcdo Oct 13, 2026
80ccf2b
is_rate_limit_error decide pela classe só na resposta recusada
bdcdo Oct 13, 2026
dfa197f
Comentários e testes da mensagem da resposta recusada
bdcdo Oct 13, 2026
7ab9a6e
Exemplos da resposta recusada que valem para cada provider e testes d…
bdcdo Oct 13, 2026
8c09e3d
Integra a main no branch da resposta recusada
bdcdo Oct 13, 2026
b01df0b
Integra a main (Python mínimo 3.11) no branch da resposta recusada
bdcdo Oct 14, 2026
8ebaee8
is_rate_limit_error decide pela classe para todo ProviderError
bdcdo Oct 14, 2026
01294b8
Integra a main no branch da resposta recusada
bdcdo Oct 14, 2026
f0c3f1f
Código de rate limit no erro JSON-RPC do codex levanta ProviderOverlo…
bdcdo Oct 14, 2026
fbd05cd
Documentação diz quando os workers caem por rate limit
bdcdo Oct 14, 2026
c6c364a
Integra a main no branch da resposta recusada
bdcdo Oct 14, 2026
92cbe79
Erro -32001 do app-server do codex levanta ProviderOverloadedError
bdcdo Oct 14, 2026
1580f65
Integra a main (#237) no branch da resposta recusada
bdcdo Oct 14, 2026
2fa6d67
Documentação da redução de workers diz que a sobrecarga só conta no c…
bdcdo Oct 14, 2026
10d082a
Integra a main (#234) no branch da resposta recusada
bdcdo Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
- `dataframeit()` declara o tipo de retorno pelo tipo da entrada: DataFrame ou Series do polars devolve DataFrame do polars, e DataFrame ou Series do pandas, `list` e `dict` devolvem DataFrame do pandas. Antes, o retorno era a união dos dois, e o checker de tipos recusava `resultado["coluna"].tolist()` (#155).
- O piso do pydantic sobe para 2.12. Na 2.11, uma `default_factory` que lê outro campo é chamada com os dados validados vazios quando esse campo falhou, e a retomada levantava o `KeyError` dela em vez do erro de campo incompatível (#154).
- Os extras `polars` e `all` passam a exigir `polars>=1.0`. No 0.20, há versões que convertem a coluna de texto com um valor de outro tipo e põem nulo no lugar dele, sem erro, e versões em que o `to_pandas()` de uma coluna `Object`, pelo qual ela volta na retomada, devolve bytes no lugar dos objetos ou falha (#218).
- O extra `codex` passa a exigir `openai-codex==0.159.2`, que traz o runtime 0.159.2. Com o runtime 0.137, que o SDK anterior fixava, a conta do ChatGPT recebia HTTP 400 nos modelos lançados depois dele, como o `gpt-6-luna`, embora o catálogo da conta os anunciasse. O `effort` é conferido contra os valores que o SDK declara, agora com `max` e `ultra`, porque o enum do SDK novo aceita qualquer texto.
- O extra `codex` passa a exigir `openai-codex==0.160.1`, que traz o runtime 0.160.1. Com o runtime 0.137, que o SDK anterior fixava, a conta do ChatGPT recebia HTTP 400 nos modelos lançados depois dele, como o `gpt-6-luna`, embora o catálogo da conta os anunciasse. O `effort` é conferido contra os valores que o SDK declara, agora com `max` e `ultra`, porque o enum do SDK novo aceita qualquer texto.
- No provider `codex`, os códigos de erro `rateLimitExceeded` e `flexUnavailable` do runtime levantam `ProviderOverloadedError`, e a linha é repetida como no HTTP 429.
- No provider `codex`, as flags `sleep_tool` e `view_image` do runtime ficam desligadas, e as ferramentas de pausa e de leitura de imagem local deixam de ser oferecidas ao modelo.
- No provider `codex`, a flag `unbounded_connection_retries` do runtime fica desligada. Ligada, uma falha de conexão deixa o turno esperando para sempre; desligada, a linha falha com `ProviderTransientError` e é repetida conforme `max_retries`.
Expand All @@ -39,6 +39,7 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR

### Corrigido

- Uma resposta recusada pela validação do modelo Pydantic deixa de ser lida como erro de credencial quando o texto tem números como 401 ou 403, comuns em texto jurídico ("art. 401 da CLT", "Súmula 403"). No `claude_code`, a falha de parsing ou de validação levanta `ProviderRejectedOutputError` e ganha nova tentativa; antes, a linha falhava na primeira tentativa com a instrução de reconfigurar a autenticação. No `codex`, a mensagem amigável do `ProviderOutputError` é decidida pela classe, e não manda mais rodar `codex login`. Nos dois, a mensagem gravada em `_error_details` leva o caminho e a regra de cada erro, sem o valor recusado. `is_rate_limit_error` decide pela classe para todo `ProviderError` e para o `ProviderOutputError`, e entre eles só o `ProviderOverloadedError` conta como rate limit: um '429' no texto, como no resumo de um erro de validação do `codex` ou no campo `result` que o SDK do Claude Code devolve num erro, deixa de reduzir os workers. No `codex`, o erro JSON-RPC de código -32001, com que o app-server recusa o pedido quando a fila de entrada está cheia, levanta `ProviderOverloadedError`: a linha é repetida e, se falhar em todas as tentativas, reduz os workers, como no HTTP 429. Antes, esse erro falhava a linha na primeira tentativa, sem reduzir os workers. O erro do `codex`, de turno ou JSON-RPC, que traz '429 Too Many Requests' só no texto deixa de reduzir os workers, porque o rate limit é reconhecido pelo código do erro (#192).
- No provider `codex`, a saída do backend e o encerramento pelo prazo fecham também os pipes de stdout e stderr do app-server, que o `close` do SDK deixa abertos; antes, os dois ficavam para o coletor de lixo, com um `ResourceWarning: unclosed file` cada. Quando o app-server morre no meio de uma linha, `dataframeit()` deixa de levantar `BrokenPipeError` ao encerrá-lo e devolve as linhas já processadas, com a linha abortada e as seguintes sem status (#225).
- Com entrada polars e `resume=True`, a coluna de um campo `int` do modelo é lida com os inteiros e os nulos do polars. Antes, o nulo de uma linha pendente levava a coluna a float: o inteiro acima de 2**53 voltava arredondado, a coluna saía `Float64` e as respostas novas saíam em float. Uma coluna `UInt64` com valor acima do `Int64` deixa de ser acusada como incompatível e sai `Object`, como na execução sem interrupção. A coluna `Int64` sem nulo do campo `int` também chega em inteiros Python, e um modelo com `strict=True` deixa de acusar como incompatível a linha processada, que o `int64` do numpy não validava (#222).
- Com entrada polars, o campo cuja anotação junta `date`, `datetime` ou `time` a outro tipo, na união, na tupla ou na lista ou tupla de união, sai `Object`, com os valores que a entrada pandas devolveria e o aviso das colunas que o polars recusa. Antes, a coluna era tipada pelo primeiro valor, sem erro nem aviso: em `Union[datetime, date]` com a data primeiro, a hora sumia; em `Union[date, int]` e `tuple[date, int]`, o inteiro 5 virava `1970-01-06`; em `Union[time, int]`, o inteiro fora do intervalo de um dia virava nulo ou meia-noite, conforme a versão do polars; em `list[Union[date, datetime]]`, a hora sumia (#228).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Field and class names are arbitrary; the examples in the [`example/`](https://gi

- **Multiple providers**: OpenAI, Google Gemini, Anthropic and Groq with their own extras, any other LangChain provider (Cohere, Mistral, Vertex AI, Bedrock, Azure) by installing its package, plus Claude Code and Codex through their official SDKs
- **Multiple input types**: pandas or Polars DataFrame and Series, list, dict
- **Structured output**: Pydantic validation; a rejected answer goes back to the model with the error
- **Structured output**: Pydantic validation; with the LangChain providers, a rejected answer goes back to the model with the error
- **Resilience**: Automatic retry with exponential backoff, and periodic checkpoints (`batch_size` + `checkpoint_path`) to resume long runs
- **Performance**: Parallel processing that halves workers on rate limits, configurable rate limiting
- **Web search**: Tavily or Exa, per field or per group of fields, with conditional fields (`condition`, `depends_on`)
Expand Down
2 changes: 1 addition & 1 deletion README.pt-BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Os nomes de classes e campos são arbitrários: os notebooks de [`example/`](exa

- **Múltiplos providers**: OpenAI, Google Gemini, Anthropic e Groq com extras próprios, qualquer outro provider do LangChain (Cohere, Mistral, Vertex AI, Bedrock, Azure) instalando o pacote dele, além de Claude Code e Codex pelos SDKs oficiais
- **Múltiplos tipos de entrada**: DataFrame e Series do pandas ou do Polars, list, dict
- **Saída estruturada**: Validação com Pydantic; uma resposta recusada volta ao modelo com o erro
- **Saída estruturada**: Validação com Pydantic; nos providers do LangChain, uma resposta recusada volta ao modelo com o erro
- **Resiliência**: Retry automático com backoff exponencial e checkpoints periódicos (`batch_size` + `checkpoint_path`) para retomar execuções longas
- **Performance**: Processamento paralelo que reduz os workers pela metade em rate limit, rate limiting configurável
- **Busca web**: Tavily ou Exa, por campo ou por grupo de campos, com campos condicionais (`condition`, `depends_on`)
Expand Down
4 changes: 3 additions & 1 deletion docs/en/guides/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,15 @@ The wait before attempt `n + 1` is `min(base_delay × 2^(n-1), max_delay)`, plus
- **Connection error**: Network issues
- **5xx errors**: Server problems

### Response rejected by validation (retry with the error)
### Response rejected by validation

- **Validation error**: the response fails the Pydantic model, including custom validators (`model_validator`, `field_validator`)
- **Parsing error**: the response is not valid JSON or did not come in the structured format

With LangChain providers, the next attempt sends the model the rejected response and the list of errors, each with the field path and the rejected value, asking it to answer again fixing those points. Repeating the same prompt tends to repeat the same error. When the response is not JSON, the request carries the parser's error text.

With `claude_code`, the next attempt repeats the request, without the list of errors. With `codex`, a response that fails validation raises `ProviderOutputError`, and the row fails without a new attempt. In both, a validation failure writes to `_error_details` the path and the rule of each error, without the rejected value; path and rule still carry response text when Pydantic puts it there, such as a custom validator's message and, with `claude_code`, a dict key. A `claude_code` parsing failure only records that the response holds no valid JSON.

With OpenAI, the SDK validates the response inside the call and raises before returning the message; the rejected response and its tokens are read from the HTTP response attached to the error. When no raw response is available, the correction request goes with the prompt, carrying the rejected values. When a later attempt succeeds, tokens from the rejected ones are added to `_input_tokens` and `_output_tokens`, because they are billed too; if every attempt fails, the row gets status `error`, no token count, and `_error_details` names the field and the rule of each error.

### Permanent Errors (no retry)
Expand Down
2 changes: 1 addition & 1 deletion docs/en/guides/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ With `resume=False`, if the model columns are already in the DataFrame and `repr

### Many 429 errors (rate limit)

The provider rejected requests for exceeding its rate. dataframeit already halves the workers on each 429 and retries with backoff, but the stable fix is to lower `parallel_requests` or raise `rate_limit_delay`. The calculation is in [Performance](performance.md#calculating-ideal-delay). With web search, the search provider's limit is usually the tightest; see [Web Search](web-search.md#rate-limits-and-parallel-processing).
The provider rejected requests for exceeding its rate. dataframeit retries with backoff and, when the row fails after all attempts, halves the workers. With `codex`, the rate limit and the app-server overload are recognized by the error code or status, without reading the message; with the LangChain providers, the rate limit is recognized by the 429 status or the error class and, when the error carries neither, by the message text. The stable fix is to lower `parallel_requests` or raise `rate_limit_delay`. The calculation is in [Performance](performance.md#calculating-ideal-delay). With web search, the search provider's limit is usually the tightest; see [Web Search](web-search.md#rate-limits-and-parallel-processing).

### How much will it cost?

Expand Down
4 changes: 3 additions & 1 deletion docs/en/guides/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ result = dataframeit(

### Auto-reduction on Rate Limits

When a 429 error is detected, DataFrameIt automatically reduces workers:
When a row fails after all attempts with a rate limit error, DataFrameIt halves the workers:

```
Start: 10 workers
Expand All @@ -34,6 +34,8 @@ Rate limit detected → 2 workers
Rate limit detected → 1 worker
```

With `codex`, the rate limit and the app-server overload are recognized by the error code or status, without reading the message. With the LangChain providers, the rate limit is recognized by the 429 status or the error class and, when the error carries neither, by the message text; overload (529, 503) does not reduce the workers.

!!! info "Safety"
Workers are only **reduced**, never automatically increased. This prevents unexpected costs.

Expand Down
4 changes: 2 additions & 2 deletions docs/en/reference/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ ValueError
| `ProviderError` | No | Definitive provider error, such as rejected authentication or exhausted `claude_code` budget |
| `ProviderTransientError` | Yes | Network or service failure that usually goes away on its own |
| `ProviderOverloadedError` | Yes | Provider overload or HTTP 429 |
| `ProviderRejectedOutputError` | Yes | The response failed validation against the Pydantic model; the new attempt sends the model the rejected response and the error |
| `ProviderOutputError` | No | The provider finished without a usable response, such as a `codex` turn with no content |
| `ProviderRejectedOutputError` | Yes | The response failed validation against the Pydantic model. With LangChain providers, the new attempt sends the model the rejected response and the error; with `claude_code`, it repeats the request |
| `ProviderOutputError` | No | The provider finished without a usable response, such as a `codex` turn with no content or with a response that fails validation against the Pydantic model |

Errors from providers via LangChain do not use these classes. They are classified by the type LangChain itself declares, by the HTTP status and by the message. The details are in [Error Handling](../guides/error-handling.md).
4 changes: 3 additions & 1 deletion docs/guides/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,15 @@ A espera antes da tentativa `n + 1` é `min(base_delay × 2^(n-1), max_delay)`,
- **Erro de conexão**: Problemas de rede
- **Erro 5xx**: Problemas no servidor

### Resposta recusada pela validação (retry com o erro)
### Resposta recusada pela validação

- **Erro de validação**: a resposta não passa no modelo Pydantic, inclusive nos validadores próprios (`model_validator`, `field_validator`)
- **Erro de parsing**: a resposta não é JSON válido ou não veio no formato estruturado

Nos providers do LangChain, a tentativa seguinte leva ao modelo a resposta recusada e a lista de erros, cada um com o caminho do campo e o valor recusado, e pede que ele responda de novo corrigindo esses pontos. Repetir o mesmo prompt tende a repetir o mesmo erro. Quando a resposta não é JSON, o pedido leva o texto do erro do parser.

No `claude_code`, a tentativa seguinte repete o pedido, sem a lista de erros. No `codex`, a resposta que não passa na validação levanta `ProviderOutputError`, e a linha falha sem nova tentativa. Nos dois, a falha de validação grava em `_error_details` o caminho e a regra de cada erro, sem o valor recusado; caminho e regra ainda trazem texto da resposta quando o Pydantic o põe ali, como a mensagem de um validador próprio e, no `claude_code`, a chave de um dict. A falha de parsing do `claude_code` grava só que a resposta não contém JSON válido.

Na OpenAI, o SDK valida a resposta dentro da chamada e levanta o erro antes de devolver a mensagem; a resposta recusada e os tokens são lidos da resposta HTTP anexada ao erro. Quando nenhuma resposta bruta está disponível, o pedido de correção vai junto do prompt, com os valores recusados. Quando uma tentativa seguinte dá certo, os tokens das recusadas entram em `_input_tokens` e `_output_tokens`, porque também são cobrados; se todas falham, a linha fica com status `error`, sem contagem de tokens, e `_error_details` diz o campo e a regra de cada erro.

### Erros Permanentes (sem retry)
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Com `resume=False`, se as colunas do modelo já estão no DataFrame e `reprocess

### Muitos erros 429 (rate limit)

O provider recusou requisições por excesso de taxa. O dataframeit já reduz os workers pela metade a cada 429 e tenta de novo com backoff, mas a solução estável é baixar `parallel_requests` ou aumentar `rate_limit_delay`. A conta está em [Performance](performance.md#calculando-o-delay-ideal). Com busca web, o limite do provedor de busca costuma ser o mais apertado; ver [Busca Web](web-search.md#rate-limits-e-processamento-paralelo).
O provider recusou requisições por excesso de taxa. O dataframeit tenta de novo com backoff e, quando a linha falha depois de todas as tentativas, reduz os workers pela metade. No `codex`, o rate limit e a sobrecarga do app-server são reconhecidos pelo código ou pelo status do erro, sem ler a mensagem; nos providers do LangChain, o rate limit é reconhecido pelo status 429 ou pela classe do erro e, quando o erro não traz nenhum dos dois, pelo texto da mensagem. A solução estável é baixar `parallel_requests` ou aumentar `rate_limit_delay`. A conta está em [Performance](performance.md#calculando-o-delay-ideal). Com busca web, o limite do provedor de busca costuma ser o mais apertado; ver [Busca Web](web-search.md#rate-limits-e-processamento-paralelo).

### Quanto vai custar?

Expand Down
4 changes: 3 additions & 1 deletion docs/guides/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ resultado = dataframeit(

### Auto-redução em Rate Limits

Quando detecta erro 429, o DataFrameIt reduz workers automaticamente:
Quando uma linha falha depois de todas as tentativas com erro de rate limit, o DataFrameIt reduz os workers pela metade:

```
Início: 10 workers
Expand All @@ -34,6 +34,8 @@ Rate limit detectado → 2 workers
Rate limit detectado → 1 worker
```

No `codex`, o rate limit e a sobrecarga do app-server são reconhecidos pelo código ou pelo status do erro, sem ler a mensagem. Nos providers do LangChain, o rate limit é reconhecido pelo status 429 ou pela classe do erro e, quando o erro não traz nenhum dos dois, pelo texto da mensagem; a sobrecarga (529, 503) não reduz os workers.

!!! info "Segurança"
Workers são apenas **reduzidos**, nunca aumentados automaticamente. Isso evita custos inesperados.

Expand Down
4 changes: 2 additions & 2 deletions docs/reference/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ ValueError
| `ProviderError` | Não | Erro definitivo do provider, como autenticação recusada ou orçamento do `claude_code` esgotado |
| `ProviderTransientError` | Sim | Falha de rede ou do serviço que costuma passar sozinha |
| `ProviderOverloadedError` | Sim | Sobrecarga ou HTTP 429 do provider |
| `ProviderRejectedOutputError` | Sim | A resposta não passou na validação do modelo Pydantic; a nova tentativa leva ao modelo a resposta recusada e o erro |
| `ProviderOutputError` | Não | O provider terminou sem resposta utilizável, como um turno do `codex` sem conteúdo |
| `ProviderRejectedOutputError` | Sim | A resposta não passou na validação do modelo Pydantic. Nos providers do LangChain, a nova tentativa leva ao modelo a resposta recusada e o erro; no `claude_code`, repete o pedido |
| `ProviderOutputError` | Não | O provider terminou sem resposta utilizável, como um turno do `codex` sem conteúdo ou com resposta que não passa na validação do modelo Pydantic |

Erros dos providers via LangChain não usam essas classes. Eles são classificados pelo tipo que a própria LangChain declara, pelo status HTTP e pela mensagem. Os detalhes estão em [Tratamento de Erros](../guides/error-handling.md).
21 changes: 18 additions & 3 deletions src/dataframeit/claude_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@
import json
from typing import TYPE_CHECKING, Any, TypeVar

from pydantic import ValidationError

from .errors import (
ProviderError,
ProviderOverloadedError,
ProviderRejectedOutputError,
ProviderTransientError,
retry_with_backoff,
validation_error_summary,
)
from .llm import LLMConfig, _parse_usage_metadata, build_prompt
from .utils import parse_json
Expand Down Expand Up @@ -234,9 +238,20 @@ def _call() -> dict:
msg = "Claude Code SDK retornou resposta vazia"
raise ValueError(msg)

# Parse e validação
parsed = parse_json(response_text)
validated = pydantic_model.model_validate(parsed)
# A recusa sai como ProviderRejectedOutputError, transitória pela classe.
# A mensagem não leva o valor recusado, mas na falha de validação o
# caminho e a regra trazem texto da resposta quando o Pydantic o põe
# ali (ver validation_error_summary).
try:
parsed = parse_json(response_text)
except ValueError as err:
msg = "Falha no parsing da resposta: o texto não contém um JSON válido"
raise ProviderRejectedOutputError(msg) from err
try:
validated = pydantic_model.model_validate(parsed)
except ValidationError as err:
msg = f"Falha na validação da resposta: {validation_error_summary(err)}"
raise ProviderRejectedOutputError(msg) from err

usage = _usage_from_sdk(getattr(result, "usage", None), spent)
return {"data": validated.model_dump(), "usage": usage}
Expand Down
Loading
Loading