Summarize, extract to your JSON schema, classify or compare web pages, PDFs, images, audio, video, YouTube links and any Apify dataset with Google Gemini. Bring your own Gemini API key. Token and cost estimate on every row. Not affiliated with Google.
2026-11-10 (rejected API key: same behaviour in the Claude, OpenAI and Gemini Actors; local, not pushed)
A rejected key (HTTP 401, HTTP 400 API_KEY_INVALID ("API key not valid", expired key) / 403 PERMISSION_DENIED) 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 "Gemini 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: Gemini 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: Google's real answer to a wrong key (HTTP 400 INVALID_ARGUMENT "API key not valid", reason API_KEY_INVALID) was classified invalidRequest (an input error), so the run ended SUCCEEDED with 0 rows and a generic "Done" message; 401/403 used code auth + skipped.
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, Gemini 400 API_KEY_INVALID / expired / PERMISSION_DENIED.
2026-11-10 (fixes from real Gemini API tests; local, not pushed)
Fix: task detect_key_info was refused by Gemini on every item (HTTP 400 INVALID_ARGUMENT "Request contains an invalid argument", real test with gemini-3.5-flash-lite). Every keyword of its schema is in Google's supported subset; the difference with the tasks that work was "maxItems": 60 on result.facts (array of objects with a 10-value type enum), which Gemini compiles into a too-large decoding automaton. maxItems removed from facts; the prompt asks for at most 60 facts and the list is capped at 60 after the answer (warning when facts are dropped).
New test: the exact response_json_schema sent for every task (and the chunk / compare calls) is checked against Google's documented responseJsonSchema subset (keywords, enum only for strings/numbers and non-empty, $ref without siblings, required names existing properties) + arrays of objects <= 30 items.
Fix: task compare numbered the sources in completion order (the second text could become "Item 1"). Rows are now compared in INPUT order: prompt labels "Item N", comparedSources and the model's item numbers all follow the input. Done message: "2 of 2 item(s) analysed and compared (3 rows: 2 items + 1 comparison)" instead of "3 of 2 item(s) analysed"; RUN_SUMMARY itemsAnalysed counts items only, new rowsSaved.
Checked (no code change): a local datasetIds: ["gta_local"] run exits 1 because crawlee 1.11 rejects the storage name gta_local (names: a-z, 0-9, hyphens in the middle). Same layout with gta-local (datasets/gta-local/000000001.json... + __metadata__.json) reads fine. On Apify the dataset is opened by id with force_cloud.
2026-11-09 (billed tokens of failed items; local, not pushed)
Fix: an item that failed after Google processed it (maxTokens, blocked refusal/safety, cut stream, providerNoResponse/errors after partial data, budget stop mid-chunks) was shown as "0 input + 0 output tokens, est. $0.0000" although Google bills those tokens. Every attempt that reported usage is now recorded once per item, at the price of the model that answered it (fallback and audio rates included), and added to the run totals, RUN_SUMMARY and status/done messages; the FAILED_INPUTS entry gets tokens and costUsd (omitted when nothing was reported). Successful rows also include a cut attempt made before a fallback. Managed mode: the shown tokens are the ones charged per attempt; no double counting across retries/fallback.
2026-11-09 (level fallback model; local, not pushed)
New model gemini-3.6-flash (tier 36-flash, same prices as 3.8-flash incl. the 2027-01-01 increase; capabilities copied from 3.8-flash, not re-verified on the models page; managedEnabled: false, proposed events only).
levels.standard.fallback = ["gemini-3.6-flash"]: own key + no explicit model only. On providerNoResponse, HTTP 503 or HTTP 429 RESOURCE_EXHAUSTED from the level model, the item is retried once on the fallback (no explicit cache, thinking level kept when supported). Rows get model = model used, requestedModel, fallbackReason; cost at the fallback's prices; RUN_SUMMARY fallbacks/fallbackModels; status and final messages mention it. If the fallback fails, the primary's code is kept with a note. Once the primary is skipped after 2 no-responses, later items go straight to the fallback. Managed mode: no fallback.
After the first real test (198 s): with a fallback available for the item, the level model gets NO same-model retry on no-response / 503 / 429 (straight to the fallback); the single retry stays when there is no fallback. firstResponseTimeoutSecs default 90 -> 60. Done message and RUN_SUMMARY name the model(s) that actually answered (model, requestedModel, modelsUsed), e.g. "analysed with gemini-3.6-flash (fallback for gemini-3.8-flash)".
2026-11-09 (first-response timeout; local, not pushed)
Fix: gemini-3.8-flash (free tier) accepted requests but sent no bytes, and an item hung > 12 min (per-call timeout sized for max output, repeated by retries).
New input firstResponseTimeoutSecs (default 90, 15–600): a streamed call with no chunk at all (no text, thinking or usage) within it is aborted; calls already streaming keep the full per-call timeout.
Such a call fails as providerNoResponse after at most ONE retry (none in managed mode: nothing reported, nothing charged). After 2 consecutive no-responses on the model, remaining items are skipped with providerNoResponse (status message says so); other items' results still make the run SUCCEEDED.
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 and models.json (item-analyzed $0.002 -> $0.0015) updated. The input schema startUrls prefill (a large Wikipedia page) is replaced by a short texts prefill so the demo stays cheap.
0.2.3 — 2026-11-09 (commercial safety, local, not published)
Audio pricing from 0.2.2 (audio rates, managed audio refusal on gemini-3.1-flash-lite) is unchanged.
Fixed (owner-loss risks from the finance audit)
No free managed usage: managed runs are refused before any AI call unless the run is pay-per-event (ChargingManager.get_pricing_info().is_pay_per_event), every event of the model is in the run's live per_event_prices with a price > 0, and each live token price covers Google's highest price for the model (any from date, long context) × 1.625. The proposed prices in models.json are no longer used for any charging decision (Biller.event_price returns live prices only; a missing price makes the reservation refuse: missingEventPrice). charge_room() returns 0 (refuse) when the max total charge cannot be read.
Per-model managedEnabled flag in models.json (opt-in: missing or false = managed refused for that model; validated by load_catalog).
Timeouts: generation is streamed (generate_content_stream), the usage metadata is kept as chunks arrive, so a cut-off call still reports and charges its tokens. Per-call timeout ≥ maxOutputTokens / 20 tok/s + 120 s. In managed mode a timeout / network drop is retried only when the failed attempt's usage was reported and fully charged. SDK automatic continuation switched off (each continuation hop is a separately billed request that could exceed max_output_tokens).
Exact input in managed mode: every managed call is sized with count_tokens over the same parts (inline media, Files API uploads, YouTube) plus the system instruction and schema as text parts (CountTokensConfig refuses system_instruction on the Gemini API); a failed count means no call (countUnavailable). Managed URL context is refused (fetched pages cannot be counted or bounded below the context window).
Context cache: no explicit cache in managed mode (owner-paid storage, no event bills it, may survive an aborted run). Own-key storage estimate now uses the price in effect today (price_on) instead of the last (future) price entry.
Redaction: the per-item crash traceback is logged redacted (was Actor.log.exception, raw); main() catches every uncaught error, logs a redacted traceback (customer key, managed key and key-looking unknown-field values scrubbed) and fails the run without the SDK's raw traceback. Unknown input fields holding key-looking values get a warning that names the field, never the value.
split_pdf with 0 / negative pages per chunk produced empty PDFs; now one page per chunk.
Tests
168 → 202 (line coverage 84 % → 87 %; billing 95 %, main 92 %, provider 91 %): live-price fallbacks, charge_room branches, missing-event / not-PPE / managedEnabled / URL-context refusals, exact-count reservation, count failure, charge limit reached mid-run (fake Actor applies the SDK's limit arithmetic), managed timeout retry rules, partial-stream charging, Files API polling (PROCESSING → ACTIVE, FAILED, 5-minute timeout, upload error), PDF split edge cases, item and run crash paths redacted, unknown key-field warning, double-charge checks in the "never raises" billing tests.
0.2.1 — 2026-11-09 (model audit, local, not published)
Fixed
gemini-3.1-flash-lite cached-input price: was $0.25 (worst-case placeholder, "not confirmed"); the official pricing page (aistudio.google.com/docs/pricing, read 2026-11-09) lists $0.025 (text/image/video; $0.05 audio). Added shutdownDate: 2027-05-07 from aistudio.google.com/docs/deprecations (replacement gemini-3.5-flash-lite). README price table and QUALITY.md updated.
gemini-4-argondefaultThinkingLevel: was null; aistudio.google.com/docs/thinking lists "On (medium)". Informational field (not read by the code).
Known, not fixed
gemini-3.1-flash-lite audio input is $0.50/1M (cache $0.05), twice the text rate; the code prices all input at one rate, so cost estimates for audio on this model are 2x low. Managed billing charges input-tokens-1k-31-flash-lite at $0.0005/1k = $0.50/1M, which covers Google's audio cost at zero margin.
0.2 — 2026-11-09 (local build, not published)
Security
DNS rebinding closed: each hop's host 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 blockedAddress code, redirect and size caps.
Removed
Google Search grounding. Google's terms for Grounding with Google Search forbid storing/caching grounded results and Search Suggestions and require showing them unmodified only to the end user who sent the prompt; an Actor stores every result in a dataset. The useGoogleSearch input is gone (a run that still sends true is refused with a clear message before any AI call), the Google Search tool is never sent, and searchSuggestionsHtml / webSearchQueries are no longer stored or shown in the report. groundingSources now lists URL context retrievals only. The grounding price was removed from models.json and PRICING.
URL context is kept: neither the Gemini API Additional Terms nor the URL context guide set a storage or display restriction on it.
0.1 — 2026-11-09 (local build, not published)
Added
Gemini analysis of web pages, texts, PDF/DOCX/TXT/MD/CSV documents, images, audio/video files, public YouTube links and rows of other Apify datasets.
Tasks: summarize, extract (customer JSON Schema, validated up front and checked after the answer), classify (labels with model-reported confidence and quoted basis), analyze, detect_key_info, compare (+ comparison row), report, custom.
Analysis levels quick / standard / deep mapped in src/models.json (gemini-3.5-flash-lite, gemini-3.8-flash, gemini-4-argon), model override, date-effective prices (gemini-4-argon from 2026-12-01, gemini-3.8-flash from 2027-01-01).
Cost guards: estimateOnly (automatic without a key), map-reduce over long texts and PDFs with explicit truncated flag, video clip length, max output/total tokens, max estimated cost, managed-mode ceilings and max-charge check before every call.
Key modes: own key (flat item-analyzed event) and managed key (per-1k-token events per tier, paid Apify users only, key from an Actor secret env var).
Optional URL context and Google Search grounding (own key), explicit context cache for long shared instructions, Files API for large media.
SSRF guard: http(s) only, private/loopback/link-local/metadata/unique-local addresses refused after DNS resolution and on every redirect hop (max 5), also for URLs handed to Gemini (YouTube, URL context); one billing tier (pair of token events) per model.
Google Search grounding: Search Suggestions kept unmodified (searchSuggestionsHtml, webSearchQueries) and shown next to the grounded answer in the report (Gemini API terms); YouTube items label their image "YouTube thumbnail" (imageLabel).
Local mock mode (MOCK_PROVIDER=1, ignored on the Apify platform).