feat: context fill grade in chat UI
This commit is contained in:
@@ -96,7 +96,7 @@ enum PromptBuilder {
|
|||||||
additionalContext: additionalContext
|
additionalContext: additionalContext
|
||||||
)
|
)
|
||||||
|
|
||||||
let estimatedPromptTokens = (instructions.count + chatMessages.reduce(0) { $0 + $1.content.count }) * 10 / 35
|
let estimatedPromptTokens = estimatePromptTokens(instructions: instructions, chatMessages: chatMessages)
|
||||||
|
|
||||||
return PreparedPrompt(
|
return PreparedPrompt(
|
||||||
instructions: instructions,
|
instructions: instructions,
|
||||||
@@ -111,6 +111,13 @@ enum PromptBuilder {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func estimatePromptTokens(instructions: String, chatMessages: [Chat.Message]) -> Int {
|
||||||
|
let characterCount = instructions.count + chatMessages.reduce(0) { partial, message in
|
||||||
|
partial + message.content.count
|
||||||
|
}
|
||||||
|
return max(0, characterCount * 10 / 35)
|
||||||
|
}
|
||||||
|
|
||||||
private static func imageFingerprint(_ source: String) -> UInt64 {
|
private static func imageFingerprint(_ source: String) -> UInt64 {
|
||||||
var hash: UInt64 = 14_695_981_039_346_656_037
|
var hash: UInt64 = 14_695_981_039_346_656_037
|
||||||
for byte in source.utf8 {
|
for byte in source.utf8 {
|
||||||
|
|||||||
@@ -49,6 +49,34 @@ final class ChatViewModel {
|
|||||||
hasUnsavedChanges ? "\(documentDisplayName) *" : documentDisplayName
|
hasUnsavedChanges ? "\(documentDisplayName) *" : documentDisplayName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentContextLength: Int {
|
||||||
|
modelManager.currentModel?.contextLength ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var estimatedPromptTokens: Int {
|
||||||
|
let draft = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
var chatMessages = conversation.messages.compactMap(historyMessage(from:))
|
||||||
|
if !draft.isEmpty {
|
||||||
|
chatMessages.append(Chat.Message(role: .user, content: draft))
|
||||||
|
}
|
||||||
|
return PromptBuilder.estimatePromptTokens(
|
||||||
|
instructions: effectiveSystemPrompt,
|
||||||
|
chatMessages: chatMessages
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var contextUsedTokens: Int {
|
||||||
|
if isGenerating && (promptTokens > 0 || generationTokens > 0) {
|
||||||
|
return promptTokens + generationTokens
|
||||||
|
}
|
||||||
|
return estimatedPromptTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
var contextFillRatio: Double {
|
||||||
|
guard currentContextLength > 0 else { return 0 }
|
||||||
|
return min(max(Double(contextUsedTokens) / Double(currentContextLength), 0), 1)
|
||||||
|
}
|
||||||
|
|
||||||
/// Ensure a ChatSession exists for the current model.
|
/// Ensure a ChatSession exists for the current model.
|
||||||
private func ensureSession() {
|
private func ensureSession() {
|
||||||
guard let container = modelManager.modelContainer else { return }
|
guard let container = modelManager.modelContainer else { return }
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ struct StatusBarView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
||||||
|
if let model = modelManager.currentModel, model.contextLength > 0 {
|
||||||
|
contextFillView(totalContext: model.contextLength)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
// GPU memory
|
// GPU memory
|
||||||
@@ -78,4 +82,43 @@ struct StatusBarView: View {
|
|||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.background(.bar)
|
.background(.bar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func contextFillView(totalContext: Int) -> some View {
|
||||||
|
let usedTokens = viewModel.contextUsedTokens
|
||||||
|
let ratio = viewModel.contextFillRatio
|
||||||
|
let percent = Int((ratio * 100).rounded())
|
||||||
|
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Capsule()
|
||||||
|
.fill(.quaternary)
|
||||||
|
.frame(width: 48, height: 6)
|
||||||
|
.overlay(alignment: .leading) {
|
||||||
|
Capsule()
|
||||||
|
.fill(contextFillColor(for: ratio))
|
||||||
|
.frame(width: max(4, 48 * ratio), height: 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("Ctx \(percent)%")
|
||||||
|
.font(.caption.monospacedDigit())
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.help("Approximate context usage: \(formatTokenCount(usedTokens)) of \(formatTokenCount(totalContext)) tokens")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func contextFillColor(for ratio: Double) -> Color {
|
||||||
|
if ratio >= 0.9 { return .red }
|
||||||
|
if ratio >= 0.7 { return .orange }
|
||||||
|
return .blue
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatTokenCount(_ count: Int) -> String {
|
||||||
|
if count >= 1_000_000 {
|
||||||
|
return String(format: "%.1fM", Double(count) / 1_000_000)
|
||||||
|
}
|
||||||
|
if count >= 1_000 {
|
||||||
|
return String(format: "%.1fk", Double(count) / 1_000)
|
||||||
|
}
|
||||||
|
return "\(count)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,20 @@ final class PromptBuilderTests: XCTestCase {
|
|||||||
XCTAssertEqual(prepared.additionalContext?["enable_thinking"] as? Bool, legacy.additionalContext?["enable_thinking"] as? Bool)
|
XCTAssertEqual(prepared.additionalContext?["enable_thinking"] as? Bool, legacy.additionalContext?["enable_thinking"] as? Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testEstimatePromptTokensMatchesSharedCharacterHeuristic() {
|
||||||
|
let messages = [
|
||||||
|
Chat.Message(role: .user, content: "1234567890"),
|
||||||
|
Chat.Message(role: .assistant, content: "abcdefghij")
|
||||||
|
]
|
||||||
|
|
||||||
|
let estimated = PromptBuilder.estimatePromptTokens(
|
||||||
|
instructions: "system12345",
|
||||||
|
chatMessages: messages
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(estimated, 8)
|
||||||
|
}
|
||||||
|
|
||||||
func testBuildAggregatesInstructionsAndMessages() {
|
func testBuildAggregatesInstructionsAndMessages() {
|
||||||
let request = APIChatCompletionRequest(
|
let request = APIChatCompletionRequest(
|
||||||
model: "gemma",
|
model: "gemma",
|
||||||
|
|||||||
Reference in New Issue
Block a user