updates
This commit is contained in:
@@ -56,7 +56,7 @@ class ChatViewModel {
|
||||
var modelInfoTarget: ModelInfo? = nil
|
||||
var commandHistory: [String] = []
|
||||
var historyIndex: Int = 0
|
||||
var isAutoContinuing: Bool = false
|
||||
private var silentContinuePrompt: String? = nil
|
||||
|
||||
// Save tracking
|
||||
var currentConversationId: UUID? = nil
|
||||
@@ -67,7 +67,6 @@ class ChatViewModel {
|
||||
let chatCount = messages.filter { $0.role != .system }.count
|
||||
return chatCount > 0 && chatCount != savedMessageCount
|
||||
}
|
||||
var autoContinueCountdown: Int = 0
|
||||
|
||||
// MARK: - Auto-Save Tracking
|
||||
|
||||
@@ -78,7 +77,6 @@ class ChatViewModel {
|
||||
// MARK: - Private State
|
||||
|
||||
private var streamingTask: Task<Void, Never>?
|
||||
private var autoContinueTask: Task<Void, Never>?
|
||||
private let settings = SettingsService.shared
|
||||
private let providerRegistry = ProviderRegistry.shared
|
||||
|
||||
@@ -354,59 +352,21 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
streamingTask?.cancel()
|
||||
streamingTask = nil
|
||||
isGenerating = false
|
||||
cancelAutoContinue() // Also cancel any pending auto-continue
|
||||
silentContinuePrompt = nil
|
||||
}
|
||||
|
||||
func startAutoContinue() {
|
||||
isAutoContinuing = true
|
||||
autoContinueCountdown = 5
|
||||
|
||||
autoContinueTask = Task { @MainActor in
|
||||
// Countdown from 5 to 1
|
||||
for i in (1...5).reversed() {
|
||||
if Task.isCancelled {
|
||||
isAutoContinuing = false
|
||||
return
|
||||
}
|
||||
autoContinueCountdown = i
|
||||
try? await Task.sleep(for: .seconds(1))
|
||||
}
|
||||
|
||||
// Continue the task
|
||||
isAutoContinuing = false
|
||||
autoContinueCountdown = 0
|
||||
|
||||
let continuePrompt = "Please continue from where you left off."
|
||||
|
||||
// Add user message
|
||||
let userMessage = Message(
|
||||
role: .user,
|
||||
content: continuePrompt,
|
||||
tokens: nil,
|
||||
cost: nil,
|
||||
timestamp: Date(),
|
||||
attachments: nil,
|
||||
responseTime: nil,
|
||||
wasInterrupted: false,
|
||||
modelId: selectedModel?.id
|
||||
)
|
||||
messages.append(userMessage)
|
||||
|
||||
// Continue generation
|
||||
generateAIResponse(to: continuePrompt, attachments: nil)
|
||||
showSystemMessage("↩ Continuing…")
|
||||
silentContinuePrompt = "Please continue from where you left off."
|
||||
Task { @MainActor in
|
||||
generateAIResponse(to: "", attachments: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func cancelAutoContinue() {
|
||||
autoContinueTask?.cancel()
|
||||
autoContinueTask = nil
|
||||
isAutoContinuing = false
|
||||
autoContinueCountdown = 0
|
||||
}
|
||||
|
||||
func clearChat() {
|
||||
messages.removeAll()
|
||||
sessionStats.reset()
|
||||
MCPService.shared.resetBashSessionApproval()
|
||||
showSystemMessage("Chat cleared")
|
||||
}
|
||||
|
||||
@@ -419,6 +379,7 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
|
||||
messages.removeAll()
|
||||
sessionStats.reset()
|
||||
MCPService.shared.resetBashSessionApproval()
|
||||
messages = loadedMessages
|
||||
|
||||
// Rebuild session stats from loaded messages
|
||||
@@ -450,6 +411,7 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
currentProvider = newProvider
|
||||
settings.defaultModel = model.id
|
||||
settings.defaultProvider = newProvider
|
||||
MCPService.shared.resetBashSessionApproval()
|
||||
}
|
||||
|
||||
private func inferProvider(from modelId: String) -> Settings.Provider? {
|
||||
@@ -784,8 +746,9 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
let mcp = MCPService.shared
|
||||
let mcpActive = mcpEnabled || settings.mcpEnabled
|
||||
let anytypeActive = settings.anytypeMcpEnabled && settings.anytypeMcpConfigured
|
||||
let bashActive = settings.bashEnabled
|
||||
let modelSupportTools = selectedModel?.capabilities.tools ?? false
|
||||
if modelSupportTools && (anytypeActive || (mcpActive && !mcp.allowedFolders.isEmpty)) {
|
||||
if modelSupportTools && (anytypeActive || bashActive || (mcpActive && !mcp.allowedFolders.isEmpty)) {
|
||||
generateAIResponseWithTools(provider: provider, modelId: modelId)
|
||||
return
|
||||
}
|
||||
@@ -898,7 +861,7 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
temperature: chatRequest.temperature,
|
||||
imageGeneration: true
|
||||
)
|
||||
let response = try await provider.chat(request: nonStreamRequest)
|
||||
let response = try await withOverloadedRetry { try await provider.chat(request: nonStreamRequest) }
|
||||
let responseTime = Date().timeIntervalSince(startTime)
|
||||
|
||||
if let index = messages.firstIndex(where: { $0.id == messageId }) {
|
||||
@@ -1304,6 +1267,12 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
return ["role": msg.role.rawValue, "content": msg.content]
|
||||
}
|
||||
|
||||
// If this is a silent auto-continue, inject the prompt into the API call only
|
||||
if let continuePrompt = silentContinuePrompt {
|
||||
apiMessages.append(["role": "user", "content": continuePrompt])
|
||||
silentContinuePrompt = nil
|
||||
}
|
||||
|
||||
let maxIterations = 10 // Increased from 5 to reduce hitting client-side limit
|
||||
var finalContent = ""
|
||||
var totalUsage: ChatResponse.Usage?
|
||||
@@ -1315,13 +1284,15 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
break
|
||||
}
|
||||
|
||||
let response = try await provider.chatWithToolMessages(
|
||||
model: effectiveModelId,
|
||||
messages: apiMessages,
|
||||
tools: tools,
|
||||
maxTokens: settings.maxTokens > 0 ? settings.maxTokens : nil,
|
||||
temperature: settings.temperature > 0 ? settings.temperature : nil
|
||||
)
|
||||
let response = try await withOverloadedRetry {
|
||||
try await provider.chatWithToolMessages(
|
||||
model: effectiveModelId,
|
||||
messages: apiMessages,
|
||||
tools: tools,
|
||||
maxTokens: settings.maxTokens > 0 ? settings.maxTokens : nil,
|
||||
temperature: settings.temperature > 0 ? settings.temperature : nil
|
||||
)
|
||||
}
|
||||
|
||||
if let usage = response.usage { totalUsage = usage }
|
||||
|
||||
@@ -1536,6 +1507,11 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
return "Server error. The provider may be experiencing issues. Try again shortly."
|
||||
}
|
||||
|
||||
// Overloaded / 529
|
||||
if desc.contains("529") || desc.lowercased().contains("overloaded") {
|
||||
return "API is overloaded. Please try again shortly."
|
||||
}
|
||||
|
||||
// Timeout patterns
|
||||
if desc.lowercased().contains("timed out") || desc.lowercased().contains("timeout") {
|
||||
return "Request timed out. Try a shorter message or different model."
|
||||
@@ -1545,6 +1521,30 @@ Don't narrate future actions ("Let me...") - just use the tools.
|
||||
return desc
|
||||
}
|
||||
|
||||
/// Retry an async operation on overloaded (529) errors with exponential backoff.
|
||||
private func withOverloadedRetry<T>(maxAttempts: Int = 4, operation: () async throws -> T) async throws -> T {
|
||||
var attempt = 0
|
||||
while true {
|
||||
do {
|
||||
return try await operation()
|
||||
} catch {
|
||||
let desc = error.localizedDescription
|
||||
let isOverloaded = desc.contains("529") || desc.lowercased().contains("overloaded")
|
||||
attempt += 1
|
||||
if isOverloaded && attempt < maxAttempts && !Task.isCancelled {
|
||||
let delay = Double(1 << attempt) // 2s, 4s, 8s
|
||||
Log.api.warning("API overloaded, retrying in \(Int(delay))s (attempt \(attempt)/\(maxAttempts - 1))...")
|
||||
await MainActor.run {
|
||||
showSystemMessage("⏳ API overloaded, retrying in \(Int(delay))s… (attempt \(attempt)/\(maxAttempts - 1))")
|
||||
}
|
||||
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func showModelInfo(_ model: ModelInfo) {
|
||||
|
||||
Reference in New Issue
Block a user