@@ -429,7 +429,8 @@ func slotFillGrammar(slots []types.ClassifierSlot) string {
429429 if vi > 0 {
430430 rules .WriteString (" | " )
431431 }
432- rules .WriteString (gbnfLiteral (`"` + v + `"` ))
432+ encoded , _ := json .Marshal (v ) // validation rejects values JSON cannot encode
433+ rules .WriteString (gbnfLiteral (string (encoded )))
433434 }
434435 default : // string
435436 rules .WriteString ("str" )
@@ -446,6 +447,57 @@ func slotFillGrammar(slots []types.ClassifierSlot) string {
446447 return root .String () + rules .String ()
447448}
448449
450+ const (
451+ // Free-form values need an explicit ceiling; forced enum values and field
452+ // syntax are budgeted from their actual JSON encoding below.
453+ slotFillStringTokens = 64
454+ slotFillNumberTokens = 32
455+ )
456+
457+ // slotFillMaxTokens conservatively budgets one token per output byte for the
458+ // forced JSON tail, plus explicit allowances for free-form values. This avoids
459+ // truncating long enum values or field names while keeping string generation
460+ // bounded.
461+ func slotFillMaxTokens (slots []types.ClassifierSlot ) int {
462+ tokens := 1 // closing brace
463+ for i := range slots {
464+ if i > 0 {
465+ field , _ := json .Marshal (slots [i ].Name )
466+ tokens += len (field ) + len (`, : ` )
467+ }
468+ switch slots [i ].Type {
469+ case types .ClassifierSlotNumber :
470+ tokens += slotFillNumberTokens
471+ case types .ClassifierSlotString :
472+ tokens += slotFillStringTokens
473+ case types .ClassifierSlotEnum :
474+ longest := 0
475+ for _ , value := range slots [i ].Values {
476+ encoded , _ := json .Marshal (value )
477+ if len (encoded ) > longest {
478+ longest = len (encoded )
479+ }
480+ }
481+ tokens += longest
482+ }
483+ }
484+ return tokens
485+ }
486+
487+ // slotFillContextReserve includes both the generated tail and the continuation
488+ // prefix appended after the scored prompt. It intentionally over-reserves by
489+ // counting bytes as tokens; preserving the identical scoring prompt is more
490+ // important than reclaiming a handful of context tokens.
491+ func slotFillContextReserve (option * types.ClassifierOption ) int {
492+ if option == nil || option .Tool == nil || len (option .Tool .Slots ) == 0 {
493+ return 0
494+ }
495+ route , _ := json .Marshal (option .ID )
496+ field , _ := json .Marshal (option .Tool .Slots [0 ].Name )
497+ prefixBytes := len (`{"route": , : ` ) + len (route ) + len (field )
498+ return prefixBytes + slotFillMaxTokens (option .Tool .Slots )
499+ }
500+
449501// parseSlotValues closes the completed route JSON and extracts each slot's
450502// value as the string form SpliceArguments expects.
451503func parseSlotValues (chosenID , firstSlot , generated string , slots []types.ClassifierSlot ) (map [string ]string , error ) {
0 commit comments