返回提交历史
Modified
etc/unittest/test_gemini.py
+36
-9
Modified
g4f/Provider/needs_auth/Gemini.py
+42
-28
Modified
g4f/Provider/needs_auth/gemini_utils.py
+9
-1
Modified
g4f/client/service.py
+7
-0
Modified
g4f/models.py
+21
-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
代码差异
5 个文件
+115
-39
@@ -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):
@@ -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.
@@ -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",
@@ -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
)
@@ -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):