2.4.1 #7

Merged
rune merged 11 commits from 2.4.1 into main 2026-07-14 11:01:36 +02:00
Showing only changes of commit 7119cd1d06 - Show all commits
+44 -32
View File
@@ -360,7 +360,6 @@ Don't narrate future actions ("Let me...") - just use the tools.
} }
func startAutoContinue() { func startAutoContinue() {
showSystemMessage("↩ Continuing…")
silentContinuePrompt = "Please continue from where you left off." silentContinuePrompt = "Please continue from where you left off."
Task { @MainActor in Task { @MainActor in
generateAIResponse(to: "", attachments: nil) generateAIResponse(to: "", attachments: nil)
@@ -1475,9 +1474,8 @@ Don't narrate future actions ("Let me...") - just use the tools.
if finalContent.isEmpty { if finalContent.isEmpty {
// Some models (observed with Qwen via OpenRouter) stop calling tools // Some models (observed with Qwen via OpenRouter) stop calling tools
// after a long tool-call chain but return no summarizing text at all. // after a long tool-call chain but return no summarizing text at all.
// Surface a placeholder and nudge a follow-up turn instead of a silent blank bubble. // Silently nudge a follow-up turn instead of showing a placeholder bubble.
finishedWithEmptyContent = true finishedWithEmptyContent = true
finalContent = "[No response from the model — retrying]"
} }
break break
} }
@@ -1566,9 +1564,7 @@ Don't narrate future actions ("Let me...") - just use the tools.
// If this was the last iteration, note it // If this was the last iteration, note it
if iteration == maxIterations - 1 { if iteration == maxIterations - 1 {
hitIterationLimit = true // We're exiting with pending tool calls hitIterationLimit = true // We're exiting with pending tool calls
finalContent = response.content.isEmpty finalContent = response.content
? "[Tool loop reached maximum iterations]"
: response.content
} }
} }
@@ -1577,41 +1573,57 @@ Don't narrate future actions ("Let me...") - just use the tools.
wasCancelled = true wasCancelled = true
} }
// If we hit the iteration limit or the model returned no text at all, silently
// nudge a follow-up turn instead of showing a placeholder/blank bubble.
let willAutoContinue = (hitIterationLimit || finishedWithEmptyContent) && !wasCancelled
// Display the final response as an assistant message // Display the final response as an assistant message
let responseTime = Date().timeIntervalSince(startTime) let responseTime = Date().timeIntervalSince(startTime)
let assistantMessage = Message( if willAutoContinue && finalContent.isEmpty {
role: .assistant, // Nothing worth showing yet still record usage/cost for this turn.
content: finalContent, if let usage = totalUsage, let model = selectedModel {
tokens: totalUsage?.completionTokens, let hasPricing = model.pricing.prompt > 0 || model.pricing.completion > 0
cost: nil, let cost: Double? = hasPricing ? calculateCost(usage: usage, pricing: model.pricing) : nil
timestamp: Date(), sessionStats.addMessage(
attachments: nil, inputTokens: usage.promptTokens,
responseTime: responseTime, outputTokens: usage.completionTokens,
wasInterrupted: wasCancelled, cost: cost
modelId: modelId, )
generatedImages: finalImages.isEmpty ? nil : finalImages
)
messages.append(assistantMessage)
// Calculate cost
if let usage = totalUsage, let model = selectedModel {
let hasPricing = model.pricing.prompt > 0 || model.pricing.completion > 0
let cost: Double? = hasPricing ? calculateCost(usage: usage, pricing: model.pricing) : nil
if let index = messages.lastIndex(where: { $0.id == assistantMessage.id }) {
messages[index].cost = cost
} }
sessionStats.addMessage( } else {
inputTokens: usage.promptTokens, let assistantMessage = Message(
outputTokens: usage.completionTokens, role: .assistant,
cost: cost content: finalContent,
tokens: totalUsage?.completionTokens,
cost: nil,
timestamp: Date(),
attachments: nil,
responseTime: responseTime,
wasInterrupted: wasCancelled,
modelId: modelId,
generatedImages: finalImages.isEmpty ? nil : finalImages
) )
messages.append(assistantMessage)
// Calculate cost
if let usage = totalUsage, let model = selectedModel {
let hasPricing = model.pricing.prompt > 0 || model.pricing.completion > 0
let cost: Double? = hasPricing ? calculateCost(usage: usage, pricing: model.pricing) : nil
if let index = messages.lastIndex(where: { $0.id == assistantMessage.id }) {
messages[index].cost = cost
}
sessionStats.addMessage(
inputTokens: usage.promptTokens,
outputTokens: usage.completionTokens,
cost: cost
)
}
} }
isGenerating = false isGenerating = false
streamingTask = nil streamingTask = nil
// If we hit the iteration limit, or the model returned no text at all, nudge a follow-up turn if willAutoContinue {
if (hitIterationLimit || finishedWithEmptyContent) && !wasCancelled {
startAutoContinue() startAutoContinue()
} }