Skip to content

Commit e81cb64

Browse files
authored
Merge pull request #201 from krissetto/dont-continue-loop-after-max-tokens
Stop request loop when going over max_tokens
2 parents cac33b0 + 95cf1aa commit e81cb64

4 files changed

Lines changed: 37 additions & 17 deletions

File tree

pkg/model/provider/dmr/client.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,15 @@ func mergeRuntimeFlagsPreferUser(derived, user []string) (out, warnings []string
186186
}
187187

188188
func convertMessages(messages []chat.Message) []openai.ChatCompletionMessage {
189-
openaiMessages := make([]openai.ChatCompletionMessage, len(messages))
189+
openaiMessages := make([]openai.ChatCompletionMessage, 0, len(messages))
190190
for i := range messages {
191191
msg := &messages[i]
192+
193+
// Skip invalid assistant messages upfront. This can happen if the model is out of tokens (max_tokens reached)
194+
if msg.Role == chat.MessageRoleAssistant && len(msg.ToolCalls) == 0 && len(msg.MultiContent) == 0 && strings.TrimSpace(msg.Content) == "" {
195+
continue
196+
}
197+
192198
openaiMessage := openai.ChatCompletionMessage{
193199
Role: string(msg.Role),
194200
Name: msg.Name,
@@ -225,7 +231,7 @@ func convertMessages(messages []chat.Message) []openai.ChatCompletionMessage {
225231
openaiMessage.ToolCallID = msg.ToolCallID
226232
}
227233

228-
openaiMessages[i] = openaiMessage
234+
openaiMessages = append(openaiMessages, openaiMessage)
229235
}
230236

231237
var mergedMessages []openai.ChatCompletionMessage

pkg/model/provider/oaistream/adapter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func (a *StreamAdapter) Recv() (chat.MessageStreamResponse, error) {
6666
// Convert the choices
6767
for i := range openaiResponse.Choices {
6868
choice := &openaiResponse.Choices[i]
69-
if a.trackUsage && choice.FinishReason == openai.FinishReasonStop {
69+
if a.trackUsage && (choice.FinishReason == openai.FinishReasonStop || choice.FinishReason == openai.FinishReasonLength) {
7070
choice.FinishReason = openai.FinishReasonNull
7171
}
7272

pkg/model/provider/openai/client.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,15 @@ func convertMultiContent(multiContent []chat.MessagePart) []openai.ChatMessagePa
139139

140140
// convertMessages converts chat.ChatCompletionMessage to openai.ChatCompletionMessage
141141
func convertMessages(messages []chat.Message) []openai.ChatCompletionMessage {
142-
openaiMessages := make([]openai.ChatCompletionMessage, len(messages))
142+
openaiMessages := make([]openai.ChatCompletionMessage, 0, len(messages))
143143
for i := range messages {
144144
msg := &messages[i]
145+
146+
// Skip invalid assistant messages upfront. This can happen if the model is out of tokens (max_tokens reached)
147+
if msg.Role == chat.MessageRoleAssistant && len(msg.ToolCalls) == 0 && len(msg.MultiContent) == 0 && strings.TrimSpace(msg.Content) == "" {
148+
continue
149+
}
150+
145151
openaiMessage := openai.ChatCompletionMessage{
146152
Role: string(msg.Role),
147153
Name: msg.Name,
@@ -178,7 +184,7 @@ func convertMessages(messages []chat.Message) []openai.ChatCompletionMessage {
178184
openaiMessage.ToolCallID = msg.ToolCallID
179185
}
180186

181-
openaiMessages[i] = openaiMessage
187+
openaiMessages = append(openaiMessages, openaiMessage)
182188
}
183189
return openaiMessages
184190
}

pkg/runtime/runtime.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -246,17 +246,22 @@ func (r *runtime) RunStream(ctx context.Context, sess *session.Session) <-chan E
246246
streamSpan.End()
247247
slog.Debug("Stream processed", "agent", a.Name(), "tool_calls", len(calls), "content_length", len(content), "stopped", stopped)
248248

249-
// Add assistant message to conversation history
250-
assistantMessage := chat.Message{
251-
Role: chat.MessageRoleAssistant,
252-
Content: content,
253-
ReasoningContent: reasoningContent,
254-
ToolCalls: calls,
255-
CreatedAt: time.Now().Format(time.RFC3339),
256-
}
249+
// Add assistant message to conversation history, but skip empty assistant messages
250+
// Providers reject assistant messages that have neither content nor tool calls.
251+
if strings.TrimSpace(content) != "" || len(calls) > 0 {
252+
assistantMessage := chat.Message{
253+
Role: chat.MessageRoleAssistant,
254+
Content: content,
255+
ReasoningContent: reasoningContent,
256+
ToolCalls: calls,
257+
CreatedAt: time.Now().Format(time.RFC3339),
258+
}
257259

258-
sess.AddMessage(session.NewAgentMessage(a, &assistantMessage))
259-
slog.Debug("Added assistant message to session", "agent", a.Name(), "total_messages", len(sess.GetAllMessages()))
260+
sess.AddMessage(session.NewAgentMessage(a, &assistantMessage))
261+
slog.Debug("Added assistant message to session", "agent", a.Name(), "total_messages", len(sess.GetAllMessages()))
262+
} else {
263+
slog.Debug("Skipping empty assistant message (no content and no tool calls)", "agent", a.Name())
264+
}
260265

261266
contextLimit := 0
262267
if m != nil {
@@ -371,7 +376,7 @@ func (r *runtime) handleStream(ctx context.Context, stream chat.MessageStream, a
371376
continue
372377
}
373378
choice := response.Choices[0]
374-
if choice.FinishReason == chat.FinishReasonStop {
379+
if choice.FinishReason == chat.FinishReasonStop || choice.FinishReason == chat.FinishReasonLength {
375380
return toolCalls, fullContent.String(), fullReasoningContent.String(), true, nil
376381
}
377382

@@ -438,7 +443,10 @@ func (r *runtime) handleStream(ctx context.Context, stream chat.MessageStream, a
438443
}
439444
}
440445

441-
return toolCalls, fullContent.String(), fullReasoningContent.String(), false, nil
446+
// If the stream completed without producing any content or tool calls, likely because of a token limit, stop to avoid breaking the request loop
447+
// NOTE(krissetto): this can likely be removed once compaction works properly with all providers (aka dmr)
448+
stoppedDueToNoOutput := fullContent.Len() == 0 && len(toolCalls) == 0
449+
return toolCalls, fullContent.String(), fullReasoningContent.String(), stoppedDueToNoOutput, nil
442450
}
443451

444452
// processToolCalls handles the execution of tool calls for an agent

0 commit comments

Comments
 (0)