Skip to content

perf(gemini): replace disk-backed Files-API cache with thread-safe in-memory cache - #4154

Open
hpiclaranet wants to merge 1 commit into
langgenius:mainfrom
hpiclaranet:perf/gemini-filecache-in-memory-013
Open

hpiclaranet wants to merge 1 commit into
langgenius:mainfrom
hpiclaranet:perf/gemini-filecache-in-memory-013

Conversation

@hpiclaranet

Copy link
Copy Markdown
Contributor

Summary

Rebased onto current main (b4df0da3, gemini 0.9.12) and bumped to 0.9.13, superseding the closed #3759 (closed by the maintainer due to merge conflicts; rebase + bump + new PR requested).

Scope note: the previous #3759 bundled four performance changes. Since then, the main has merged the Gemini 4 Argon support (#4055), which reworked usage/streaming handling in the same files. Rebasing the old diff wholesale would have regressed that Argon behaviour, so this re-submission is scoped to the single change that is still missing from main and does not touch the Argon paths: replacing the disk-backed Files-API cache with a thread-safe in-memory cache. The remaining #3759 items (the genai.Client connection-pool reuse and local token counting in the embedding splitter) are deliberately left out and tracked as follow-ups.

What this PR does

models/gemini/models/llm/utils.py — FileCache is now an in-memory, thread-safe cache (with lazy expiry-based eviction and a hard cap on live entries) instead of a disk-backed JSON file that was re-read and re-written on every exists/get/setex call with no locking (concurrent workers could lose updates or corrupt the file).

Trade-offs (documented in code):

  • Entries no longer survive a process restart — identical files may be re-uploaded to the Gemini Files API after a restart. The upload is free; it only costs a little latency.
  • The cache is bounded: a lazy sweep purges expired entries (triggered above 500 entries, at most once per 10 min) and a hard cap (5 000 live entries, oldest-first) bounds memory growth.

Release Notes

  • Performance: the Gemini Files API upload cache is now an in-memory, thread-safe cache with expiry-based eviction and bounded size (replaces the unlocked disk JSON file that was rewritten on every access; entries do not persist across restarts, with a free re-upload fallback).

Change Type

  • Documentation / non-plugin change
  • Non-LLM plugin (tools, extensions, datasource, etc.)
  • LLM plugin

Screenshots / Videos

Not applicable — no user-facing behaviour change (internal cache storage only; identical model outputs).

LLM Plugin Checklist

Areas affected by this change (check all that apply)
  • Message flow (system messages, user ↔ assistant turn-taking)
  • Tool interaction flow (multi-round usage, Agent App and Agent Node)
  • Multimodal input (images, PDFs, audio, video, etc.) — the cache backs the Files API upload checks for multimodal parts
  • Multimodal output (images, audio, video, etc.)
  • Structured output (JSON, XML, etc.)
  • Token consumption metrics
  • Other LLM functionality (reasoning, grounding, prompt caching, etc.) — caching layer used by multimodal content handling
  • New models / model parameter fixes

Version

Testing

  • Local deployment — clean uv venv matching manifest.yaml, pyproject.toml, and uv.lock (no Dify platform instance in this run; validation was the plugin suite in that venv): full gemini suite passes on the rebased head (main b4df0da3) — 256 passed, 40 deselected (live-API tests, no GEMINI_API_KEY set; includes the Gemini 4 Argon tests from feat(gemini): add Gemini 4 Argon support #4055). New models/tests/test_perf_caches.py covers the in-memory cache: setex/get/expiry, 8-thread × 200-key concurrent writes with zero lost updates, eviction purging expired entries only, concurrent reads during writes, signature backwards compatibility, and the hard cap on live entries. ruff check reports zero findings on the changed files.
  • SaaS (cloud.dify.ai)

Known limitations:

  • Cached uploads do not survive a plugin/daemon restart (one free re-upload per distinct file after restart).
  • Pre-existing (unchanged here): the exists() → get() sequence around the cache in file_parts.py is not atomic — if an entry expires in that window, get() returns None and the code can raise AttributeError; this predates this PR and is unchanged by it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants