# Changelog of AI for Every Row: Bulk LLM to Classify & Summarize Datasets (`humble-echidna/ai-rows`) Actor

- **URL**: https://apify.com/humble-echidna/ai-rows/changelog.md
- **Full Actor documentation**: https://apify.com/humble-echidna/ai-rows.md

## Changelog

Versions follow MAJOR.MINOR.PATCH (`src/version.py`); Apify shows MAJOR.MINOR from `.actor/actor.json`.
Every run logs its version and records it in the `RUN_STATS` key-value record.

### 0.1.3 (2026-10-16)

- A run that returns nothing (every input bad, private, blocked or unreachable, or an empty input) now ends
  SUCCEEDED with an empty dataset and the reason as its status message and in the log, instead of FAILED. Only an
  unexpected error still fails the run. `RUN_STATS` keeps the reason as before.
- No change to output fields or price.

### 0.1.2 (unreleased)

- **Fixed: one flagged row no longer stops the run.** When the model provider's moderation flags a row's text,
  OpenRouter answers with HTTP 403, the same status as a refused token, and the run stopped there and failed. That
  row now comes back with the reason, uncharged, and the other rows carry on. A refused token or account still stops
  the run at once.
- A model that declines to answer now says so, with its reason, instead of "the model's answer isn't JSON: (empty)".

### 0.1.1 (2026-10-03)

- A run no longer fails when the store it remembers earlier runs in can't be opened (403: the account already has
  a store of that name that this actor didn't create). It falls back to `humble-echidna-<name>`, and if that is
  refused too it runs without memory for that run. Users' existing memory is untouched: the usual name is tried first.

### 0.1.0 (unreleased)

First release: an LLM on every row of a dataset or an inline JSON list, one call per row.

- Rows from `rows` (inline JSON; a plain string is `{"text": ...}`) or any Apify dataset (`datasetId`, read with the
  run's own token, only the top-level fields it needs; up to 20,000 rows per run).
- Only `textFields` are sent to the model, each as "field: value"; `keepFields` are copied to the output (`kept`)
  for joins, with `sourceIndex`.
- What comes back: one label from `labels` (`data.label`, a strict enum), plain-English `fields`, or a JSON `schema`
  (precedence schema > fields > labels). Every answer validated; invalid, empty and failed rows are rows with the
  reason, never charged.
- Model through Apify's OpenRouter proxy with the run's own token (the shared `mms_llm` client, also used by
  ai-extract 1.0.5): retries on 429/5xx, strict mode retried as a hint when a provider refuses it, a refusing proxy
  stops the run.
- `onlyNew`: skips rows whose text the same setup (schema, instructions, model, text fields) already answered
  validly; text hashes in the user's own `ai-rows-seen` store, remembered only after the row is in the dataset.
- Charged per valid row (`row-processed`), after the push; the max cost per run is honoured before a row is sent.
  `maxRows` and `maxTokensPerRow` cap the token spend. A run near its timeout stops starting rows, saves the
  memory and RUN_STATS, and ends cleanly.
- No source is fetched (no robots.txt or terms gate applies): the only requests are to Apify's own API (the user's
  dataset) and to Apify's OpenRouter proxy, both on the user's token.
