Skip to content

Commit 7d2a762

Browse files
localai-botmudlerclaude
authored
feat(realtime): configurable pipeline.max_history_items (#10331)
Composed realtime pipelines (VAD+STT+LLM+TTS) defaulted to unlimited history, so a long-running session grew every turn and fed the whole conversation to the LLM until its context window filled. Add an optional pipeline.max_history_items to cap the trailing items per turn; explicit value (including 0=unlimited) wins over the per-model-type default. Self-contained any-to-any models keep their 6-item default. Co-authored-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 61cde6f commit 7d2a762

3 files changed

Lines changed: 42 additions & 1 deletion

File tree

core/config/model_config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,13 @@ type Pipeline struct {
510510
// LLM model config. Unset leaves the LLM model config in charge.
511511
DisableThinking *bool `yaml:"disable_thinking,omitempty" json:"disable_thinking,omitempty"`
512512

513+
// MaxHistoryItems caps how many trailing conversation items are fed to the
514+
// LLM each realtime turn (0 = unlimited, rely on the LLM's context window).
515+
// Unset (nil) uses the per-model-type default. Set it on a composed pipeline
516+
// (VAD+STT+LLM+TTS) so a long-running session doesn't grow until the LLM's
517+
// context fills.
518+
MaxHistoryItems *int `yaml:"max_history_items,omitempty" json:"max_history_items,omitempty"`
519+
513520
// VoiceRecognition gates the pipeline behind speaker verification. Nil
514521
// (block absent) means no gate, preserving existing behavior.
515522
VoiceRecognition *PipelineVoiceRecognition `yaml:"voice_recognition,omitempty" json:"voice_recognition,omitempty"`

core/http/endpoints/openai/realtime.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,17 @@ func defaultMaxHistoryItems(cfg *config.ModelConfig) int {
340340
return 0
341341
}
342342

343+
// resolveMaxHistoryItems honors an explicit pipeline.max_history_items when set,
344+
// otherwise falls back to the per-model-type default. This lets a composed
345+
// pipeline (VAD+STT+LLM+TTS) cap its history so a long-running session doesn't
346+
// grow until the LLM's context window fills.
347+
func resolveMaxHistoryItems(cfg *config.ModelConfig) int {
348+
if cfg != nil && cfg.Pipeline.MaxHistoryItems != nil {
349+
return *cfg.Pipeline.MaxHistoryItems
350+
}
351+
return defaultMaxHistoryItems(cfg)
352+
}
353+
343354
// trimRealtimeItems returns the tail of items capped at maxItems (0 = no cap).
344355
// Walks backwards keeping function_call + function_call_output pairs together
345356
// so we never feed the LLM an orphaned tool result that references a call it
@@ -492,7 +503,7 @@ func runRealtimeSession(application *application.Application, t Transport, model
492503
Conversations: make(map[string]*Conversation),
493504
InputSampleRate: defaultRemoteSampleRate,
494505
OutputSampleRate: defaultRemoteSampleRate,
495-
MaxHistoryItems: defaultMaxHistoryItems(cfg),
506+
MaxHistoryItems: resolveMaxHistoryItems(cfg),
496507
}
497508

498509
// Create a default conversation

core/http/endpoints/openai/realtime_gate_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,29 @@ var _ = Describe("defaultMaxHistoryItems", func() {
107107
})
108108
})
109109

110+
var _ = Describe("resolveMaxHistoryItems", func() {
111+
ptr := func(i int) *int { return &i }
112+
113+
It("uses an explicit pipeline.max_history_items", func() {
114+
cfg := &config.ModelConfig{Pipeline: config.Pipeline{LLM: "llama", MaxHistoryItems: ptr(10)}}
115+
Expect(resolveMaxHistoryItems(cfg)).To(Equal(10))
116+
})
117+
It("honors an explicit 0 (unlimited) over the type default", func() {
118+
cfg := &config.ModelConfig{
119+
KnownUsecases: withUsecases(config.FLAG_REALTIME_AUDIO),
120+
Pipeline: config.Pipeline{MaxHistoryItems: ptr(0)},
121+
}
122+
Expect(resolveMaxHistoryItems(cfg)).To(Equal(0))
123+
})
124+
It("falls back to the type default when unset", func() {
125+
cfg := &config.ModelConfig{KnownUsecases: withUsecases(config.FLAG_REALTIME_AUDIO)}
126+
Expect(resolveMaxHistoryItems(cfg)).To(Equal(6))
127+
})
128+
It("tolerates nil", func() {
129+
Expect(resolveMaxHistoryItems(nil)).To(Equal(0))
130+
})
131+
})
132+
110133
var _ = Describe("trimRealtimeItems", func() {
111134
user := func(id string) *types.MessageItemUnion {
112135
return &types.MessageItemUnion{User: &types.MessageItemUser{ID: id}}

0 commit comments

Comments
 (0)