From c58bbc3672acbd56bcf11df932658fc45cbf9566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Miquel?= Date: Mon, 12 Oct 2026 11:06:44 +0200 Subject: [PATCH] Fixing timeouts in label detection, maybe. --- copienator/commands/gemini_for_labels.py | 14 ++++++++++--- tests/test_label_timeouts.py | 25 ++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/copienator/commands/gemini_for_labels.py b/copienator/commands/gemini_for_labels.py index f05e774..40b3ef2 100644 --- a/copienator/commands/gemini_for_labels.py +++ b/copienator/commands/gemini_for_labels.py @@ -31,6 +31,7 @@ from copienator.utils import natural_key, read_all_labels MODEL_ID = config.MODEL_FOR_LABEL_ID api_key = config.API_KEY +REQUEST_TIMEOUT_SECONDS = 80 my_prompt = """I'm giving you an image of the left columns of a written exam. Students answer several exercises, which can have several questions. @@ -213,6 +214,8 @@ def generate_request( wrong_label_text_context="", wrong_label_text_file: Path | None = None, seed: int = 0, + *, + retry: bool = False, ): """Generates request for Gemini with context.""" @@ -275,11 +278,11 @@ def generate_request( generate_content_config = types.GenerateContentConfig( http_options=types.HttpOptions( - timeout=120_000, + timeout=REQUEST_TIMEOUT_SECONDS * 1000, retry_options=types.HttpRetryOptions(attempts=1), ), automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True), - thinking_config=thinking_config(config.THINKING_LEVEL_FOR_LABEL), + thinking_config=thinking_config("low" if retry else config.THINKING_LEVEL_FOR_LABEL), seed=seed, max_output_tokens=65535, response_mime_type= "application/json", @@ -435,7 +438,11 @@ def process_copy_group( if attempt > 0: print(f"[{group_key}] Retrying {image_file.name} in {10 * attempt}s...") sleep(10 * attempt) - print(f"[{group_key}] {image_file.name}: attempt {attempt + 1}/{MAX_ATTEMPTS} (120s timeout).") + level = "low" if attempt > 0 else config.THINKING_LEVEL_FOR_LABEL + print( + f"[{group_key}] {image_file.name}: attempt {attempt + 1}/{MAX_ATTEMPTS} " + f"({REQUEST_TIMEOUT_SECONDS}s timeout, thinking={level or 'default'})." + ) try: text_context_file, text_context = closest_text_context( workspace, wrong_labels @@ -455,6 +462,7 @@ def process_copy_group( text_context, text_context_file, seed=request_seed, + retry=attempt > 0, ) response = client.models.generate_content( model=MODEL_ID, diff --git a/tests/test_label_timeouts.py b/tests/test_label_timeouts.py index de68be3..f688489 100644 --- a/tests/test_label_timeouts.py +++ b/tests/test_label_timeouts.py @@ -69,10 +69,25 @@ class LabelTimeoutTests(unittest.TestCase): self.assertEqual(self.client.models.generate_content.call_count, 2) for call in self.client.models.generate_content.call_args_list: options = call.kwargs["config"].http_options - self.assertEqual(options.timeout, 120_000) + self.assertEqual(options.timeout, 80_000) self.assertEqual(options.retry_options.attempts, 1) self.assertTrue((self.workspace.copies_dir / "Copie01_01.json").exists()) + def test_retries_use_low_thinking_and_next_image_restores_configured_level(self): + self.client.models.generate_content.side_effect = [ + TimeoutError(), TimeoutError(), self.response, self.response, + ] + with patch.object(labels.config, "THINKING_LEVEL_FOR_LABEL", "medium"): + self.assertEqual(self.group(self.images[:2]), 2) + self.assertEqual(labels.config.THINKING_LEVEL_FOR_LABEL, "medium") + configs = [call.kwargs["config"] + for call in self.client.models.generate_content.call_args_list] + self.assertEqual( + [cfg.thinking_config.thinking_level.value for cfg in configs], + ["MEDIUM", "LOW", "LOW", "MEDIUM"], + ) + self.assertTrue(all(cfg.http_options.timeout == 80_000 for cfg in configs)) + def test_exhaustion_defers_dependent_pages_but_other_copies_finish_and_resume(self): self.client.models.generate_content.side_effect = [ TimeoutError(), TimeoutError(), TimeoutError(), self.response, @@ -99,9 +114,15 @@ class LabelTimeoutTests(unittest.TestCase): self.client.models.generate_content.side_effect = [Mock(text=( '{"name":"Student","list":[{"box_2d":[1,2,3,4],"label":"bad%d"}]}' % index )) for index in range(3)] - with self.assertRaises(labels.IncompleteCopyError): + with patch.object(labels.config, "THINKING_LEVEL_FOR_LABEL", "medium"), \ + self.assertRaises(labels.IncompleteCopyError): self.group(self.images[:1]) self.assertEqual(self.client.models.generate_content.call_count, 3) + self.assertEqual( + [call.kwargs["config"].thinking_config.thinking_level.value + for call in self.client.models.generate_content.call_args_list], + ["MEDIUM", "LOW", "LOW"], + ) def test_progress_reports_waiting_image_and_elapsed_time(self): entered, release = Event(), Event()