Skip to content

Commit 5fa7525

Browse files
committed
feat(autoparser): prefer chat deltas from backends when emitted
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 7962dd1 commit 5fa7525

3 files changed

Lines changed: 67 additions & 4 deletions

File tree

core/backend/llm.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,27 @@ type TokenUsage struct {
3636
Completion int
3737
TimingPromptProcessing float64
3838
TimingTokenGeneration float64
39+
ChatDeltas []*proto.ChatDelta // per-chunk deltas from C++ autoparser (only set during streaming)
40+
}
41+
42+
// HasChatDeltaContent returns true if any chat delta carries content or reasoning text.
43+
// Used to decide whether to prefer C++ autoparser deltas over Go-side tag extraction.
44+
func (t TokenUsage) HasChatDeltaContent() bool {
45+
for _, d := range t.ChatDeltas {
46+
if d.Content != "" || d.ReasoningContent != "" {
47+
return true
48+
}
49+
}
50+
return false
51+
}
52+
53+
// ChatDeltaReasoningAndContent extracts accumulated reasoning and content from chat deltas.
54+
func (t TokenUsage) ChatDeltaReasoningAndContent() (reasoning, content string) {
55+
for _, d := range t.ChatDeltas {
56+
content += d.Content
57+
reasoning += d.ReasoningContent
58+
}
59+
return reasoning, content
3960
}
4061

4162
// ModelInferenceFunc is a test-friendly indirection to call model inference logic.
@@ -171,6 +192,9 @@ func ModelInference(ctx context.Context, s string, messages schema.Messages, ima
171192
allChatDeltas = append(allChatDeltas, reply.ChatDeltas...)
172193
}
173194

195+
// Attach per-chunk chat deltas to tokenUsage so the callback can use them
196+
tokenUsage.ChatDeltas = reply.ChatDeltas
197+
174198
// Parse logprobs from reply if present (collect from last chunk that has them)
175199
if len(reply.Logprobs) > 0 {
176200
var parsedLogprobs schema.Logprobs
@@ -200,6 +224,9 @@ func ModelInference(ctx context.Context, s string, messages schema.Messages, ima
200224
if len(msg) == 0 {
201225
tokenCallback("", tokenUsage)
202226
}
227+
228+
// Clear per-chunk deltas so they don't leak to the next chunk
229+
tokenUsage.ChatDeltas = nil
203230
})
204231
if len(allChatDeltas) > 0 {
205232
xlog.Debug("[ChatDeltas] streaming completed, accumulated deltas from C++ autoparser", "total_deltas", len(allChatDeltas))

core/http/endpoints/openai/chat.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,17 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
8181
extractor := reason.NewReasoningExtractor(thinkingStartToken, config.ReasoningConfig)
8282

8383
_, _, _, err := ComputeChoices(req, s, config, cl, startupOptions, loader, func(s string, c *[]schema.Choice) {}, func(s string, tokenUsage backend.TokenUsage) bool {
84-
reasoningDelta, contentDelta := extractor.ProcessToken(s)
84+
var reasoningDelta, contentDelta string
85+
86+
// Prefer pre-parsed chat deltas from C++ autoparser when available
87+
if tokenUsage.HasChatDeltaContent() {
88+
reasoningDelta, contentDelta = tokenUsage.ChatDeltaReasoningAndContent()
89+
// Keep extractor state consistent for fallback
90+
extractor.ProcessToken(s)
91+
} else {
92+
// Fallback: Go-side extraction from raw text
93+
reasoningDelta, contentDelta = extractor.ProcessToken(s)
94+
}
8595

8696
usage := schema.OpenAIUsage{
8797
PromptTokens: tokenUsage.Prompt,
@@ -133,7 +143,18 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
133143

134144
_, tokenUsage, chatDeltas, err := ComputeChoices(req, prompt, config, cl, startupOptions, loader, func(s string, c *[]schema.Choice) {}, func(s string, usage backend.TokenUsage) bool {
135145
result += s
136-
reasoningDelta, contentDelta := extractor.ProcessToken(s)
146+
147+
var reasoningDelta, contentDelta string
148+
149+
// Prefer pre-parsed chat deltas from C++ autoparser when available
150+
if usage.HasChatDeltaContent() {
151+
reasoningDelta, contentDelta = usage.ChatDeltaReasoningAndContent()
152+
// Keep extractor state consistent for fallback
153+
extractor.ProcessToken(s)
154+
} else {
155+
// Fallback: Go-side extraction from raw text
156+
reasoningDelta, contentDelta = extractor.ProcessToken(s)
157+
}
137158

138159
// Emit reasoning deltas in their own SSE chunks before any tool-call chunks
139160
// (OpenAI spec: reasoning and tool_calls never share a delta)

core/http/endpoints/openresponses/responses.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1819,7 +1819,14 @@ func handleOpenResponsesStream(c echo.Context, responseID string, createdAt int6
18191819

18201820
// If no tool calls detected yet, handle reasoning and text
18211821
if !inToolCallMode {
1822-
reasoningDelta, contentDelta := extractor.ProcessToken(token)
1822+
var reasoningDelta, contentDelta string
1823+
// Prefer pre-parsed chat deltas from C++ autoparser when available
1824+
if tokenUsage.HasChatDeltaContent() {
1825+
reasoningDelta, contentDelta = tokenUsage.ChatDeltaReasoningAndContent()
1826+
extractor.ProcessToken(token) // keep state consistent
1827+
} else {
1828+
reasoningDelta, contentDelta = extractor.ProcessToken(token)
1829+
}
18231830

18241831
// Handle reasoning item
18251832
if extractor.Reasoning() != "" {
@@ -2338,7 +2345,15 @@ func handleOpenResponsesStream(c echo.Context, responseID string, createdAt int6
23382345
// Stream text deltas with reasoning extraction
23392346
tokenCallback := func(token string, tokenUsage backend.TokenUsage) bool {
23402347
accumulatedText += token
2341-
reasoningDelta, contentDelta := extractor.ProcessToken(token)
2348+
2349+
var reasoningDelta, contentDelta string
2350+
// Prefer pre-parsed chat deltas from C++ autoparser when available
2351+
if tokenUsage.HasChatDeltaContent() {
2352+
reasoningDelta, contentDelta = tokenUsage.ChatDeltaReasoningAndContent()
2353+
extractor.ProcessToken(token) // keep state consistent
2354+
} else {
2355+
reasoningDelta, contentDelta = extractor.ProcessToken(token)
2356+
}
23422357

23432358
// Handle reasoning item
23442359
if extractor.Reasoning() != "" {

0 commit comments

Comments
 (0)