Summary
Selecting gemini-3.1-pro always comes back labelled 3.7 Flash upstream, with zero thinking
markers in the response. slot79 (MODE_CATEGORY) in the request payload is no longer honoured by
the backend, so this project's model selection is effectively inert — every request gets the account's
default model regardless of which model the caller asked for.
The account is a paid Google AI Pro subscription, the cookie is valid, and the same conversations show
up in the Gemini web UI sidebar.
Environment
| Item |
Value |
| Base commit |
2bb988b feat(models): 新增 gemini-3.7-flash 并调整 3.6 版本描述 (#77) |
| Python |
3.11.2 |
gemini_bl |
boq_assistant-bard-web-server_20260816.12_p0, then refreshed to 20260817.02_p0 (both tested) |
auth_user |
0 |
| Endpoint |
/u/0/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate |
All logs below are redacted. Cookies, SAPISID, XSRF tokens, conversation/response IDs and account
geo context are replaced by placeholders or reduced to a byte length. gemini_bl is kept as-is
because it is a public build ID, not a credential.
Reproduction
Send a genuinely hard reasoning problem (not something answerable in one token) to gemini-3.1-pro,
then read slot42 of the final upstream frame — that field is the model name the web UI itself displays.
[05:39:33] fetching account page WIZ tokens (not a generation request)
[05:39:35] page tokens keys=['at', 'f_sid', 'pctx', 'push_id'] at=yes f_sid=yes image_model=None
[05:39:35] resolve_model -> name=gemini-3.1-pro mode(slot79)=3 think(slot17)=0 extra=None err=None
[05:39:35] gemini_bl=boq_assistant-bard-web-server_20260816.12_p0 auth_user=0 xsrf_token=present
[05:39:35] [t1_stream_pro] POST .../StreamGenerate slot79=3 slot17=0
[05:39:37] [t1_stream_pro] HTTP 200
[05:39:37] [t1_stream_pro] TTFB (first byte) = 2.01s
[05:39:51] [t1_stream_pro] TTFT (first text frame) = 16.20s
[05:39:56] [t1_stream_pro] total=21.18s raw=296210B
### t1_stream_pro model-label scan (raw 296210B)
version+tier strings: [('3.7 Flash', 36)]
tier keyword hits: [('Flash', 36)]
[05:39:56] === sleeping 200s to stay under rate limiting ===
[05:43:16] [t2_plain_pro] non-streaming _generate_raw slot79=3 slot17=0
[05:43:34] [t2_plain_pro] total=17.49s raw=179856B
### t2_plain_pro model-label scan (raw 179856B)
version+tier strings: [('3.7 Flash', 33)]
tier keyword hits: [('Flash', 33)]
Controlled experiment: 4 real upstream generations
A different question each time, ≥200s apart to avoid rate limiting.
| # |
Mode |
slot79 |
gemini_bl |
upstream slot42 |
thinking markers |
| 1 |
streaming |
3 (PRO) |
20260816.12 (stale) |
3.7 Flash |
0 |
| 2 |
non-streaming |
3 (PRO) |
20260816.12 (stale) |
3.7 Flash |
0 |
| 3 |
non-streaming |
3 (PRO) |
20260817.02 (refreshed) |
3.7 Flash |
0 |
| 4 |
non-streaming |
6 (FLASH_LITE) |
20260817.02 |
3.7 Flash |
0 |
Run 4 is the decisive one. Switching slot79 from 3 (PRO) to 6 (FLASH_LITE) — two entirely
different tiers — produced the identical label. slot79 is not participating in routing at all; the
backend just serves the account default.
Ruled out
- Stale BL — run 3 used that day's newest
20260817.02_p0. slot42 did not change.
- Invalid / ineffective cookie — the response frames carry account geo context and real
c_ / r_ conversation IDs, and the conversation is visible in the web UI. The session is authenticated.
- "TTFT < 5s therefore no thinking" is a flawed premise — measured TTFB (first byte) is 2.01s, which
is just HTTP headers plus padding; TTFT (first real text frame) is 16.20s. The latency is fine; what
is genuinely missing is thinking — 0 markers in the response, matching what the web UI shows.
Final upstream frame (redacted)
=== non-empty slots of the final frame (redacted) ===
slot[ 1] = <redacted: conversation / response IDs>
slot[ 4] = <redacted: answer body container>
slot[ 5] = <redacted: account geo / personalization context>
slot[14] = true
slot[19] = "zh"
slot[23] = true
slot[26] = [[[[null, [null, 0, "结论:存在这样的填法。", … <- text delta, truncated
slot[42] = "3.7 Flash" <- authoritative model label
slot[43] = true
slot[44] = [[[[1004, "Longer", 5, … <- length-option chips
slot[46] = 2
slot[58] = 1
slot[59] = 1
Root cause
Model selection appears to have moved from a payload slot to a request header:
x-goog-ext-525001261-jspb. It is JSPB-encoded (a protobuf message serialized as a positional JSON
array), and index 4 holds a 16-hex internal model ID:
[1,null,null,null,"<16-hex model id>",null,null,0,[4,5,6,8],null,null,<capacity>,null,null,<category>]
This repository already builds that header — _build_model_headers() / _image_model_headers() in
gemini.py — but only the image path sends it. The text path's _build_headers() sends no model
header at all and relies solely on slot79, which the backend now ignores. That also explains why image
generation does reach a non-default model while text is always pinned to the account default.
Two more observations:
- The account page enumerates the currently available model IDs as feature-flag lists
(e.g. sylssb / flag 45749278), so the valid IDs are discoverable at runtime rather than hardcoded.
- The
MODE_CATEGORY enum quoted in models.py comes from an old JS bundle (028-6eb337387583.js).
That enum is most likely no longer the routing key.
Proposed fix (not yet verified)
Send x-goog-ext-525001261-jspb on the text path too, selecting the tier by real model ID instead of
slot79.
I have not confirmed this experimentally yet. I am currently sweeping the candidate model IDs one at
a time — a different question per request, >3 minutes apart to stay clear of rate limiting — to see
whether slot42 follows the ID in the header. I will follow up in this thread with the result.
Related
Summary
Selecting
gemini-3.1-proalways comes back labelled3.7 Flashupstream, with zero thinkingmarkers in the response.
slot79(MODE_CATEGORY) in the request payload is no longer honoured bythe backend, so this project's model selection is effectively inert — every request gets the account's
default model regardless of which model the caller asked for.
The account is a paid Google AI Pro subscription, the cookie is valid, and the same conversations show
up in the Gemini web UI sidebar.
Environment
2bb988b feat(models): 新增 gemini-3.7-flash 并调整 3.6 版本描述 (#77)gemini_blboq_assistant-bard-web-server_20260816.12_p0, then refreshed to20260817.02_p0(both tested)auth_user0/u/0/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerateReproduction
Send a genuinely hard reasoning problem (not something answerable in one token) to
gemini-3.1-pro,then read
slot42of the final upstream frame — that field is the model name the web UI itself displays.Controlled experiment: 4 real upstream generations
A different question each time, ≥200s apart to avoid rate limiting.
slot79gemini_blslot423(PRO)20260816.12(stale)3.7 Flash3(PRO)20260816.12(stale)3.7 Flash3(PRO)20260817.02(refreshed)3.7 Flash6(FLASH_LITE)20260817.023.7 FlashRun 4 is the decisive one. Switching
slot79from3(PRO) to6(FLASH_LITE) — two entirelydifferent tiers — produced the identical label.
slot79is not participating in routing at all; thebackend just serves the account default.
Ruled out
20260817.02_p0.slot42did not change.c_/r_conversation IDs, and the conversation is visible in the web UI. The session is authenticated.is just HTTP headers plus padding; TTFT (first real text frame) is 16.20s. The latency is fine; what
is genuinely missing is thinking — 0 markers in the response, matching what the web UI shows.
Final upstream frame (redacted)
Root cause
Model selection appears to have moved from a payload slot to a request header:
x-goog-ext-525001261-jspb. It is JSPB-encoded (a protobuf message serialized as a positional JSONarray), and index 4 holds a 16-hex internal model ID:
This repository already builds that header —
_build_model_headers()/_image_model_headers()ingemini.py— but only the image path sends it. The text path's_build_headers()sends no modelheader at all and relies solely on
slot79, which the backend now ignores. That also explains why imagegeneration does reach a non-default model while text is always pinned to the account default.
Two more observations:
(e.g.
sylssb/ flag45749278), so the valid IDs are discoverable at runtime rather than hardcoded.MODE_CATEGORYenum quoted inmodels.pycomes from an old JS bundle (028-6eb337387583.js).That enum is most likely no longer the routing key.
Proposed fix (not yet verified)
Send
x-goog-ext-525001261-jspbon the text path too, selecting the tier by real model ID instead ofslot79.I have not confirmed this experimentally yet. I am currently sweeping the candidate model IDs one at
a time — a different question per request, >3 minutes apart to stay clear of rate limiting — to see
whether
slot42follows the ID in the header. I will follow up in this thread with the result.Related
slot79breakage was not identified at the time