Raisonnement : NUDGE_IN_TOOL décide sur le verdict de la forme de la requête

La sonde de gabarit gardait le verdict de la DERNIÈRE forme sondée (outils,
chat_template_kwargs, reasoning_effort), quelle qu'elle soit. NUDGE_IN_TOOL
pouvait donc glisser le rappel de budget dans un résultat d'outil sur la foi
d'un « préfixe instable » conclu pour une autre forme — un autre jeu
d'outils, la réflexion coupée, un autre niveau — dont le rendu n'a rien à
voir.

- Les verdicts sont aussi rangés par forme (empreinte tplShape, au plus 16,
  la plus ancienne cède) ; tplCapsFor rend celui de la forme demandée.
- NUDGE_IN_TOOL calcule la forme de la requête courante (mêmes outils,
  kwargs et niveau que ceux envoyés) et ne place le rappel dans le résultat
  que si CETTE forme est « instable » pour ce modèle ; forme jamais sondée ou
  inconnue : message à part, comme sans la clé. Une relance outils coupés ou
  tool_choice « none » (forme que la sonde ne voit jamais) se replie aussi.
- L'affichage (/api/perf/summary) et REASONING_ECHO gardent le dernier
  verdict, comme avant. Sans la clé, rien ne change.
- Tests : verdict par forme dans la sonde, autre forme / autres kwargs /
  autre niveau refusés, bout à bout avec le verdict d'une autre forme
  (échouait sur l'ancien code).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-04 14:50:15 +02:00
1 parent ac40a2556a
commit 4b659ac186
6 files changed
+154 -20

No files matched your search

+42 -6
View File
@@ -165,9 +165,14 @@ var (
// Attente entre deux essais sur un 503 (modèle en chargement).
tplProbeBackoff = time.Second
tplProbeMu sync.Mutex
tplProbeCache = map[string]tplProbeResult{}
tplProbeLast *tplProbeResult
tplProbeMu sync.Mutex
tplProbeCache = map[string]tplProbeResult{}
tplProbeLast *tplProbeResult
// tplProbeByShape : le dernier verdict de chaque forme de requête
// (tplShape.hash). tplProbeLast est celui de la dernière forme sondée,
// quelle qu'elle soit : bon pour l'affichage, pas pour décider d'une
// requête d'une autre forme (tplCapsFor).
tplProbeByShape = map[string]tplProbeResult{}
tplProbeRunning bool
tplProbeTried time.Time
tplProbeLogged string
@@ -215,6 +220,20 @@ func tplCapsCurrent() (tplProbeResult, bool) {
return *tplProbeLast, true
}
// tplCapsFor : le dernier verdict pour CETTE forme de requête (mêmes outils,
// mêmes chat_template_kwargs, même reasoning_effort). Pas de verdict pour elle
// — jamais sondée, ou sortie du cache — : rien, et l'appelant se replie comme
// sur « inconnu ». Rien non plus pour un preset externe.
func tplCapsFor(shape tplShape) (tplProbeResult, bool) {
if externalActive() {
return tplProbeResult{}, false
}
tplProbeMu.Lock()
defer tplProbeMu.Unlock()
r, ok := tplProbeByShape[shape.hash()]
return r, ok
}
// tplProbeEnsure : verdict pour la forme donnée, depuis le cache ou une sonde.
func tplProbeEnsure(ctx context.Context, shape tplShape) tplProbeResult {
if resolveChatEndpoint().External {
@@ -232,7 +251,7 @@ func tplProbeEnsure(ctx context.Context, shape tplShape) tplProbeResult {
props, err := p.props(ctx)
if err != nil {
r := tplUnknownResult("/props illisible : " + err.Error())
tplProbeStore("", r)
tplProbeStore("", shape.hash(), r)
return r
}
h := fnv.New64a()
@@ -245,6 +264,7 @@ func tplProbeEnsure(ctx context.Context, shape tplShape) tplProbeResult {
tplProbeMu.Lock()
if r, ok := tplProbeCache[key]; ok {
tplProbeLast = &r
tplProbeKeepShape(shape.hash(), r)
tplProbeMu.Unlock()
return r
}
@@ -265,14 +285,30 @@ func tplProbeEnsure(ctx context.Context, shape tplShape) tplProbeResult {
if fresh {
fmt.Fprintln(os.Stderr, line)
}
tplProbeStore(key, r)
tplProbeStore(key, shape.hash(), r)
return r
}
func tplProbeStore(key string, r tplProbeResult) {
// tplProbeKeepShape note le verdict d'une forme (tplProbeMu tenu). Au plus
// tplProbeCacheMax formes : la plus ancienne cède sa place.
func tplProbeKeepShape(shape string, r tplProbeResult) {
if _, ok := tplProbeByShape[shape]; !ok && len(tplProbeByShape) >= tplProbeCacheMax {
oldest := ""
for k, v := range tplProbeByShape {
if oldest == "" || v.At.Before(tplProbeByShape[oldest].At) {
oldest = k
}
}
delete(tplProbeByShape, oldest)
}
tplProbeByShape[shape] = r
}
func tplProbeStore(key, shape string, r tplProbeResult) {
tplProbeMu.Lock()
defer tplProbeMu.Unlock()
tplProbeLast = &r
tplProbeKeepShape(shape, r)
if key == "" || !r.cacheable {
return
}