File size: 3,031 Bytes
f9b0358
 
 
 
 
 
 
 
 
 
671888a
f9b0358
671888a
 
 
f9b0358
671888a
 
 
 
 
f9b0358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import { generateText, type LanguageModel } from "ai"
import { countTokens } from "@/lib/tokens"

export type CompactableMessage = {
  role: string
  content: unknown
}

const CONTEXT_LIMITS: Record<string, number> = {
  "gpt-4o": 128_000,
  "gpt-4o-mini": 128_000,
  "gemini-2.5-flash": 1_000_000,
  "gemini-2.5-flash-lite": 1_000_000,
  "gemma-4-26b-a4b-it": 32_000,
  "gemma-4-31b-it": 32_000,
  "llama-3.1-8b-instant": 131_000,
  "llama-3.3-70b-versatile": 131_000,
  "openai/gpt-oss-120b": 131_000,
  "openai/gpt-oss-20b": 131_000,
  "qwen/qwen3-32b": 131_000,
  "meta-llama/llama-4-scout-17b-16e-instruct": 131_000,
  default: 128_000,
}

const COMPACTION_PROMPT =
  "Tóm tắt cuộc trò chuyện sau thành 1 system note ngắn gọn (bullet points). Giữ tên, fact quan trọng, decisions:"

function contentToText(content: unknown): string {
  if (typeof content === "string") return content
  if (Array.isArray(content)) {
    return content
      .map((part) => {
        if (typeof part === "string") return part
        if (part && typeof part === "object" && "text" in part) return String(part.text ?? "")
        return ""
      })
      .filter(Boolean)
      .join(" ")
  }
  if (content == null) return ""
  try {
    return JSON.stringify(content)
  } catch {
    return String(content)
  }
}

function serializeMessages(messages: CompactableMessage[]) {
  return messages
    .map((message) => `${message.role}: ${contentToText(message.content)}`.trim())
    .join("\n\n")
}

export function getContextLimit(modelId: string) {
  return CONTEXT_LIMITS[modelId] ?? CONTEXT_LIMITS.default
}

export function countConversationTokens(messages: CompactableMessage[]) {
  return countTokens(serializeMessages(messages))
}

export async function compactMessagesIfNeeded({
  messages,
  modelId,
  compactModel,
}: {
  messages: CompactableMessage[]
  modelId: string
  compactModel?: LanguageModel
}): Promise<{ messages: CompactableMessage[]; compacted: boolean; totalTokens: number; limit: number }> {
  const safeMessages = Array.isArray(messages) ? messages : []
  const limit = getContextLimit(modelId)
  const totalTokens = countConversationTokens(safeMessages)

  if (!compactModel || totalTokens <= limit * 0.8 || safeMessages.length <= 4) {
    return { messages: safeMessages, compacted: false, totalTokens, limit }
  }

  const olderMessages = safeMessages.slice(0, -4)
  const recentMessages = safeMessages.slice(-4)
  const transcript = serializeMessages(olderMessages)
  if (!transcript.trim()) return { messages: safeMessages, compacted: false, totalTokens, limit }

  const { text } = await generateText({
    model: compactModel,
    prompt: `${COMPACTION_PROMPT}\n\n${transcript}`,
    maxTokens: 700,
  })

  const summary = text.trim()
  if (!summary) return { messages: safeMessages, compacted: false, totalTokens, limit }

  return {
    messages: [{ role: "system", content: `Tóm tắt context cũ:\n${summary}` }, ...recentMessages],
    compacted: true,
    totalTokens,
    limit,
  }
}