mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-11-11 08:03:01 +03:00
Measure Gemini image-token cost instead of using the pixel heuristic (#24469)
* Measure Gemini image-token cost instead of using the pixel heuristic Review descriptions size their frame budget from the provider's estimate_image_tokens(). Gemini did not override it, so the base ~1-token-per-1250-pixels heuristic was used: about 46 tokens for a 320x180 preview frame. Gemini 3 models actually bill about 1100 tokens per image regardless of size, so the 24000-token image budget let Frigate send one frame per second of the review item (e.g. 90 frames for a 90 s alert) instead of ~21, and review descriptions cost roughly 4x more than intended. Probe the model with the free count_tokens endpoint, the same way the llama.cpp provider measures its mmproj, and cache the result per image size. Fall back to the base heuristic if the request fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bound Gemini image-token probes and back off after failures estimate_image_tokens() runs on the enrichment thread, and count_tokens was using the client's defaults: a 120 s timeout and 3 attempts on 429/5xx. A hung endpoint could stall review processing for minutes per review, and a rate-limited key retried on every review of an unmeasured size. Give the probe a 5 s timeout with no retries, and after a failure use the heuristic without probing again for 5 minutes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Update docs to note that Gemini charges roughly-flat token cost regardless of video resolution --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
52c6175255
commit
463942e01d
@@ -431,6 +431,56 @@ class TestGeminiProvider(unittest.TestCase):
|
||||
self.assertEqual(final["content"], "ok")
|
||||
|
||||
|
||||
class TestGeminiImageTokens(unittest.TestCase):
|
||||
def _client(self, count_tokens):
|
||||
client = _make_client("gemini", model="gemini-3-flash", api_key="k")
|
||||
client.provider = SimpleNamespace(
|
||||
models=SimpleNamespace(count_tokens=count_tokens)
|
||||
)
|
||||
return client
|
||||
|
||||
def test_measures_image_cost_and_caches_per_size(self):
|
||||
count_tokens = MagicMock(
|
||||
side_effect=lambda model, contents, config: SimpleNamespace(
|
||||
total_tokens=2 if len(contents) == 1 else 1102
|
||||
)
|
||||
)
|
||||
client = self._client(count_tokens)
|
||||
|
||||
self.assertEqual(client.estimate_image_tokens(320, 180), 1100)
|
||||
self.assertEqual(client.estimate_image_tokens(320, 180), 1100)
|
||||
self.assertEqual(count_tokens.call_count, 2)
|
||||
|
||||
image = count_tokens.call_args.kwargs["contents"][1]
|
||||
self.assertEqual(image.inline_data.mime_type, "image/jpeg")
|
||||
self.assertEqual(count_tokens.call_args.kwargs["model"], "gemini-3-flash")
|
||||
http_options = count_tokens.call_args.kwargs["config"].http_options
|
||||
self.assertEqual(http_options.timeout, 5000)
|
||||
self.assertEqual(http_options.retry_options.attempts, 1)
|
||||
|
||||
client.estimate_image_tokens(640, 360)
|
||||
self.assertEqual(count_tokens.call_count, 4)
|
||||
|
||||
def test_failure_falls_back_to_heuristic_and_waits_before_retrying(self):
|
||||
count_tokens = MagicMock(side_effect=RuntimeError("offline"))
|
||||
client = self._client(count_tokens)
|
||||
|
||||
with patch("frigate.genai.plugins.gemini.time.monotonic", return_value=1000):
|
||||
self.assertEqual(client.estimate_image_tokens(320, 180), 320 * 180 / 1250)
|
||||
self.assertEqual(client.estimate_image_tokens(640, 360), 640 * 360 / 1250)
|
||||
self.assertEqual(count_tokens.call_count, 1)
|
||||
|
||||
with patch("frigate.genai.plugins.gemini.time.monotonic", return_value=1300):
|
||||
client.estimate_image_tokens(320, 180)
|
||||
self.assertEqual(count_tokens.call_count, 2)
|
||||
|
||||
def test_missing_total_falls_back_to_heuristic(self):
|
||||
client = self._client(
|
||||
MagicMock(return_value=SimpleNamespace(total_tokens=None))
|
||||
)
|
||||
self.assertEqual(client.estimate_image_tokens(320, 180), 320 * 180 / 1250)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ollama
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user