update Gemini API : use thinking levels
This commit is contained in:
1 parent
7bc588e9a1
commit
ec6ac9580b
9 files changed
+161
-21
No files matched your search
@@ -174,6 +174,15 @@ ou éventuellement, la renseigner directement dans le fichier
|
|||||||
|
|
||||||
Copier `default_config.py` en `config.py`. Éventuellement le modifier.
|
Copier `default_config.py` en `config.py`. Éventuellement le modifier.
|
||||||
|
|
||||||
|
Les niveaux de réflexion Gemini se règlent indépendamment des modèles :
|
||||||
|
=THINKING_LEVEL_LITE= (énoncés, ="low"=), =THINKING_LEVEL_FOR_LABEL=
|
||||||
|
(labels, ="medium"=), =THINKING_LEVEL_FLASH= (correction courante,
|
||||||
|
="medium"=) et =THINKING_LEVEL_PRO= (correction difficile, ="high"=).
|
||||||
|
Ces réglages s'appliquent aussi aux requêtes batch. Utiliser =None= pour
|
||||||
|
laisser le modèle choisir son défaut. Les niveaux disponibles dépendent
|
||||||
|
du modèle : Gemini 3.8 Flash accepte ="low"=, ="medium"= et ="high"=,
|
||||||
|
mais pas ="minimal"=. Avec un modèle Gemini 2.5, utiliser =None=.
|
||||||
|
|
||||||
** Correction d'un paquet de copies
|
** Correction d'un paquet de copies
|
||||||
|
|
||||||
1. Créer un fichier =names= dans le dossier courant, avec les
|
1. Créer un fichier =names= dans le dossier courant, avec les
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ from copienator import configuration as config
|
|||||||
from copienator.commands import grouping
|
from copienator.commands import grouping
|
||||||
from copienator.answer_origins import save_origins
|
from copienator.answer_origins import save_origins
|
||||||
from copienator import prompting
|
from copienator import prompting
|
||||||
|
from copienator.gemini_config import thinking_config
|
||||||
|
from copienator import configuration as settings
|
||||||
from copienator.feedback_boxes import valid_feedback_box
|
from copienator.feedback_boxes import valid_feedback_box
|
||||||
from copienator.latex_json import repair_latex_json
|
from copienator.latex_json import repair_latex_json
|
||||||
from copienator import (
|
from copienator import (
|
||||||
@@ -279,6 +281,9 @@ def call_gemini_with_retries(model_id, contents, config,
|
|||||||
# Switch to fallback immediately if quota was exhausted by another thread
|
# Switch to fallback immediately if quota was exhausted by another thread
|
||||||
if model_id == MODEL_ID_pro and pro_quota_exhausted and fallback_model_id:
|
if model_id == MODEL_ID_pro and pro_quota_exhausted and fallback_model_id:
|
||||||
model_id = fallback_model_id
|
model_id = fallback_model_id
|
||||||
|
config = config.model_copy(update={"thinking_config": thinking_config(
|
||||||
|
settings.THINKING_LEVEL_FLASH
|
||||||
|
)})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
full_response_text = ""
|
full_response_text = ""
|
||||||
@@ -312,6 +317,9 @@ def call_gemini_with_retries(model_id, contents, config,
|
|||||||
if is_quota_error and model_id == MODEL_ID_pro and fallback_model_id:
|
if is_quota_error and model_id == MODEL_ID_pro and fallback_model_id:
|
||||||
tprint(f"\tGemini Pro quota hit ({e}). \n\n\tFalling back to Flash permanently...")
|
tprint(f"\tGemini Pro quota hit ({e}). \n\n\tFalling back to Flash permanently...")
|
||||||
model_id = fallback_model_id
|
model_id = fallback_model_id
|
||||||
|
config = config.model_copy(update={"thinking_config": thinking_config(
|
||||||
|
settings.THINKING_LEVEL_FLASH
|
||||||
|
)})
|
||||||
pro_quota_exhausted = True
|
pro_quota_exhausted = True
|
||||||
continue # Retry immediately with Flash
|
continue # Retry immediately with Flash
|
||||||
|
|
||||||
@@ -506,7 +514,11 @@ def process_single_task(task_tuple, precomputed_response=None):
|
|||||||
flash_count += 1
|
flash_count += 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
contents, config = prompting.generate_request(INPUT_DIR, file_path, label)
|
contents, config = prompting.generate_request(
|
||||||
|
INPUT_DIR, file_path, label,
|
||||||
|
thinking_level=(settings.THINKING_LEVEL_FLASH if use_flash
|
||||||
|
else settings.THINKING_LEVEL_PRO),
|
||||||
|
)
|
||||||
model_to_use = MODEL_ID_flash if use_flash else MODEL_ID_pro
|
model_to_use = MODEL_ID_flash if use_flash else MODEL_ID_pro
|
||||||
|
|
||||||
if precomputed_response:
|
if precomputed_response:
|
||||||
@@ -891,13 +903,10 @@ def run_configured(args: argparse.Namespace) -> ExitCode:
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"generation_config": {
|
"generation_config": prompting.batch_generation_config(
|
||||||
"temperature": 1.0,
|
settings.THINKING_LEVEL_FLASH if use_flash
|
||||||
"topP": 0.95,
|
else settings.THINKING_LEVEL_PRO
|
||||||
"maxOutputTokens": 65535,
|
),
|
||||||
"responseMimeType": "application/json",
|
|
||||||
"responseSchema": prompting.UNROLLED_SCHEMA,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
line = json.dumps(request)
|
line = json.dumps(request)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from copienator import (
|
|||||||
workspace_from_args,
|
workspace_from_args,
|
||||||
)
|
)
|
||||||
from copienator import configuration as config
|
from copienator import configuration as config
|
||||||
|
from copienator.gemini_config import thinking_config
|
||||||
from copienator.filesystem import staged_files
|
from copienator.filesystem import staged_files
|
||||||
from copienator.latex_json import normalize_literal_newlines, repair_latex_json
|
from copienator.latex_json import normalize_literal_newlines, repair_latex_json
|
||||||
from copienator.platform import validate_windows_labels
|
from copienator.platform import validate_windows_labels
|
||||||
@@ -249,7 +250,7 @@ def _request_rubrics(client, group_context_text: str):
|
|||||||
config=types.GenerateContentConfig(
|
config=types.GenerateContentConfig(
|
||||||
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
||||||
system_instruction="Rédige tous les barèmes et consignes de notation en français. Conserve les clés JSON, les labels et les formules mathématiques.",
|
system_instruction="Rédige tous les barèmes et consignes de notation en français. Conserve les clés JSON, les labels et les formules mathématiques.",
|
||||||
temperature=0.2,
|
thinking_config=thinking_config(config.THINKING_LEVEL_LITE),
|
||||||
response_mime_type="application/json",
|
response_mime_type="application/json",
|
||||||
response_json_schema=GroupRubrics.model_json_schema(),
|
response_json_schema=GroupRubrics.model_json_schema(),
|
||||||
),
|
),
|
||||||
@@ -370,7 +371,7 @@ def refine_existing(workspace: EvaluationWorkspace, mode: str, *, api_client=Non
|
|||||||
))])],
|
))])],
|
||||||
config=types.GenerateContentConfig(
|
config=types.GenerateContentConfig(
|
||||||
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
||||||
temperature=0.1, response_mime_type="application/json",
|
thinking_config=thinking_config(config.THINKING_LEVEL_LITE), response_mime_type="application/json",
|
||||||
response_json_schema=LabelGroups.model_json_schema(),
|
response_json_schema=LabelGroups.model_json_schema(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -488,7 +489,7 @@ def process_exam(
|
|||||||
|
|
||||||
config_1 = types.GenerateContentConfig(
|
config_1 = types.GenerateContentConfig(
|
||||||
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
||||||
temperature=0.1,
|
thinking_config=thinking_config(config.THINKING_LEVEL_LITE),
|
||||||
response_mime_type="application/json",
|
response_mime_type="application/json",
|
||||||
response_json_schema=ExamQuestions.model_json_schema(),
|
response_json_schema=ExamQuestions.model_json_schema(),
|
||||||
)
|
)
|
||||||
@@ -505,7 +506,7 @@ def process_exam(
|
|||||||
|
|
||||||
config_2 = types.GenerateContentConfig(
|
config_2 = types.GenerateContentConfig(
|
||||||
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
||||||
temperature=0.1,
|
thinking_config=thinking_config(config.THINKING_LEVEL_LITE),
|
||||||
response_mime_type="application/json",
|
response_mime_type="application/json",
|
||||||
response_json_schema=ExamSolutions.model_json_schema(),
|
response_json_schema=ExamSolutions.model_json_schema(),
|
||||||
)
|
)
|
||||||
@@ -540,7 +541,7 @@ def process_exam(
|
|||||||
|
|
||||||
config_3 = types.GenerateContentConfig(
|
config_3 = types.GenerateContentConfig(
|
||||||
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
||||||
temperature=0.1,
|
thinking_config=thinking_config(config.THINKING_LEVEL_LITE),
|
||||||
response_mime_type="application/json",
|
response_mime_type="application/json",
|
||||||
response_json_schema=ExamContext.model_json_schema(),
|
response_json_schema=ExamContext.model_json_schema(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from google.genai import types
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from copienator import configuration as config
|
from copienator import configuration as config
|
||||||
|
from copienator.gemini_config import thinking_config
|
||||||
from copienator import (
|
from copienator import (
|
||||||
CliError,
|
CliError,
|
||||||
EvaluationWorkspace,
|
EvaluationWorkspace,
|
||||||
@@ -277,8 +278,7 @@ def generate_request(
|
|||||||
retry_options=types.HttpRetryOptions(attempts=1),
|
retry_options=types.HttpRetryOptions(attempts=1),
|
||||||
),
|
),
|
||||||
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
||||||
temperature=1.0,
|
thinking_config=thinking_config(config.THINKING_LEVEL_FOR_LABEL),
|
||||||
top_p=0.95,
|
|
||||||
seed=seed,
|
seed=seed,
|
||||||
max_output_tokens=65535,
|
max_output_tokens=65535,
|
||||||
response_mime_type= "application/json",
|
response_mime_type= "application/json",
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ else:
|
|||||||
_configuration = _load_user_config(CONFIG_PATH)
|
_configuration = _load_user_config(CONFIG_PATH)
|
||||||
|
|
||||||
# Keep new optional settings compatible with older personal configuration files.
|
# Keep new optional settings compatible with older personal configuration files.
|
||||||
|
THINKING_LEVEL_LITE = "low"
|
||||||
|
THINKING_LEVEL_FOR_LABEL = "medium"
|
||||||
|
THINKING_LEVEL_FLASH = "medium"
|
||||||
|
THINKING_LEVEL_PRO = "high"
|
||||||
ALWAYS_CROP = False
|
ALWAYS_CROP = False
|
||||||
SHOW_PERSONAL_STEPS = False
|
SHOW_PERSONAL_STEPS = False
|
||||||
RETURN_JPEG_ENABLED = True
|
RETURN_JPEG_ENABLED = True
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from google.genai import types
|
||||||
|
|
||||||
|
|
||||||
|
def thinking_config(level: str | None) -> types.ThinkingConfig | None:
|
||||||
|
"""Use a supported reasoning level, or omit it for the model default."""
|
||||||
|
if level is None:
|
||||||
|
return None
|
||||||
|
if level not in {"minimal", "low", "medium", "high"}:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid Gemini thinking level {level!r}; "
|
||||||
|
'expected "minimal", "low", "medium", "high", or None'
|
||||||
|
)
|
||||||
|
return types.ThinkingConfig(thinking_level=level)
|
||||||
+19
-6
@@ -1,6 +1,8 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import io
|
import io
|
||||||
from . import utils
|
from . import utils
|
||||||
|
from . import configuration as settings
|
||||||
|
from .gemini_config import thinking_config
|
||||||
|
|
||||||
PERSPECTIVE_GUIDANCE = (
|
PERSPECTIVE_GUIDANCE = (
|
||||||
"Ce barème est indicatif, si une réponse est entièrement correcte mais utilise "
|
"Ce barème est indicatif, si une réponse est entièrement correcte mais utilise "
|
||||||
@@ -173,8 +175,20 @@ UNROLLED_SCHEMA = {
|
|||||||
|
|
||||||
from google.genai import types
|
from google.genai import types
|
||||||
|
|
||||||
|
def batch_generation_config(thinking_level):
|
||||||
|
"""Build the REST generation config for a grading batch request."""
|
||||||
|
config = {
|
||||||
|
"maxOutputTokens": 65535,
|
||||||
|
"responseMimeType": "application/json",
|
||||||
|
"responseSchema": UNROLLED_SCHEMA,
|
||||||
|
}
|
||||||
|
thinking = thinking_config(thinking_level)
|
||||||
|
if thinking is not None:
|
||||||
|
config["thinkingConfig"] = thinking.model_dump(mode="json", by_alias=True, exclude_none=True)
|
||||||
|
return config
|
||||||
|
|
||||||
# The root model for parsing is be: List[EvaluationEntry]
|
# The root model for parsing is be: List[EvaluationEntry]
|
||||||
def generate_request(input_dir, file, full_label):
|
def generate_request(input_dir, file, full_label, *, thinking_level=settings.THINKING_LEVEL_PRO):
|
||||||
"""Generates request for Gemini."""
|
"""Generates request for Gemini."""
|
||||||
prompt = make_prompt(input_dir, full_label)
|
prompt = make_prompt(input_dir, full_label)
|
||||||
image_path = Path(file)
|
image_path = Path(file)
|
||||||
@@ -194,8 +208,7 @@ def generate_request(input_dir, file, full_label):
|
|||||||
|
|
||||||
generate_content_config = types.GenerateContentConfig(
|
generate_content_config = types.GenerateContentConfig(
|
||||||
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
||||||
temperature=1.0,
|
thinking_config=thinking_config(thinking_level),
|
||||||
top_p=0.95,
|
|
||||||
seed=0,
|
seed=0,
|
||||||
max_output_tokens=65535,
|
max_output_tokens=65535,
|
||||||
response_mime_type= "application/json",
|
response_mime_type= "application/json",
|
||||||
@@ -272,7 +285,7 @@ Commentaires d'origine :
|
|||||||
|
|
||||||
config = types.GenerateContentConfig(
|
config = types.GenerateContentConfig(
|
||||||
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
||||||
temperature=1.0,
|
thinking_config=thinking_config(settings.THINKING_LEVEL_FLASH),
|
||||||
response_mime_type="application/json",
|
response_mime_type="application/json",
|
||||||
response_json_schema=TypeAdapter(List[FeedbackItem]).json_schema()
|
response_json_schema=TypeAdapter(List[FeedbackItem]).json_schema()
|
||||||
)
|
)
|
||||||
@@ -303,7 +316,7 @@ Voici les labels possibles. Ta réponse doit être l'un d'entre eux :
|
|||||||
types.Part.from_text(text=prompt)])]
|
types.Part.from_text(text=prompt)])]
|
||||||
config = types.GenerateContentConfig(
|
config = types.GenerateContentConfig(
|
||||||
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
||||||
temperature=1.0,
|
thinking_config=thinking_config(settings.THINKING_LEVEL_FLASH),
|
||||||
)
|
)
|
||||||
return contents, config
|
return contents, config
|
||||||
|
|
||||||
@@ -334,7 +347,7 @@ d'entre eux :
|
|||||||
])]
|
])]
|
||||||
config = types.GenerateContentConfig(
|
config = types.GenerateContentConfig(
|
||||||
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
automatic_function_calling=types.AutomaticFunctionCallingConfig(disable=True),
|
||||||
temperature=1.0,
|
thinking_config=thinking_config(settings.THINKING_LEVEL_FLASH),
|
||||||
response_mime_type="application/json",
|
response_mime_type="application/json",
|
||||||
)
|
)
|
||||||
return contents, config
|
return contents, config
|
||||||
@@ -27,20 +27,28 @@ FINAL_SCORE_OUTPUT_DIR = Path("Server") / "copies"
|
|||||||
FINAL_SCORE_HISTOGRAM_PATH = Path("histogramme.pdf")
|
FINAL_SCORE_HISTOGRAM_PATH = Path("histogramme.pdf")
|
||||||
FINAL_SCORE_FONT_PATH = None
|
FINAL_SCORE_FONT_PATH = None
|
||||||
|
|
||||||
|
# Niveaux de réflexion : "minimal", "low", "medium", "high", ou None
|
||||||
|
# pour utiliser le défaut du modèle. Choisir un niveau supporté par le modèle
|
||||||
|
# (Gemini 3.8 Flash ne supporte pas "minimal" ; Gemini 2.5 : utiliser None).
|
||||||
|
|
||||||
# Modèle pour des choses très légères
|
# Modèle pour des choses très légères
|
||||||
# Le modèle 3.5-flash-lite est un peu moins bon pour la duplication de contexte pour des questions, dans l'analyse de l'énoncé.
|
# Le modèle 3.5-flash-lite est un peu moins bon pour la duplication de contexte pour des questions, dans l'analyse de l'énoncé.
|
||||||
# MODEL_LITE_ID = "gemini-3.5-flash-lite"
|
# MODEL_LITE_ID = "gemini-3.5-flash-lite"
|
||||||
MODEL_LITE_ID = "gemini-3.8-flash"
|
MODEL_LITE_ID = "gemini-3.8-flash"
|
||||||
|
THINKING_LEVEL_LITE = "low"
|
||||||
|
|
||||||
# Modèle pour identifier visuellement les labels
|
# Modèle pour identifier visuellement les labels
|
||||||
# MODEL_FOR_LABEL_ID = "gemini-3.5-flash-lite" # 3.5 flash lite marche plutôt mal, il insiste pour mettre les labels dans l'ordre, sans considérer ce qu'il y a écrit
|
# MODEL_FOR_LABEL_ID = "gemini-3.5-flash-lite" # 3.5 flash lite marche plutôt mal, il insiste pour mettre les labels dans l'ordre, sans considérer ce qu'il y a écrit
|
||||||
MODEL_FOR_LABEL_ID = "gemini-3.8-flash"
|
MODEL_FOR_LABEL_ID = "gemini-3.8-flash"
|
||||||
|
THINKING_LEVEL_FOR_LABEL = "low"
|
||||||
|
|
||||||
# Modèle pour des choses normales
|
# Modèle pour des choses normales
|
||||||
MODEL_FLASH_ID = "gemini-3.8-flash"
|
MODEL_FLASH_ID = "gemini-3.8-flash"
|
||||||
|
THINKING_LEVEL_FLASH = "medium"
|
||||||
|
|
||||||
# Modèle pour des choses dures
|
# Modèle pour des choses dures
|
||||||
MODEL_PRO_ID = "gemini-3.8-flash"
|
MODEL_PRO_ID = "gemini-3.8-flash"
|
||||||
|
THINKING_LEVEL_PRO = "medium"
|
||||||
# MODEL_PRO_ID = "gemini-3.1-pro-preview"
|
# MODEL_PRO_ID = "gemini-3.1-pro-preview"
|
||||||
|
|
||||||
PAGE_SPLITTER_KB = {
|
PAGE_SPLITTER_KB = {
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
from google.genai import types
|
||||||
|
|
||||||
|
from copienator import configuration, prompting
|
||||||
|
from copienator.commands import correction, gemini_for_enonce, gemini_for_labels
|
||||||
|
from copienator.gemini_config import thinking_config
|
||||||
|
|
||||||
|
|
||||||
|
class GeminiConfigTests(unittest.TestCase):
|
||||||
|
def assert_request_config(self, config, level):
|
||||||
|
data = config.model_dump(mode="json", by_alias=True, exclude_none=True)
|
||||||
|
for key in ("temperature", "topP", "topK", "thinkingBudget"):
|
||||||
|
self.assertNotIn(key, data)
|
||||||
|
self.assertNotIn(key, data.get("thinkingConfig", {}))
|
||||||
|
if level is None:
|
||||||
|
self.assertNotIn("thinkingConfig", data)
|
||||||
|
else:
|
||||||
|
self.assertEqual(data["thinkingConfig"], {"thinkingLevel": level.upper()})
|
||||||
|
|
||||||
|
def test_grading_live_and_batch_settings(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
image = Path(directory) / "answer.jpg"
|
||||||
|
image.write_bytes(b"image")
|
||||||
|
for level in ("minimal", "low", "medium", "high", None):
|
||||||
|
with self.subTest(level=level), patch.object(prompting, "make_prompt", return_value="Grade"):
|
||||||
|
_, config = prompting.generate_request(directory, image, "Ex 1", thinking_level=level)
|
||||||
|
self.assert_request_config(config, level)
|
||||||
|
batch = prompting.batch_generation_config(level)
|
||||||
|
self.assert_request_config(types.GenerateContentConfig.model_validate(batch), level)
|
||||||
|
self.assertEqual(batch["responseSchema"], prompting.UNROLLED_SCHEMA)
|
||||||
|
|
||||||
|
def test_labels_and_statement_use_their_own_settings(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
image = Path(directory) / "answer.jpg"
|
||||||
|
image.write_bytes(b"image")
|
||||||
|
with patch.object(configuration, "THINKING_LEVEL_FOR_LABEL", "low"):
|
||||||
|
_, config = gemini_for_labels.generate_request(image, "Ex 1", "Student", [], [])
|
||||||
|
self.assert_request_config(config, "low")
|
||||||
|
client = Mock()
|
||||||
|
with patch.object(configuration, "THINKING_LEVEL_LITE", "high"):
|
||||||
|
gemini_for_enonce._request_rubrics(client, "Question")
|
||||||
|
self.assert_request_config(client.models.generate_content.call_args.kwargs["config"], "high")
|
||||||
|
|
||||||
|
def test_correction_helpers_use_flash_level(self):
|
||||||
|
with patch.object(configuration, "THINKING_LEVEL_FLASH", "low"), patch.object(
|
||||||
|
prompting, "get_single_image_bytes", return_value=b"image"
|
||||||
|
):
|
||||||
|
requests = [
|
||||||
|
prompting.request_for_box_correction("answer.pdf", []),
|
||||||
|
prompting.request_for_wrong_label("answer.pdf", "Ex 1", "Question", "Ex 1"),
|
||||||
|
prompting.request_for_additional_answer("answer.pdf", "Ex 1", "Question", "Ex 1"),
|
||||||
|
]
|
||||||
|
for _, config in requests:
|
||||||
|
self.assert_request_config(config, "low")
|
||||||
|
|
||||||
|
def test_quota_fallback_switches_thinking_level_without_mutating_request(self):
|
||||||
|
client = Mock()
|
||||||
|
client.models.generate_content_stream.side_effect = [
|
||||||
|
RuntimeError("429 daily quota exhausted"), [SimpleNamespace(text="result")],
|
||||||
|
]
|
||||||
|
config = types.GenerateContentConfig(thinking_config=thinking_config("high"))
|
||||||
|
with patch.object(correction, "client", client), patch.object(
|
||||||
|
correction, "MODEL_ID_pro", "pro-model"
|
||||||
|
), patch.object(correction, "pro_quota_exhausted", False), patch.object(
|
||||||
|
configuration, "THINKING_LEVEL_FLASH", "low"
|
||||||
|
), patch.object(correction, "tprint"), patch.object(correction, "stop_requested") as stop:
|
||||||
|
stop.is_set.return_value = False
|
||||||
|
result = correction.call_gemini_with_retries("pro-model", [], config, "flash-model")
|
||||||
|
self.assertEqual(result, "result")
|
||||||
|
calls = client.models.generate_content_stream.call_args_list
|
||||||
|
self.assert_request_config(calls[0].kwargs["config"], "high")
|
||||||
|
self.assert_request_config(calls[1].kwargs["config"], "low")
|
||||||
|
self.assertEqual(calls[1].kwargs["model"], "flash-model")
|
||||||
|
self.assert_request_config(config, "high")
|
||||||
|
|
||||||
|
def test_invalid_level_fails_before_submission(self):
|
||||||
|
with self.assertRaisesRegex(ValueError, "Invalid Gemini thinking level"):
|
||||||
|
thinking_config("ultra")
|
||||||
Reference in new issue
Block a user