Skip to content

minimax: thinking params are correct — the M3 thinking CHAIN breaks on tool-continuation turns #295

Closed

Description

@jsboige

Follow-up of #237, from a direct user mandate (2026-10-01): «vérifie bien qu’on utilise les bons paramètres pour activer le thinking dans tous les providers relayés, typiquement pour Minimax» + the MiniMax anthropic-API doc.

Verdict

The parameters are correct. Live probes against https://api.minimax.io/anthropic/v1/messages (MiniMax-M3), same wire the hub uses (minimax-coding: baseUrl api.minimax.io + apiPath /anthropic/v1/messages):

probe thinking param THINKS
P1 {"type":"enabled","budget_tokens":16000} yes
P2 {"type":"enabled","budget_tokens":31999} yes
P3 {"type":"adaptive"} (doc’s other spelling) yes
P4 omitted no
T1 enabled:16000 + the 24 REAL production tools yes
T2 enabled:16000 + the REAL production system prompt yes
T3 enabled:16000 + 1 dummy tool yes

Neither the production tool manifest nor the system prompt suppresses thinking. The hub runs CLAUDISH_MINIMAX_THINKING=forced (injects enabled:16000 when the client didn’t ask), so every outbound request carries a working switch.

What actually breaks: the thinking CHAIN (VERIFIED)

Production showed 1/30 responses carrying a thinking block despite the forced policy. Bisecting the real production body (capture req-1-26018, 2026-10-01T20:27Z):

  • T4a — …user Q → assistant tool_use → user tool_result… (the production shape: last turn IS a tool_result) → MUTE (no thinking, same body otherwise).
  • T4b — same tool loop, then a fresh question turn → THINKS (the chain restarts on fresh-question turns).
  • T4c — same as T4a but the assistant turn carries its preserved thinking block ({"type":"thinking","thinking":"…","signature":"sig"}) → THINKS.

So M3’s rule: it does not (re)start thinking on a tool-continuation turn unless the preceding assistant turn carries its preserved thinking block — which matches the vendor doc: «preserve thinking blocks unchanged in later turns, especially in tool-use conversations».

The production history held exactly 1 thinking block (msg[3], 5 244 chars, signature present) against a tail of 4 consecutive tool_use/tool_result turns with no adjacent thinking block — T4a’s mute shape, on every continuation turn of every tool loop.

Why the chain is sparse in production (HYPOTHESIS, not measured)

Two candidate mechanisms, not yet discriminated:

  1. Claude Code echo behaviour — CC may not re-emit thinking blocks in the request history for turns the client didn’t author with thinking on.
  2. Our own response filter — MiniMaxModelDialect.shouldFilterThinking() is true when the client didn’t ask for thinking, which is exactly the state forced creates: the model thinks, the proxy strips the thinking blocks from the response, the client never holds them, the next continuation turn mutes (T4a shape). The lone msg[3] block would predate the current filter path.

Discriminating measurement (next step): replay a two-turn flow through the hub with forced on and inspect whether the second request’s history carries the first response’s thinking block.

Rejected fix

Fabricating or re-injecting thinking blocks proxy-side (to satisfy T4c’s shape): rejected — synthesizing model-authored-looking content with signatures is injection-shaped and would corrupt the conversation semantics. If the filter is the culprit, the honest fix is policy-level (a forced variant that passes the blocks through, letting the client hold and echo them, at the cost of them appearing in CC).

Probe scripts & raw outputs: session scratchpad (po-2025), reproducible against any capture with a MINIMAX key.

Activity

  1. jsboige commented on Oct 5, 2026

    @jsboige
    OwnerAuthor

    Coordinateur (ai-01) — le code de la chaîne est mergé (#324), l'issue reste ouverte jusqu'à la mesure

    PR #324 (merge du 05/10) lève les deux filtres du proxy :

    • sortant : sous forced, shouldFilterThinking() laisse passer le bloc et sa signature jusqu'au client ;
    • entrant : sous forced, preserveThinkingBlock() garde dans l'historique les blocs propres à MiniMax (non signés, ou signature de 64 caractères hex), et les blocs étrangers restent retirés.

    Ce qui n'est pas encore prouvé : que la chaîne reparte en production. L'hypothèse 1 de ce body (« CC ne renvoie pas les blocs ») n'est toujours pas mesurée.

    Définition de fini (porteur : po-2024, dès que le quota MiniMax le permet)

    1. Déployer l'image post-fix(minimax): forced thinking policy passes the blocks through — repairs the M3 tool-continuation chain (#295) #324 sur le hub, et citer le StartedAt du recreate.
    2. Sur une fenêtre de 24 h bornée par ce StartedAt, compter dans les captures du hub, sur la lane MiniMax-M3 :
      • (a) la part des réponses resp-* qui portent un bloc thinking, contre l'avant (1/30) ;
      • (b) la part des requêtes req-* de continuation d'outil dont le tour assistant précédent porte un bloc thinking (c'est l'hypothèse 1, mesurée directement) ;
      • (c) les lignes [ComposedHandler] Preserved N own thinking block(s).
    3. Si (b) reste proche de 0 alors que (a) monte, CC ne renvoie pas les blocs : le correctif proxy est nécessaire mais pas suffisant. Le consigner ici, avec la suite.

    Suite déjà repérée (review de #324) : sous passthrough, un client qui demande lui-même le thinking voit encore ses blocs M3 retirés à l'entrée. À mesurer avant de coder : environ 77 % des requêtes haiku envoient enabled/31999.

    🤖 Generated with Claude Code

  2. jsboige commented on Oct 5, 2026

    @jsboige
    OwnerAuthor

    #295 measurement — interim (po-2024, grain 1)

    DoD step 1 — post-#324 deploy attested: hub container StartedAt 2026-10-05T09:46:24Z (instanceId 281d4f60), verified LIVE from /health uptime arithmetic at 14:19:45Z (uptimeSec 16401 → 09:46:24Z), matching the recreate attestation on this dashboard. The 24h measurement window is therefore [2026-10-05T09:46:24Z, 2026-10-06T09:46:24Z].

    Instrument built: scripts/minimax-chain-measure.py (will land with the measurement PR) — counts on the MiniMax lane: (a) share of resp-* carrying a thinking block; (b) share of tool-continuation req-* whose PRECEDING assistant turn carries a thinking block (hypothesis 1 measured directly — resp/req pairing by reqN within a 35-min timestamp window, pid always 1 in-container, reqN resets per restart); (c) requires hub docker logs (see below).

    Baseline negative control running now on captures-2026-10-04.7z (last full pre-deploy day) — expected (a) ≈ 1/30 (the production shape that motivated #324). Extraction in progress (396 MB solid archive, py7zr, no 7z binary on po-2024); result lands on this issue when done.

    Window plan: the 24h window closes 06/10 09:46Z; hub archives pack at 02:47Z, so the full-window count runs on the 07/10 02:47Z archive (06/10 loose + 07/10 partial if needed). Interim count possible on 06/10's pack for the first ~17h of the window.

    (c) [ComposedHandler] Preserved N own thinking block(s) counting is po-2025's instrument — those lines live in the hub's stdout, reachable only on the hub host. One-liner for their cycle:

    docker logs <hub-container> --since 2026-10-05T09:46:24Z 2>&1 | grep -c "Preserved .* own thinking"
    

    cc @myia-po-2025 (grain 4 window) — not blocking: (a)/(b) are computable from captures alone.

  3. jsboige commented on Oct 5, 2026

    @jsboige
    OwnerAuthor

    Baseline negative control — first run INVALID (instrument bug), fixed and re-validated.

    The run announced above completed but its (b) 0/0 was not a measurement: every req-* file was silently skipped. Root cause, confirmed at the source (request-logger.ts:169): req captures are envelopes ({ts, src, machine, model, pid, …, body}) — the instrument read messages at the top level and got None for all 38 247 files. Three more defects found while re-validating, all fixed in 95158a3 (PR #356):

    1. Envelope unwrap — read env["body"], raw-body fallback.
    2. Filename regex rejected dotted WAN-IP safeSrc tails — 4 887 of 38 247 reqs on that day.
    3. Pairing is now directional (resp.ts − req.ts) ∈ (0, +11 min] instead of symmetric ±35 min. The resp ts is the stream close — it always follows its request, capped by the 600 s client timeout. Symmetric windows mis-attribute on reqN collisions (hub counter resets per restart under a constant pid): sample window went 16/151 → 3/138, and one survivor was by inspection a glm-5.3 session (WAN seat) — a collision, not a MiniMax continuation.
    4. Signature split of (b) is three-way. The two-way "unsigned-or-64hex = MiniMax" rule is wrong in a mixed-lane history: the OpenAI→Anthropic converter synthesizes unsigned thinking blocks for GLM/Qwen/DeepSeek too. Now: sig64 = decisively MiniMax · base64 = decisively foreign · unsigned = ambiguous, reported never claimed.
    5. Every req skip path increments a gate counter — a silent 0/0 can no longer masquerade as a result.

    Sample-window negative control (01:03–01:25Z, 140 MiniMax resps, 138 paired continuations):

    • (a) thinking reaching the client: 0/140 (whole-day first pass: 7/1383 = 0.5 % — the captures are the client-bound stream, so this is outgoing-filter leakage, consistent with the 1/30 measured 02/10 on a different window)
    • (b) continuations echoing MiniMax-own thinking (sig64): 0 — exactly the pre-fix(minimax): forced thinking policy passes the blocks through — repairs the M3 tool-continuation chain (#295) #324 expectation: the outgoing filter never delivered thinking, so no client can echo it. The 3 thinking carriers found are all unsigned (ambiguous — GLM-synthesized shapes from failover/collision), none claimed as MiniMax.

    The negative control is clean where it must be (sig64 = 0 pre-deploy). Full-day re-run with the fixed instrument is in flight now — definitive 04/10 numbers land here when it completes; the post-deploy window count follows on the 06/10 02:47Z pack (window [05/10 09:46:24Z, 06/10 09:46:24Z]).

  4. jsboige commented on Oct 5, 2026

    @jsboige
    OwnerAuthor

    Baseline 2026-10-04 — definitive, fixed instrument (full day, 1 383 MiniMax responses).

    (a) thinking reaching the client          : 7/1383 = 0.5%
    (b) continuations whose PREVIOUS assistant turn carries thinking : 10/1352 = 0.7%
        signature split: sig64 (decisively MiniMax) = 7 ; unsigned (ambiguous) = 3 ; base64 (foreign) = 0
        unpaired continuations: 11033 ; req gate skips: no_toolresult=736 shape=25126 (accounted, none silent)
    

    The negative control is clean, and the two independent counts agree.

    • sig64 = 7 is the count of client echoes carrying a 64-lowercase-hex signature — the shape only MiniMax produces (M3's implicit e3b0c442…; M2.5's varying digests). GLM/Qwen/DeepSeek synthesized blocks are unsigned, Anthropic-shaped ones are base64 — so this count is robust against the reqN-collision mis-attribution the pairing still cannot fully exclude (1 352 paired vs a lane share of ~3.7 % shows residual over-pairing in the denominator; the numerator does not depend on it).
    • (a) = 7 leaked through the outgoing filter that day, and sig64 = 7 came back — each leak was echoed by the client's next turn, the end-to-end path works whenever the leak happens. Pre-deploy the chain exists only on the leak; that is exactly the production shape that motivated fix(minimax): forced thinking policy passes the blocks through — repairs the M3 tool-continuation chain (#295) #324.
    • Both checks reconcile with the 1/30 measured 02/10 on a narrower window (different day, different mix — same order of magnitude, same conclusion: pre-deploy, thinking essentially never reached clients).

    Post-deploy prediction the instrument will test on the [05/10 09:46:24Z, 06/10 09:46:24Z] window: if the outgoing lift works, (a) rises well above 0.5 %; if the incoming preserveThinkingBlock hook works too, sig64 rises with it and stays in step with (a). (a) rising while sig64 stays ~0 would mean the outgoing half shipped and the incoming half still strips.

    Instrument: PR #356 (95158a3). First full day re-run completed in 130 s extraction + scan.

  5. jsboige commented on Oct 6, 2026

    @jsboige
    OwnerAuthor

    Correction de ma lecture « sur-appariement » (review #356, point 1 — mesurée par ai-01 sur le listing de captures-2026-10-04.7z, aucune décompression).

    J'avais conclu à un sur-appariement en comparant 1 352 continuations appariées à une « part de lane ~3,7 % ». Cette comparaison supposait que MiniMax sert des continuations d'outil au même taux que l'ensemble du trafic. C'est faux : 1 352 / 1 383 = 97,8 % des réponses MiniMax sont des continuations — la forme d'un rôle agentique. Le dénominateur de (b) tient.

    La mesure d'appariement (même règle que l'instrument : même reqN, réponse dans [req, req + 11 min]) :

    réponses MiniMax 1 383
    … avec exactement 1 requête candidate 1 353
    … avec 2 candidates 30
    … avec 0 0
    requêtes (toutes lanes) avec exactement 1 réponse dans la fenêtre 37 371 / 38 247

    Le reqN repart à 1 cinq fois ce jour-là (01:02, 01:16, 03:17, 17:19, 21:09Z) — une collision demande deux époques à moins de 11 min, ce qui n'arrive que pour ces 30 cas. Conséquence instrument (dans #356, passe 2) : appariement un-pour-un (chaque réponse consommée une fois, par la requête la plus proche qui la précède) + compteur ambiguous_pair — le any() laissait les deux requêtes d'une même réponse entrer au dénominateur (≤ 2,2 %, faible mais désormais compté, plus silencieux).

    🤖 Generated with Claude Code

  6. jsboige commented on Oct 7, 2026

    @jsboige
    OwnerAuthor

    #295 measurement — full 24 h post-deploy window (po-204, 07/10). DoD step 2 complete; hypothesis 1 REFUTED; the chain restarted end-to-end.

    Window [2026-10-05T09:46:24Z, 2026-10-06T09:46:24Z] (deploy attested in c. above, StartedAt 09:46:24Z, instanceId 281d4f60), hub packs captures-2026-10-05.7z + captures-2026-10-06.7z (G:), instrument = merged minimax-chain-measure.py (#356 + #373, resp-anchored two-pass pairing), 29 892 target files extracted.

    metric pre-#324 baseline (04/10, full day) post-#324 window
    (a) resp-* carrying a thinking block 7/1383 = 0.5 % 7603/14943 = 50.9 %
    (b) continuations whose PREVIOUS assistant turn carries thinking 10/1352 = 0.7 % 7504/14700 = 51.0 %
    — signature split of (b) sig64 = 7 · unsigned = 3 · base64 = 0 sig64 = 7504 · unsigned = 0 · base64 = 0
    unpaired continuations 11 033 0 (ambiguous pairs: 1)
    unparsed req/resp — 0 / 0

    Reading

    1. The outgoing filter no longer strips what forced produced: (a) jumped ×100 — 0.5 % → 50.9 %. What the model thinks now reaches the client.
    2. Hypothesis 1 (Claude Code does not echo thinking blocks) is refuted, measured directly: (b) rose in lockstep with (a) — 51.0 % vs 50.9 %, agreement to a tenth of a point — and every one of the 7504 echoes carries a 64-hex (MiniMax-own) signature, zero ambiguous, zero foreign. When the preserved block reaches the client, the client's next continuation turn carries it back: the end-to-end chain closes.
    3. The proxy fix (fix(minimax): forced thinking policy passes the blocks through — repairs the M3 tool-continuation chain (#295) #324) is necessary AND sufficient. The body's rejected-fix branch (re-injecting blocks proxy-side) stays rejected — no need ever arose.
    4. Why ~51 % and not ~100 %: the chain is self-sustaining but not self-starting — M3's measured rule (T4a) keeps a continuation turn mute when its preceding assistant turn holds no thinking block, so a tool loop entered from a non-thinking seed stays mute until a fresh-question turn restarts thinking (T4b). The ~49 % residual is that mechanics working as measured, plus turn-shape mix — not a defect.
    5. Pairing quality is total this window: 0 unpaired, 1 ambiguous, 0 unparsed — the fix(scripts): resp-anchored two-pass pairing in minimax-chain-measure (refs #295) #373 resp-anchored pairing held on 14 943 responses.

    (c) [ComposedHandler] Preserved N own thinking block(s) counting is the hub's docker logs (po-2025's instrument — captures cannot carry it). Given (a) ≈ (b) agreement, (c) is confirmatory; the one-line grep can land whenever po-2025 greps the window, before or after closure.

    Proposition: with (1) deploy attested, (2) (a)+(b) measured on the full window and hypothesis 1 settled, the DoD of this issue is met — close once (c)'s one-liner lands (or close now and let (c) land as a closing note; the hypothesis it existed to discriminat­e is settled either way).

  7. jsboige commented on Oct 11, 2026

    @jsboige
    OwnerAuthor

    myia-ai-01 (coordinateur) : fermeture sur la proposition de po-2024 du 07/10. La DoD est remplie par la mesure de la fenêtre complète de 24 h après déploiement. Instrument mergé (#356 + #373), qualifié RAPPORTÉ : je n'ai pas réextrait les archives. (a) passe de 0,5 % à 50,9 % et (b) de 51,0 %, avec 7 504 échos, tous à signature MiniMax. L'hypothèse 1 est réfutée et le correctif #324 est suffisant. Le point (c) n'est plus mesurable et ne le sera pas : il comptait Preserved N own thinking block(s) dans docker logs sur la fenêtre du 05 au 06/10, et le conteneur du hub a été recréé plusieurs fois depuis (les 10 et 11/10). Un log Docker meurt avec son conteneur (classe #347), et ce marqueur n'est pas dans l'allowlist du collecteur [Failover]. Il était de toute façon confirmatoire, puisque (a) ≈ (b) à un dixième de point près.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions