XFEstudio/gpt4free
Enhance Gemini provider with cookie handling, XSRF retry, and conversation fixes (#3501)
* Gemini: cookie snapshot, XSRF retry, prompt Add robust cookie handling, XSRF retry logic and prompt normalization for the Gemini provider. - Introduce _normalize_messages and _resolve_gemini_prompt to handle prompt-only and messages inputs. - Improve unknown-model error to list supported models. - Use a request-local cookie snapshot (request_cookies) so original cookie dict isn't mutated during requests. - start_auto_refresh now accepts cookies, waits before rotating, and updates stored cookies only when appropriate. - Detect XSRF errors (_is_xsrf_error) and retry by refetching snlm0e/sid and updating params/data. - Propagate request_cookies through upload_images, image responses and Authorization generation. - Add/adjust unit tests to cover prompt-only requests, XSRF retry flow, and auto-refresh behaviors. * Update test_gemini.py * Fix Gemini conversation reuse for anonymous users Gemini returns conversation identifiers to anonymous sessions but rejects them on subsequent turns with BardErrorInfo 1096. This commit fixes the issue by discarding conversation handles for unauthenticated users and keeping full message history instead, while maintaining conversation reuse for authenticated sessions. Adds helper functions `_has_authenticated_session()` and `_resolve_gemini_conversation()` to handle the logic, and includes tests for both anonymous and authenticated conversation flows. * Add gemini-3.6 & expanded thinking support Align Gemini provider with authenticated Gemini Web behavior: switch default/aliases to gemini-3.6-flash, introduce a boolean expanded_thinking option (mapped to request field 80) instead of legacy per-depth think, and expand the request payload to 97 entries. Populate key request fields (79=model, 80=expanded, 17=turn counter, 96=first-turn flag) and wire conversation.turn_index through to requests. Add/adjust MODEL_ALIASES and EXPANDED_MODEL_ALIASES, register new models in ModelRegistry and any_model_map, and update unit tests to validate the new format and mappings.
2e84a647
代码差异
@@ -27,6 +27,7 @@ from g4f.Provider.needs_auth.Gemini import (
)
from g4f.errors import MissingAuthError, ResponseError, ResponseStatusError
from g4f.models import ModelRegistry
from g4f.providers.any_model_map import model_map
GEMINI_MODULE = importlib.import_module("g4f.Provider.needs_auth.Gemini")
@@ -83,45 +84,96 @@ class GeminiHelpersTest(unittest.TestCase):
self.assertIsNone(field_13[11])
self.assertEqual(field_13[12], 2)
def test_mode_categories_and_default_thinking_depths(self):
def test_current_models_match_authenticated_web_request_fields(self):
expected = {
"gemini-3.5-flash": (1, 4),
"gemini-3.5-flash-thinking": (2, 0),
"gemini-3.1-pro": (3, 4),
"gemini-auto": (4, 4),
"gemini-3.5-flash-thinking-lite": (5, 0),
"gemini-flash-lite": (6, 4),
"gemini-3.6-flash": (1, 1),
"gemini-3.5-flash-lite": (6, 1),
"gemini-3.1-pro": (3, 1),
}
for requested, (mode, default_think) in expected.items():
for requested, (mode, request_96) in expected.items():
with self.subTest(model=requested):
model, think = _resolve_model(requested)
model, expanded = _resolve_model(requested)
request = Gemini.build_request(
"test", "en", model, think, request_uuid="test-request"
"test", "en", model, expanded, request_uuid="test-request"
)
self.assertEqual(len(request), 97)
self.assertEqual(request[6], [1])
self.assertIsNone(request[9])
self.assertEqual(request[17], [[0]])
self.assertEqual(request[41], [1])
self.assertEqual(request[68], 2)
self.assertEqual(request[79], mode)
self.assertEqual(request[17], [[default_think]])
self.assertEqual(request[80], 1)
self.assertEqual(request[91], 0)
self.assertEqual(request[96], request_96)
def test_expanded_thinking_is_independent_for_every_current_model(self):
for requested in (
"gemini-3.6-flash",
"gemini-3.5-flash-lite",
"gemini-3.1-pro",
):
with self.subTest(model=requested):
model, _ = _resolve_model(requested)
request = Gemini.build_request(
"test", "en", model, True, request_uuid="test-request"
)
self.assertEqual(request[17], [[0]])
self.assertEqual(request[80], 2)
self.assertEqual(request[96], 1)
def test_explicit_thinking_depths_are_preserved(self):
def test_legacy_thinking_depths_map_to_binary_expanded_option(self):
for requested_depth in range(5):
with self.subTest(depth=requested_depth):
model, think = _resolve_model(
model, expanded = _resolve_model(
f"gemini-3.5-flash-thinking@think={requested_depth}"
)
self.assertEqual(model, "gemini-3.6-flash")
self.assertEqual(expanded, requested_depth <= 2)
def test_conversation_turn_counter_uses_request_field_17(self):
for model, expanded in (
("gemini-3.6-flash", False),
("gemini-3.6-flash", True),
("gemini-3.5-flash-lite", False),
("gemini-3.1-pro", False),
):
with self.subTest(model=model, expanded=expanded):
conversation = Conversation(
"conversation-id",
"response-id",
"choice-id",
model,
turn_index=2,
)
request = Gemini.build_request(
"test", "en", model, think, request_uuid="test-request"
"test",
"en",
model,
expanded,
conversation=conversation,
request_uuid="test-request",
)
self.assertEqual(request[17], [[requested_depth]])
self.assertEqual(request[17], [[2]])
self.assertEqual(request[96], 0)
def test_legacy_model_names_resolve_to_current_modes(self):
expected = {
"gemini-2.0": "gemini-3.5-flash",
"gemini-2.0-flash": "gemini-3.5-flash",
"gemini-2.0-flash-thinking": "gemini-3.5-flash-thinking",
"gemini-2.0-flash-thinking-with-apps": "gemini-3.5-flash-thinking",
"gemini-2.5-flash": "gemini-3.5-flash",
"gemini-2.0": "gemini-3.6-flash",
"gemini-2.0-flash": "gemini-3.6-flash",
"gemini-2.0-flash-thinking": "gemini-3.6-flash",
"gemini-2.0-flash-thinking-with-apps": "gemini-3.6-flash",
"gemini-2.5-flash": "gemini-3.6-flash",
"gemini-3.5-flash": "gemini-3.6-flash",
"gemini-3.5-flash-thinking": "gemini-3.6-flash",
"gemini-auto": "gemini-3.6-flash",
"gemini-2.5-pro": "gemini-3.1-pro",
"gemini-3.1-flash-lite": "gemini-flash-lite",
"gemini-3.1-flash-lite": "gemini-3.5-flash-lite",
"gemini-flash-lite": "gemini-3.5-flash-lite",
"gemini-3.5-flash-thinking-lite": "gemini-3.5-flash-lite",
}
for legacy_name, current_name in expected.items():
@@ -131,16 +183,20 @@ class GeminiHelpersTest(unittest.TestCase):
def test_unknown_model_lists_supported_models(self):
with self.assertRaises(ValueError) as context:
_resolve_model("gemini-3.6-flash")
_resolve_model("gemini-3.7-flash")
message = str(context.exception)
self.assertIn("Unknown Gemini model: gemini-3.6-flash", message)
self.assertIn("Unknown Gemini model: gemini-3.7-flash", message)
self.assertIn("Supported models:", message)
self.assertIn("gemini-3.5-flash", message)
self.assertIn("gemini-3.6-flash", message)
self.assertIn("gemini-3.5-flash-lite", message)
def test_public_model_registry_exposes_current_models(self):
self.assertEqual(ModelRegistry.get("gemini-auto").name, "gemini-auto")
for model in (
"gemini-3.6-flash",
"gemini-3.5-flash-lite",
"gemini-3.1-pro",
"gemini-3.5-flash-thinking",
"gemini-auto",
"gemini-3.5-flash-thinking-lite",
@@ -148,6 +204,7 @@ class GeminiHelpersTest(unittest.TestCase):
):
with self.subTest(model=model):
self.assertEqual(ModelRegistry.get(model).name, model)
self.assertEqual(model_map[model]["Gemini"], model)
def test_prompt_only_requests_accept_missing_messages(self):
messages = _normalize_messages(None)
@@ -170,21 +227,21 @@ class GeminiHelpersTest(unittest.TestCase):
"conversation-id",
"response-id",
"choice-id",
"gemini-auto",
"gemini-3.6-flash",
)
self.assertFalse(_has_authenticated_session({}))
resolved = _resolve_gemini_conversation(
conversation,
"gemini-auto",
"gemini-3.6-flash",
{},
)
prompt = _resolve_gemini_prompt(messages, None, resolved)
request = Gemini.build_request(
prompt,
"en",
"gemini-auto",
4,
"gemini-3.6-flash",
False,
conversation=resolved,
request_uuid="test-request",
)
@@ -216,7 +273,7 @@ class GeminiHelpersTest(unittest.TestCase):
self.assertIsNone(
_resolve_gemini_conversation(
conversation,
"gemini-auto",
"gemini-3.1-pro",
cookies,
)
)
@@ -254,6 +311,7 @@ class GeminiHelpersTest(unittest.TestCase):
with self.assertRaises(MissingAuthError):
ProbeGemini.validate_model_access("gemini-3.1-pro")
ProbeGemini.validate_model_access("gemini-3.6-flash")
ProbeGemini.validate_model_access("gemini-3.5-flash")
ProbeGemini.validate_model_access(
"gemini-3.1-pro", allow_model_fallback=True
@@ -382,10 +440,11 @@ class GeminiStreamTest(unittest.IsolatedAsyncioTestCase):
side_effect=check_status,
):
generator = ProbeGemini.create_async_generator(
model="gemini-3.5-flash",
model="gemini-3.6-flash",
messages=None,
prompt="prompt-only request",
cookies=source_cookies,
expanded_thinking=True,
max_retries=1,
)
await generator.__anext__()
@@ -408,6 +467,9 @@ class GeminiStreamTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(source_cookies["__Secure-1PSIDTS"], "old")
request = json.loads(json.loads(session.calls[1]["data"]["f.req"])[1])
self.assertEqual(request[0][0], "prompt-only request")
self.assertEqual(request[79], 1)
self.assertEqual(request[80], 2)
self.assertEqual(request[96], 1)
async def test_auto_refresh_waits_before_rotating(self):
with patch.object(
@@ -126,21 +126,35 @@ MAX_CONCURRENT_UPLOADS = 4
RETRYABLE_STATUS_CODES = {408, 425, 429, 500, 502, 503, 504}
models = {
"gemini-3.5-flash": {"mode": 1, "think": 4},
"gemini-3.5-flash-thinking": {"mode": 2, "think": 0},
"gemini-3.1-pro": {"mode": 3, "think": 4},
"gemini-auto": {"mode": 4, "think": 4},
"gemini-3.5-flash-thinking-lite": {"mode": 5, "think": 0},
"gemini-flash-lite": {"mode": 6, "think": 4},
# Values captured from authenticated Gemini Web requests. The model is
# selected by field 79; expanded thinking is an independent field 80.
"gemini-3.6-flash": {"mode": 1},
"gemini-3.5-flash-lite": {"mode": 6},
"gemini-3.1-pro": {"mode": 3},
}
MODEL_ALIASES = {
"gemini-2.0": "gemini-3.5-flash",
"gemini-2.0-flash": "gemini-3.5-flash",
"gemini-2.0-flash-thinking": "gemini-3.5-flash-thinking",
"gemini-2.0-flash-thinking-with-apps": "gemini-3.5-flash-thinking",
"gemini-2.5-flash": "gemini-3.5-flash",
"gemini-2.0": "gemini-3.6-flash",
"gemini-2.0-flash": "gemini-3.6-flash",
"gemini-2.0-flash-thinking": "gemini-3.6-flash",
"gemini-2.0-flash-thinking-with-apps": "gemini-3.6-flash",
"gemini-2.5-flash": "gemini-3.6-flash",
"gemini-2.5-pro": "gemini-3.1-pro",
"gemini-3.1-flash-lite": "gemini-flash-lite",
"gemini-3.1-flash-lite": "gemini-3.5-flash-lite",
"gemini-3.5-flash": "gemini-3.6-flash",
"gemini-3.5-flash-thinking": "gemini-3.6-flash",
"gemini-3.6-flash-thinking": "gemini-3.6-flash",
"gemini-auto": "gemini-3.6-flash",
"gemini-3.5-flash-thinking-lite": "gemini-3.5-flash-lite",
"gemini-3.5-flash-lite-thinking": "gemini-3.5-flash-lite",
"gemini-flash-lite": "gemini-3.5-flash-lite",
}
EXPANDED_MODEL_ALIASES = {
"gemini-2.0-flash-thinking",
"gemini-2.0-flash-thinking-with-apps",
"gemini-3.5-flash-thinking",
"gemini-3.6-flash-thinking",
"gemini-3.5-flash-thinking-lite",
"gemini-3.5-flash-lite-thinking",
}
@@ -223,10 +237,12 @@ async def _iter_response_lines(
yield buffer.decode("utf-8", errors="replace")
def _resolve_model(model: str, think_override: int = None) -> tuple[str, int]:
def _resolve_model(model: str, think_override: int = None) -> tuple[str, bool]:
requested_model = model
think_mode = think_override
if "@think=" in model:
model, think_value = model.rsplit("@think=", 1)
requested_model = model
try:
think_mode = int(think_value)
except ValueError as exc:
@@ -240,7 +256,12 @@ def _resolve_model(model: str, think_override: int = None) -> tuple[str, int]:
f"Unknown Gemini model: {model}. "
f"Supported models: {', '.join(models)}"
)
return model, models[model]["think"] if think_mode is None else think_mode
expanded_thinking = (
requested_model in EXPANDED_MODEL_ALIASES
if think_mode is None
else think_mode <= 2
)
return model, expanded_thinking
def _normalize_messages(messages: Messages | None) -> Messages:
@@ -276,7 +297,9 @@ def _resolve_gemini_conversation(
):
if conversation is None:
return None
if getattr(conversation, "model", None) != model:
model = MODEL_ALIASES.get(model, model)
conversation_model = getattr(conversation, "model", None)
if MODEL_ALIASES.get(conversation_model, conversation_model) != model:
return None
# Gemini currently returns conversation identifiers to anonymous sessions,
# but rejects those identifiers with BardErrorInfo 1096 when they are used
@@ -299,7 +322,7 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
active_by_default = True
use_nodriver = True
default_model = "gemini-3.5-flash"
default_model = "gemini-3.6-flash"
default_image_model = default_model
default_vision_model = default_model
image_models = [default_image_model]
@@ -442,9 +465,8 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
@classmethod
def get_model_headers(cls, model: str) -> dict[str, str]:
family = MODEL_FAMILIES.get(model)
# The 80-field request's mode category selects Flash/Thinking/Lite.
# Pro additionally needs the account-specific model header or Google
# silently routes it back to Flash.
# Request field 79 selects the model. Pro additionally needs the
# account-specific model header or Google silently routes it to Flash.
if family != "pro":
return {}
for model_data in cls._account_models.values():
@@ -458,6 +480,7 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
model: str,
allow_model_fallback: bool = False,
) -> None:
model = MODEL_ALIASES.get(model, model)
if allow_model_fallback or cls._account_status is None:
return
if cls._account_status == ACCOUNT_STATUS_UNAUTHENTICATED:
@@ -508,12 +531,11 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
audio: dict = None,
auth_user: int | str = None,
think_override: int = None,
expanded_thinking: bool = None,
**kwargs
) -> AsyncResult:
messages = _normalize_messages(messages)
model = model or cls.default_model
if cls.model_aliases and model in cls.model_aliases:
model = cls.model_aliases[model]
if audio is not None or model == "gemini-audio":
prompt = format_media_prompt(messages, prompt)
filename = get_filename(["gemini"], prompt, ".ogx", prompt)
@@ -524,7 +546,11 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
f.write(chunk)
yield AudioResponse(f"/media/{filename}", text=prompt)
return
if think_override is None:
if expanded_thinking is not None:
if not isinstance(expanded_thinking, bool):
raise TypeError("expanded_thinking must be a boolean")
think_override = 0 if expanded_thinking else 4
elif think_override is None:
think_override = {
"none": 4,
"minimal": 4,
@@ -533,7 +559,7 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
"high": 1,
"xhigh": 0,
}.get(kwargs.get("reasoning_effort"))
model, think_mode = _resolve_model(model, think_override)
model, expanded_thinking = _resolve_model(model, think_override)
if cookies is not None:
cls._cookies = cookies
elif cls._cookies is None:
@@ -631,7 +657,7 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
'f.req': json.dumps([None, json.dumps(cls.build_request(
prompt,
model=model,
think_mode=think_mode,
expanded_thinking=expanded_thinking,
language=language,
conversation=conversation,
uploads=uploads,
@@ -760,6 +786,11 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
response_part[1][1],
response_part[4][0][0],
model,
turn_index=(
getattr(conversation, "turn_index", 0) + 1
if conversation is not None
else 1
),
)
except (IndexError, TypeError):
pass
@@ -896,14 +927,19 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
prompt: str,
language: str,
model: str,
think_mode: int,
expanded_thinking: bool = False,
conversation: Conversation = None,
uploads: list[list[str, str]] = None,
tools: list[list[str]] = None,
request_uuid: str = None,
) -> list:
image_list = [[[image_url, 1], image_name] for image_url, image_name in uploads] if uploads else []
request = [None] * 80
turn_index = (
getattr(conversation, "turn_index", 0)
if conversation is not None
else 0
)
request = [None] * 97
request[0] = [prompt, 0, None, image_list, None, None, 0]
request[1] = [language]
request[2] = [
@@ -918,21 +954,26 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
None,
"",
]
request[6] = [0]
request[6] = [1]
request[7] = 1
request[9] = tools or []
if tools:
request[9] = tools
request[10] = 1
request[11] = 0
request[17] = [[think_mode]]
request[17] = [[turn_index]]
request[18] = 0
request[27] = 1
request[30] = [4]
request[41] = [2]
request[41] = [1]
request[53] = 0
request[59] = request_uuid or str(uuid.uuid4())
request[61] = []
request[68] = 1
request[68] = 2
request[79] = models[model]["mode"]
request[80] = 2 if expanded_thinking else 1
request[91] = 0
# Gemini Web marks the first turn with 1 and follow-up turns with 0.
request[96] = int(conversation is None)
return request
@classmethod
@@ -1038,12 +1079,14 @@ class Conversation(JsonConversation):
conversation_id: str,
response_id: str,
choice_id: str,
model: str
model: str,
turn_index: int = 0,
) -> None:
self.conversation_id = conversation_id
self.response_id = response_id
self.choice_id = choice_id
self.model = model
self.turn_index = turn_index
async def iter_filter_base64(chunks: AsyncIterator[bytes]) -> AsyncIterator[bytes]:
@@ -16,11 +16,11 @@ MODEL_HEADER_AUXILIARY = {
"x-goog-ext-73010990-jspb": "[0]",
}
MODEL_FAMILIES = {
"gemini-3.5-flash": "flash",
"gemini-3.5-flash-thinking": "thinking",
"gemini-3.6-flash": "flash",
"gemini-3.5-flash-lite": "flash",
"gemini-3.1-pro": "pro",
}
ANONYMOUS_MODELS = {"gemini-3.5-flash", "gemini-auto"}
ANONYMOUS_MODELS = {"gemini-3.6-flash", "gemini-3.5-flash-lite"}
KNOWN_MODEL_IDS = {
"fbb127bbb056c959": "flash",
"5bf011840784117a": "thinking",
@@ -424,6 +424,18 @@ gemini_3_1_flash_lite = Model(
best_provider = "Gemini"
)
gemini_3_6_flash = Model(
name = 'gemini-3.6-flash',
base_provider = 'Google',
best_provider = "Gemini"
)
gemini_3_5_flash_lite = Model(
name = 'gemini-3.5-flash-lite',
base_provider = 'Google',
best_provider = "Gemini"
)
gemini_3_5_flash = Model(
name = 'gemini-3.5-flash',
base_provider = 'Google',
@@ -311,6 +311,12 @@ model_map = {
"Pollinations": "gemini",
"Puter": "openrouter:google/gemini-3.5-flash"
},
"gemini-3.6-flash": {
"Gemini": "gemini-3.6-flash"
},
"gemini-3.5-flash-lite": {
"Gemini": "gemini-3.5-flash-lite"
},
"gemini-3.5-flash-thinking": {
"Gemini": "gemini-3.5-flash-thinking",
"GeminiPro": "gemini-3.5-flash-thinking"