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

- **URL**: https://apify.com/badr_xba/ai-document-web-analyzer-openai/changelog.md
- **Full Actor documentation**: https://apify.com/badr_xba/ai-document-web-analyzer-openai.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) 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 "OpenAI 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: OpenAI 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 "OpenAI refused the API key (401)...", an error row in the dataset, codes `authError`/`permissionDenied` + `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 (compare order; local, not pushed)

- Fix: task `compare` sent the profiles and listed `comparedSources` in the order the items finished (with concurrency > 1 the second source could come first). They now follow the input order. The Done message already counted "N item(s) analysed + 1 comparison".

### 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.

### 0.3 — 2026-11-09 (local build, not published; no price change)

#### Commercial safety (FINANCE_AUDIT C1–C4, TESTS_AUDIT)

- Managed mode is refused before any AI call (`managedPricingMissing`) when the run is not pay-per-event, or when the `input-/output-tokens-1k-<tier>` events of the chosen model are missing from the run's live pricing info (`ChargingManager.get_pricing_info()`) or priced at 0. Proposed prices no longer let a call through: a managed event without a live price makes the worst case infinite.
- New per-model `managedEnabled` flag in `models.json` (missing = off). A disabled model is refused in managed mode (`managedDisabled`) without an Apify price change.
- Answers are streamed (`responses.create(stream=True)`). While a stream runs, the Actor keeps a live token count: the counted input plus the text received so far. An item timeout or a dropped stream therefore still leaves a chargeable lower bound. In managed mode a stream that had started is not retried; its tokens are charged once with the failed item.
- Timeouts come from the output budget: `itemTimeoutSecs` 0 (new default) = max(300, ceil(maxOutput / 30 tok/s) + 60). With the managed key a shorter value is raised to that minimum; with your own key you get a warning. The OpenAI client read timeout follows the same figure. Schema: `itemTimeoutSecs` now 0–7200, default 0.
- Managed calls are counted first with OpenAI's documented `POST /v1/responses/input_tokens`. If the count fails, a conservative local count is used: ASCII/3 + 1 token per non-ASCII character, ×1.25. The limit per call is min(maxInputTokensPerItem, 272K − 2K), so a managed call never crosses the long-context step. When the count is above the limit, the content is split using the counted size: chunks are halved, or the final notes are trimmed.
- Estimate-only + managed key for a user without a paid plan: the managed key is dropped, so no provider is built and no token count is made. `exactTokenCount` is skipped with a warning.
- `Actor.charge` results are read: `charged_count` goes into `billedEvents` and RUN_SUMMARY, and any shortfall goes into `unchargedEvents`. A partial charge or `event_charge_limit_reached` stops further AI calls (`maxTotalChargeUsd`). A failed charge is logged at warning level and, in managed mode, stops further calls (`chargeFailed`).
- Every uncaught exception is caught at the top level. Its traceback is logged redacted, and the run fails without re-raising, so the SDK's raw traceback is never printed. An unknown input field that holds a key-looking value triggers a warning that names the field but does not echo the value.

#### Tests

- 175 tests, up from 152. New tests cover:
  - the ChargingManager path of `remaining_charge_usd()`: finite limit, infinite limit, manager failure falling back to the env value;
  - live prices only;
  - charge failure, partial charge and limit-reached, at unit level and in full runs (the fake Actor reports the limit);
  - missing-event and non-PPE refusals, `managedDisabled`;
  - estimate-only with the managed key for a free user;
  - counted-size splitting with CJK text, chunk halving and notes trimming;
  - timeout and broken-stream charging, no retry in managed mode, retry with your own key;
  - stream error and cut events;
  - traceback redaction, the key-looking unknown field, derived timeouts.
- `test_event_prices_keep_margin` now compares against hand-computed figures. The kit-copy test checks a SHA-256 stored in the test and no longer reads a file outside the repository.

### 0.2 — 2026-11-09 (local build, not published)

#### Security

- DNS rebinding closed: each host is resolved once (cached per run), every answer must be public, and every connection (first URL, robots.txt, each redirect) 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 `blockedAddress` code, redirect and size caps. Tests run the real httpx/httpcore stack on a fake network backend.

### 0.1 — 2026-11-09 (local build, not published)

#### Added

- First version: web pages, texts, documents (PDF, DOCX, PPTX, XLSX, TXT, MD, CSV, JSON), images and Apify datasets analysed with OpenAI GPT through the Responses API and strict Structured Outputs.
- Tasks: summarize, extract (customer JSON Schema checked against strict-mode rules), classify, analyze, detect_key_info, compare (with a comparison row), report, custom.
- Levels quick / standard / deep → gpt-6-luna / gpt-6.1-sol / gpt-6-astra, with model and effort overrides. Models and prices live in `src/models.json`.
- Cost guards, estimate-only mode (optional exact count through `responses.input_tokens.count`), chunk map-reduce for long content (never silent truncation), long-context pricing above 272K input tokens.
- Key modes: own key (`item-analyzed` event) and managed (`input/output-tokens-1k-<tier>` events, paid Apify users only, server-side caps, max-charge check before each call).
- Retries with back-off on 429/5xx/timeouts that respect Retry-After. Quota, auth and model errors stop the run. Refusals and incomplete answers are recorded with their tokens.
- Visual report (kit 1.1.0), RUN_SUMMARY, FAILED_INPUTS, review invitation on success, key redaction.
- SSRF guard: only http/https. Every hop (first URL and each redirect, followed by hand, max 5) must resolve to public addresses only. Loopback, private, link-local/metadata (169.254.169.254), unique-local IPv6, unspecified, `localhost` and `*.internal` are refused with code `blockedAddress`. Responses are size-capped.
- gpt-6.1-terra has its own billing tier `mid` (managed events `input/output-tokens-1k-mid`).
- `MOCK_PROVIDER=1` canned answers for local runs only (refused when `APIFY_IS_AT_HOME=1`).
