XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 1
返回提交历史

XFEstudio/gpt4free

fix(gemini): preserve model identity and decouple capability mode resolution (#3518)

Stop falling back or rewriting unknown Gemini model names to older models like gemini-3.6-flash. Dynamically determine protocol execution modes and family headers without mutating model string. Closes #3518.

64c85669
Anand Mall <anand@example.com>
提交于

代码差异

5 个文件 +115 -39
Modified etc/unittest/test_gemini.py +36 -9
@@ -181,22 +181,38 @@ class GeminiHelpersTest(unittest.TestCase):
181 181 resolved, _ = _resolve_model(legacy_name)
182 182 self.assertEqual(resolved, current_name)
183 183
184 def test_unknown_model_falls_back_to_known_model(self):
185 # New Gemini models ship faster than the hard-coded registry can track
186 # them. Unknown names route to the closest known model of the same
187 # family instead of raising, so the provider no longer returns a 500.
188 self.assertEqual(_resolve_model("gemini-3.7-flash")[0], "gemini-3.6-flash")
189 self.assertEqual(_resolve_model("gemini-3.8-flash")[0], "gemini-3.6-flash")
190 self.assertEqual(_resolve_model("gemini-3.8-pro")[0], "gemini-3.1-pro")
184 def test_unknown_model_preserves_exact_name(self):
185 # Resolving unknown model names preserves their exact identity
186 # instead of categorizing/routing them to an older model of the family (#3518).
187 self.assertEqual(_resolve_model("gemini-3.7-flash")[0], "gemini-3.7-flash")
188 self.assertEqual(_resolve_model("gemini-3.8-flash")[0], "gemini-3.8-flash")
189 self.assertEqual(_resolve_model("gemini-3.8-pro")[0], "gemini-3.8-pro")
191 190 self.assertEqual(
192 _resolve_model("gemini-3.9-flash-lite")[0], "gemini-3.5-flash-lite"
191 _resolve_model("gemini-3.9-flash-lite")[0], "gemini-3.9-flash-lite"
193 192 )
193 self.assertEqual(_resolve_model("gemini-4.0-flash")[0], "gemini-4.0-flash")
194 194
195 195 def test_unknown_thinking_model_enables_expanded_thinking(self):
196 196 model, expanded = _resolve_model("gemini-3.8-flash-thinking")
197 self.assertEqual(model, "gemini-3.6-flash")
197 self.assertEqual(model, "gemini-3.8-flash")
198 198 self.assertTrue(expanded)
199 199
200 def test_unknown_models_dynamically_determine_request_mode(self):
201 self.assertEqual(Gemini.get_model_mode("gemini-3.7-flash"), 1)
202 self.assertEqual(Gemini.get_model_mode("gemini-3.8-flash"), 1)
203 self.assertEqual(Gemini.get_model_mode("gemini-3.8-pro"), 3)
204 self.assertEqual(Gemini.get_model_mode("gemini-3.9-flash-lite"), 6)
205 self.assertEqual(Gemini.get_model_mode("gemini-4.0-flash"), 1)
206
207 req_flash = Gemini.build_request("test", "en", "gemini-3.8-flash")
208 self.assertEqual(req_flash[79], 1)
209
210 req_pro = Gemini.build_request("test", "en", "gemini-3.8-pro")
211 self.assertEqual(req_pro[79], 3)
212
213 req_lite = Gemini.build_request("test", "en", "gemini-3.9-flash-lite")
214 self.assertEqual(req_lite[79], 6)
215
200 216 def test_invalid_thinking_mode_still_raises(self):
201 217 with self.assertRaises(ValueError):
202 218 _resolve_model("gemini-3.6-flash@think=abc")
@@ -319,9 +335,15 @@ class GeminiHelpersTest(unittest.TestCase):
319 335
320 336 with self.assertRaises(MissingAuthError):
321 337 ProbeGemini.validate_model_access("gemini-3.1-pro")
338 with self.assertRaises(MissingAuthError):
339 ProbeGemini.validate_model_access("gemini-3.8-pro")
322 340 ProbeGemini.validate_model_access("gemini-3.6-flash")
323 341 ProbeGemini.validate_model_access("gemini-3.5-flash")
342 ProbeGemini.validate_model_access("gemini-3.7-flash")
343 ProbeGemini.validate_model_access("gemini-3.8-flash")
344 ProbeGemini.validate_model_access("gemini-4.0-flash")
324 345 ProbeGemini.validate_model_access("gemini-3.1-pro", allow_model_fallback=True)
346 ProbeGemini.validate_model_access("gemini-3.8-pro", allow_model_fallback=True)
325 347
326 348 def test_dynamic_headers_only_for_available_pro(self):
327 349 _, registry = build_account_response(ACCOUNT_STATUS_AVAILABLE)
@@ -334,8 +356,13 @@ class GeminiHelpersTest(unittest.TestCase):
334 356 ProbeGemini.get_model_headers("gemini-3.1-pro")[MODEL_HEADER_KEY]
335 357 )
336 358 self.assertEqual(pro_header[4], "9d8ca3786ebdfbea")
359 pro_header_38 = json.loads(
360 ProbeGemini.get_model_headers("gemini-3.8-pro")[MODEL_HEADER_KEY]
361 )
362 self.assertEqual(pro_header_38[4], "9d8ca3786ebdfbea")
337 363 self.assertEqual(ProbeGemini.get_model_headers("gemini-3.5-flash"), {})
338 364 self.assertEqual(ProbeGemini.get_model_headers("gemini-3.5-flash-thinking"), {})
365 self.assertEqual(ProbeGemini.get_model_headers("gemini-3.8-flash"), {})
339 366
340 367
341 368 class GeminiStreamTest(unittest.IsolatedAsyncioTestCase):
Modified g4f/Provider/needs_auth/Gemini.py +42 -28
@@ -133,6 +133,9 @@ models = {
133 133 "gemini-3.6-flash": {"mode": 1},
134 134 "gemini-3.5-flash-lite": {"mode": 6},
135 135 "gemini-3.1-pro": {"mode": 3},
136 "gemini-3.7-flash": {"mode": 1},
137 "gemini-3.8-flash": {"mode": 1},
138 "gemini-3.8-pro": {"mode": 3},
136 139 }
137 140 MODEL_ALIASES = {
138 141 "gemini-2.0": "gemini-3.6-flash",
@@ -240,18 +243,6 @@ async def _iter_response_lines(
240 243 yield buffer.decode("utf-8", errors="replace")
241 244
242 245
243 def _fallback_model(requested_model: str) -> str:
244 # Google ships new Gemini models faster than the hard-coded registry can
245 # track them. Route an unknown name to the closest known model of the same
246 # family so the request keeps working until the registry is refreshed.
247 name = requested_model.lower()
248 if "pro" in name:
249 return "gemini-3.1-pro"
250 if "lite" in name:
251 return "gemini-3.5-flash-lite"
252 return "gemini-3.6-flash"
253
254
255 246 def _resolve_model(model: str, think_override: int = None) -> tuple[str, bool]:
256 247 requested_model = model
257 248 think_mode = think_override
@@ -265,15 +256,7 @@ def _resolve_model(model: str, think_override: int = None) -> tuple[str, bool]:
265 256 if think_mode is not None:
266 257 if not isinstance(think_mode, int) or not 0 <= think_mode <= 4:
267 258 raise ValueError("Thinking mode must be an integer between 0 and 4")
268 model = MODEL_ALIASES.get(model, model)
269 if model not in models:
270 fallback = _fallback_model(requested_model)
271 debug.log(
272 f"Unknown Gemini model: {model!r}. "
273 f"Falling back to {fallback!r}. "
274 f"Known models: {', '.join(models)}"
275 )
276 model = fallback
259
277 260 if think_mode is None:
278 261 expanded_thinking = (
279 262 requested_model in EXPANDED_MODEL_ALIASES
@@ -281,6 +264,16 @@ def _resolve_model(model: str, think_override: int = None) -> tuple[str, bool]:
281 264 )
282 265 else:
283 266 expanded_thinking = think_mode <= 2
267
268 # Check explicit model aliases (e.g. gemini-auto -> gemini-3.6-flash)
269 if model in MODEL_ALIASES:
270 model = MODEL_ALIASES[model]
271 elif expanded_thinking and "thinking" in model.lower():
272 cleaned = re.sub(r"-thinking(-with-apps)?", "", model, flags=re.IGNORECASE)
273 cleaned = re.sub(r"-lite-thinking", "-lite", cleaned, flags=re.IGNORECASE)
274 cleaned = re.sub(r"-thinking-lite", "-lite", cleaned, flags=re.IGNORECASE)
275 model = MODEL_ALIASES.get(cleaned, cleaned)
276
284 277 return model, expanded_thinking
285 278
286 279
@@ -480,11 +473,35 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
480 473 cls._account_models = registry
481 474 cls._account_models_fetched_at = time.time()
482 475
476 @classmethod
477 def get_model_mode(cls, model: str) -> int:
478 if model in cls._account_models:
479 return cls._account_models[model]["mode"]
480 if model in models:
481 return models[model]["mode"]
482 name = model.lower()
483 if "pro" in name:
484 return 3
485 if "lite" in name:
486 return 6
487 return 1
488
489 @classmethod
490 def get_model_family(cls, model: str) -> str:
491 if model in cls._account_models:
492 return cls._account_models[model].get("family", "flash")
493 if model in MODEL_FAMILIES:
494 return MODEL_FAMILIES[model]
495 name = model.lower()
496 if "pro" in name:
497 return "pro"
498 return "flash"
499
483 500 @classmethod
484 501 def get_model_headers(cls, model: str) -> dict[str, str]:
485 502 if model in cls._account_models:
486 503 return cls._account_models[model].get("headers", {})
487 family = MODEL_FAMILIES.get(model)
504 family = cls.get_model_family(model)
488 505 # Request field 79 selects the model. Pro additionally needs the
489 506 # account-specific model header or Google silently routes it to Flash.
490 507 if family != "pro":
@@ -505,8 +522,9 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
505 522 model = MODEL_ALIASES.get(model, model)
506 523 if allow_model_fallback or cls._account_status is None:
507 524 return
525 family = cls.get_model_family(model)
508 526 if cls._account_status == ACCOUNT_STATUS_UNAUTHENTICATED:
509 if model not in ANONYMOUS_MODELS:
527 if model not in ANONYMOUS_MODELS and family == "pro":
510 528 raise MissingAuthError(
511 529 f"Gemini session is unauthenticated; model {model!r} would fall back to Flash"
512 530 )
@@ -515,7 +533,6 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
515 533 raise ResponseError(
516 534 f"Gemini account is unavailable (status {cls._account_status})"
517 535 )
518 family = MODEL_FAMILIES.get(model)
519 536 if family != "pro" or not cls._account_models:
520 537 return
521 538 if not any(
@@ -1101,10 +1118,7 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
1101 1118 request[59] = request_uuid or str(uuid.uuid4())
1102 1119 request[61] = []
1103 1120 request[68] = 2
1104 if model in cls._account_models:
1105 request[79] = cls._account_models[model]["mode"]
1106 else:
1107 request[79] = models[model]["mode"]
1121 request[79] = cls.get_model_mode(model)
1108 1122 request[80] = 2 if expanded_thinking else 1
1109 1123 request[91] = 0
1110 1124 # Gemini Web marks the first turn with 1 and follow-up turns with 0.
Modified g4f/Provider/needs_auth/gemini_utils.py +9 -1
@@ -19,8 +19,16 @@ MODEL_FAMILIES = {
19 19 "gemini-3.6-flash": "flash",
20 20 "gemini-3.5-flash-lite": "flash",
21 21 "gemini-3.1-pro": "pro",
22 "gemini-3.7-flash": "flash",
23 "gemini-3.8-flash": "flash",
24 "gemini-3.8-pro": "pro",
25 }
26 ANONYMOUS_MODELS = {
27 "gemini-3.6-flash",
28 "gemini-3.5-flash-lite",
29 "gemini-3.7-flash",
30 "gemini-3.8-flash",
22 31 }
23 ANONYMOUS_MODELS = {"gemini-3.6-flash", "gemini-3.5-flash-lite"}
24 32 KNOWN_MODEL_IDS = {
25 33 "fbb127bbb056c959": "flash",
26 34 "5bf011840784117a": "thinking",
Modified g4f/client/service.py +7 -0
@@ -91,6 +91,10 @@ def get_model_and_provider(
91 91 if isinstance(model, str):
92 92 if model in ModelUtils.convert:
93 93 model = ModelUtils.convert[model]
94 else:
95 dynamic_model = ModelUtils.get_model(model)
96 if dynamic_model is not None:
97 model = dynamic_model
94 98
95 99 if not model:
96 100 if has_images:
@@ -112,6 +116,9 @@ def get_model_and_provider(
112 116 if not provider:
113 117 raise ProviderNotFoundError(f"No provider found for model: {model}")
114 118
119 if isinstance(provider, str):
120 provider = convert_to_provider(provider)
121
115 122 provider_name = (
116 123 provider.__name__ if hasattr(provider, "__name__") else type(provider).__name__
117 124 )
Modified g4f/models.py +21 -1
@@ -386,6 +386,18 @@ gemini_3_6_flash = Model(
386 386 name="gemini-3.6-flash", base_provider="Google", best_provider="Gemini"
387 387 )
388 388
389 gemini_3_7_flash = Model(
390 name="gemini-3.7-flash", base_provider="Google", best_provider="Gemini"
391 )
392
393 gemini_3_8_flash = Model(
394 name="gemini-3.8-flash", base_provider="Google", best_provider="Gemini"
395 )
396
397 gemini_3_8_pro = Model(
398 name="gemini-3.8-pro", base_provider="Google", best_provider="Gemini"
399 )
400
389 401 gemini_3_5_flash_lite = Model(
390 402 name="gemini-3.5-flash-lite", base_provider="Google", best_provider="Gemini"
391 403 )
@@ -586,7 +598,15 @@ class ModelUtils:
586 598 @classmethod
587 599 def get_model(cls, name: str) -> Optional[Model]:
588 600 """Get model by name or alias"""
589 return ModelRegistry.get(name)
601 model = ModelRegistry.get(name)
602 if model is None and isinstance(name, str) and name.startswith("gemini-"):
603 model = Model(
604 name=name,
605 base_provider="Google",
606 best_provider=IterListProvider(["Gemini", "GeminiPro", "GeminiCLI"]),
607 )
608 cls.refresh()
609 return model
590 610
591 611 @classmethod
592 612 def register_alias(cls, alias: str, model_name: str):