# Changelog of AI Document & Web Analyzer — powered by Claude (`badr_xba/ai-document-web-analyzer-claude`) Actor

- **URL**: https://apify.com/badr_xba/ai-document-web-analyzer-claude/changelog.md
- **Full Actor documentation**: https://apify.com/badr_xba/ai-document-web-analyzer-claude.md

## Changelog

### 2026-11-10 (rejected API key: same behaviour in the Claude, OpenAI and Gemini Actors; local, not pushed)

- A rejected key (HTTP 401 / 403 (also from `count_tokens` and the Message Batches API)) is a configuration error of the run: it stops after the FIRST such error (no further AI call; until the key has answered once, items are sent one at a time so a bad key costs one request whatever `concurrency`), every item not analysed goes to `FAILED_INPUTS` with one shared code `invalidApiKey` (or `apiKeyNoAccess` for 403 / permission) and a fix hint (where to create/check the key, the "Anthropic API key" field, nothing billed), `RUN_SUMMARY` gets `stoppedReason` + `stoppedHint` + `apiKeyError`, no dataset row, and the run ends FAILED via `Actor.fail` with the status "Stopped: Anthropic rejected the API key — nothing was analysed or billed. ..." (schedules and integrations notice a broken key). Only Apify's run start event is charged.
- Before: run FAILED with "Anthropic rejected the API key (401). Check the key in "API key"", codes `providerFatal` + `notProcessed`.
- Unchanged: 429 and 5xx are retried and never treated as key errors; a missing key still gives the successful estimate-only demo run.
- Shared wording in `src/api_key_errors.py` (identical in the three Actors except the provider constants). New tests: 401, 403, stop after the first call (concurrency 1 and 5), 429 not a key error.

### 2026-11-10 (status messages; local, not pushed)

- Fix: the estimate-only status said "about 0 tokens used (estimates are in each row's estimate)". It now states the total estimated input, like the OpenAI/Gemini Actors: "Estimate only: N item(s) estimated, nothing analysed. Total estimated input: X tokens ...".
- Fix: task `compare` ended with "Done: 1 of 3 item(s) analysed" (one comparison row for all sources); now "Done: 3 source(s) compared in 1 comparison row ...". Sources were already compared in input order ("Source N").

### 2026-11-09 (empty-input demo + own-key fee by level; local, not pushed)

#### Fixed

- A run without any source (no startUrls/texts/documentUrls/imageUrls/mediaUrls/datasetIds), e.g. Apify's automatic Store test with `{}` or defaults only, no longer ends with "Nothing to analyse": it uses the demo sources = the `prefill` of those fields in `.actor/input_schema.json` (read at runtime; `src/demo.py` keeps a fallback checked against the schema by a test), logs "No source given: running the demo input" and continues. Without a key it is an estimate-only run: SUCCEEDED, 1 row, nothing billed. Runs with explicit sources are unchanged.

#### Changed (owner decision 2026-11-09)

- Own-key fee scales with the analysis level: one event `item-analyzed` ($0.0015) charged with count 1 (quick), 2 (standard), 4 (deep) per analysed item = $0.0015 / $0.003 / $0.006. Never charged for errors, refusals, estimates or skipped items. README launch text ("free ... no fee of its own") replaced; PRICING.md updated.

### 2026-11-09 (0.3, commercial safety: local, not pushed; no price change)

#### Fixed (from FINANCE_AUDIT.md part C and TESTS_AUDIT.md)

- **Managed mode refuses before any AI call** unless the run is pay-per-event and the live pricing info of the run (apify 3.x `Actor.get_charging_manager().get_pricing_info()`: `pricing_model`, `per_event_prices`) has the input / cached-input / output events of the chosen model (a 0-priced event counts as missing). Charging decisions no longer fall back to the proposed prices of models.json (`billing.plan_managed_pricing` / `ManagedPricing`).
- Refusal-fallback models are billed on their own events, else on the most expensive configured tier **only if** its live prices cover the fallback's list price; otherwise managed mode is refused up front. Finding: with the proposed prices, Fable's cache-read event ($0.41/M) does not cover Opus 5 / Opus 4.8 cache reads ($0.50/M), so those events must be configured for Opus 5.5 and Fable 5.1.
- New per-model `managedEnabled` flag in models.json (true on every current model; missing = disabled): switches managed mode off for one model with a build, not a price change.
- **Timeouts**: every managed request is streamed and its usage accumulated from the stream events (`message_start`, `content_block_delta`, `message_delta`), so a request broken late (timeout, network, `itemTimeoutSecs` cancellation) is still charged on the reported tokens (output at least a neutral estimate of the text received). Managed mode never retries a timeout or broken attempt whose usage was not charged; a retry after a charged partial attempt re-reserves the budget first. `itemTimeoutSecs` defaults to 2 x output limit / 30 tok/s + 60 s (managed: a floor; own key: warning if set lower; the schema default 600 was removed). An item cut by the timeout after billing gives an error row with its tokens and `billedEvents`.
- **Token estimates**: managed mode always counts the input with `messages.count_tokens` (free per the claude-api skill) before each call, including every map-reduce part (a part counted over the cap is split again) and the final reduce/compare request, so Haiku 5.5 stays strictly under its 100K-prompt price step on counted tokens. The heuristic now applies a non-Latin safety multiplier (1.5 tokens per non-Latin character, 3 per emoji / non-BMP) - `src/tokens.py`.
- **Charge limit**: when `Actor.charge` reports `event_charge_limit_reached` (or charges fewer events than asked), the run stops starting items (`stopReason: maxTotalChargeUsd`), both key modes.
- **Redaction**: every log record goes through a redacting filter (message and `exc_info` traceback); an uncaught error is logged as a redacted traceback and re-raised as `CrashError` with a redacted message and no chained cause; `__main__` installs a redacting `sys.excepthook`. Unknown input fields whose value looks like a key (`sk-ant-`, `sk-`, `AIza`) trigger a warning to rotate the key; the value is registered for redaction and never echoed. The Actor warns when Apify's `apify-default-dataset-item` event is configured.

#### Tests

- 188 offline tests (was 159). New: compare and Message Batches through `main()` (rows, errored and skipped items, batch fatal), item timeout (managed: streamed usage charged, no retry; own key: FAILED_INPUTS), crash path (FAILED report, redacted traceback, CrashError), charge limit reached mid-run (fake Actor reports it), missing-event / not-PPE / no-manager refusal, `managedEnabled: false` refusal, log-filter traceback redaction, key pasted in an unknown field, streamed partial usage and retry rules, Haiku under 100K on counted CJK tokens (with re-splitting), non-Latin multiplier, timeout sizing. The Node card-renderer test now skips cleanly when `node` is absent or does not run.

### 2026-11-09 (0.2.1, model audit: local, not pushed)

#### Fixed

- `claude-sonnet-5-5` cache-read price was $0.20/MTok; the official pricing page (platform.claude.com/docs/en/about-claude/pricing, read 2026-11-09) lists $0.10/MTok (0.05x input, footnote 2, same rule as Opus 5.5). `src/models.json` and `PRICING.md` corrected. The event price proposal `cached-input-tokens-1k-sonnet-5-5` ($0.000325) was left unchanged: it still covers the cost (now 2x margin) and is an owner pricing decision.
- `claude-haiku-5-5` price note: cache-read ($0.01 / $0.05) and cache-write prices are published on the official page, not derived; values unchanged.
- All 7 model ids, statuses (all Active), effort low/medium/high, structured outputs, PDF, vision, batch and server-side fallback support re-checked against platform.claude.com/docs: no other change.

### 2026-11-09 (0.2, local build: not pushed)

#### Security

- DNS rebinding closed: each hop's host (first URL, robots.txt, every redirect) is resolved once, every answer must be public, and the connection is pinned to that exact IP (`PinnedIPTransport`: URL rewritten to the IP, original host kept in the `Host` header and as the httpx/httpcore `sni_hostname` extension, so TLS SNI and certificate hostname verification still use the real name; TLS verification unchanged). Keep-alive off so a TLS connection is never reused across host names; proxies from the environment ignored (`trust_env=False`). Same `privateAddress` code, redirect and size caps. Tests run the real httpx/httpcore stack on a fake network backend.

### 2026-11-09 (0.1, local build: not pushed)

#### Added

- First version. Sources: web pages (honest plain GET, robots.txt, no anti-bot bypass), texts, documents (PDF native document blocks, DOCX, TXT, MD, CSV), images (vision), and Apify datasets (`datasetIds` + `datasetFields`).
- Tasks: summarize, extract (customer JSON Schema, normalised for structured outputs and validated), classify (labels with basis; confidence shown only as model-reported), analyze, detect_key_info, compare, report, custom.
- Claude through the official `anthropic` SDK 1.x. Structured output via `output_config.format`; `output_config.effort` per analysis level (quick low / standard medium / deep high) on the default model Claude Opus 5.5; model override from `src/models.json` (Sonnet 5.5, Haiku 5.5, Fable 5.1); prompt caching of the shared system prompt; streaming above 16K output tokens; Message Batches API option (`useBatchApi`).
- Stop reasons: `refusal` handled before reading the content, with the server-side refusal fallback (`fallbacks: "default"`, beta `server-side-fallback-2026-07-01`) on Opus 5.5 / Sonnet 5.5 / Fable 5.1; `max_tokens` retried once with twice the limit.
- Retries with back-off and `retry-after` on 429 / 5xx / 529 / network errors; typed SDK exceptions; a bad key stops the run at once.
- Cost guards: maxInputTokensPerItem (map-reduce chunking, never silent truncation), maxOutputTokensPerItem, maxTotalTokens, maxCostUsd, estimateOnly (count_tokens), worst-case check before every request.
- Key modes: own_key (`item-analyzed` event) and managed (paying Apify users only, per-1k-token events per model, server ceilings, Apify max-charge check before each request; key from the `MANAGED_ANTHROPIC_API_KEY` secret env var).
- Security: secrets redacted from every message; SSRF guard (private, loopback, link-local, metadata and ULA addresses refused after DNS resolution and after each redirect, at most 5 redirects, 32 MB cap).
- Visual report (kit 1.1.0, copied from kick-streamers-scraper), RUN_SUMMARY, FAILED_INPUTS, review invitation on successful runs.
- `MOCK_PROVIDER=1` local mock mode (ignored on the Apify platform).
