da9c8334d8
- SSE Keepalive Ping (15s心跳防止代理断连) - Timing HTTP 头 (X-Timing-Queue/Inference/Total-Ms) - Adapter Request-ID 传播到后端 - Session 清理日志回调 - Server 安全加固 (ReadHeaderTimeout/MaxHeaderBytes 防 slowloris) - Usage Tracker 数据保留清理 (retentionDays + 定期清理) - Config Reload 后 Adapter Registry 更新 (RegisterIfAbsent + RWMutex) - Rate Limiter 空闲 Bucket 清理 (30分钟过期) - Shutdown Drain 超时可配置 (ShutdownDrainSeconds) - Config 模型字段校验增强 (provider/endpoint/actual_model) - Auth 过期 Key 自动清理 (5分钟扫描) - Admin API Rate Limiting - Adapter Health Check 独立超时 (每个 adapter 3s) - TCP 连接阶段超时 (DialContext 5s + KeepAlive 30s) - 幂等键缓存、审计日志、Gzip 中间件、CORS Expose Headers - Backpressure 响应头、熔断器 Prometheus 指标 - 连接池优化、Trace-ID 全链路传播
223 lines
5.9 KiB
Go
223 lines
5.9 KiB
Go
package context
|
|
|
|
import (
|
|
"github.com/edgeai/gateway/internal/config"
|
|
"github.com/edgeai/gateway/pkg/api"
|
|
)
|
|
|
|
// Assembler assembles context messages for a chat request.
|
|
type Assembler struct {
|
|
estimator *TokenEstimator
|
|
cfg *config.ContextConfig
|
|
summarizer *Summarizer
|
|
}
|
|
|
|
// NewAssembler creates a new context assembler.
|
|
func NewAssembler(cfg *config.ContextConfig) *Assembler {
|
|
return &Assembler{
|
|
estimator: NewTokenEstimator(),
|
|
cfg: cfg,
|
|
}
|
|
}
|
|
|
|
// SetSummarizer 注入摘要器,启用后 trimSummaryAndRecent 策略将生成真实 LLM 摘要。
|
|
func (a *Assembler) SetSummarizer(s *Summarizer) {
|
|
a.summarizer = s
|
|
}
|
|
|
|
// AssembleResult contains the assembled messages and metadata.
|
|
type AssembleResult struct {
|
|
Messages []api.Message
|
|
InputTokens int
|
|
Trimmed bool
|
|
TrimmedCount int
|
|
}
|
|
|
|
// Assemble combines session history with new messages, applying context window limits.
|
|
func (a *Assembler) Assemble(history []api.Message, newMessages []api.Message, contextWindow int, maxOutputTokens int, policy string) *AssembleResult {
|
|
// Calculate available context for history
|
|
availableForHistory := contextWindow - maxOutputTokens
|
|
if availableForHistory < 0 {
|
|
availableForHistory = contextWindow / 2
|
|
}
|
|
|
|
// Apply safety margin
|
|
availableForHistory = int(float64(availableForHistory) * (1.0 - a.cfg.SafetyMarginRatio))
|
|
|
|
// Combine all messages
|
|
allMessages := make([]api.Message, 0, len(history)+len(newMessages))
|
|
allMessages = append(allMessages, history...)
|
|
allMessages = append(allMessages, newMessages...)
|
|
|
|
// Estimate total tokens
|
|
totalTokens := a.estimateAllTokens(allMessages)
|
|
|
|
if totalTokens <= availableForHistory {
|
|
return &AssembleResult{
|
|
Messages: allMessages,
|
|
InputTokens: totalTokens,
|
|
Trimmed: false,
|
|
}
|
|
}
|
|
|
|
// Need to trim — apply policy
|
|
trimmed := a.applyPolicy(allMessages, availableForHistory, policy)
|
|
|
|
return &AssembleResult{
|
|
Messages: trimmed.messages,
|
|
InputTokens: trimmed.tokens,
|
|
Trimmed: true,
|
|
TrimmedCount: len(allMessages) - len(trimmed.messages),
|
|
}
|
|
}
|
|
|
|
type trimResult struct {
|
|
messages []api.Message
|
|
tokens int
|
|
}
|
|
|
|
func (a *Assembler) applyPolicy(messages []api.Message, budget int, policy string) trimResult {
|
|
switch policy {
|
|
case "recent_only":
|
|
return a.trimRecentOnly(messages, budget)
|
|
case "summary_and_recent":
|
|
return a.trimSummaryAndRecent(messages, budget)
|
|
case "full":
|
|
return a.trimFull(messages, budget)
|
|
default:
|
|
return a.trimSummaryAndRecent(messages, budget)
|
|
}
|
|
}
|
|
|
|
// trimRecentOnly keeps only the most recent messages within budget.
|
|
func (a *Assembler) trimRecentOnly(messages []api.Message, budget int) trimResult {
|
|
result := make([]api.Message, 0)
|
|
tokens := 0
|
|
|
|
// Iterate from the end (most recent first)
|
|
for i := len(messages) - 1; i >= 0; i-- {
|
|
msgTokens := a.estimateMsgTokens(messages[i])
|
|
if tokens+msgTokens > budget && len(result) > 0 {
|
|
break
|
|
}
|
|
// Prepend to maintain order
|
|
result = append([]api.Message{messages[i]}, result...)
|
|
tokens += msgTokens
|
|
}
|
|
|
|
return trimResult{messages: result, tokens: tokens}
|
|
}
|
|
|
|
// trimSummaryAndRecent keeps system message + a summary placeholder + recent messages.
|
|
func (a *Assembler) trimSummaryAndRecent(messages []api.Message, budget int) trimResult {
|
|
if len(messages) == 0 {
|
|
return trimResult{}
|
|
}
|
|
|
|
// Always keep system messages at the front
|
|
systemMsgs := []api.Message{}
|
|
rest := []api.Message{}
|
|
for _, m := range messages {
|
|
if m.Role == "system" {
|
|
systemMsgs = append(systemMsgs, m)
|
|
} else {
|
|
rest = append(rest, m)
|
|
}
|
|
}
|
|
|
|
systemTokens := 0
|
|
for _, m := range systemMsgs {
|
|
systemTokens += a.estimateMsgTokens(m)
|
|
}
|
|
|
|
// Reserve space for a summary placeholder (~50 tokens)
|
|
summaryTokens := 50
|
|
availableForRecent := budget - systemTokens - summaryTokens
|
|
if availableForRecent < 0 {
|
|
availableForRecent = budget / 2
|
|
}
|
|
|
|
// Keep most recent messages
|
|
recentMsgs := []api.Message{}
|
|
recentTokens := 0
|
|
for i := len(rest) - 1; i >= 0; i-- {
|
|
msgTokens := a.estimateMsgTokens(rest[i])
|
|
if recentTokens+msgTokens > availableForRecent && len(recentMsgs) > 0 {
|
|
break
|
|
}
|
|
recentMsgs = append([]api.Message{rest[i]}, recentMsgs...)
|
|
recentTokens += msgTokens
|
|
}
|
|
|
|
// Add summary if we trimmed anything
|
|
result := make([]api.Message, 0, len(systemMsgs)+1+len(recentMsgs))
|
|
result = append(result, systemMsgs...)
|
|
if len(recentMsgs) < len(rest) {
|
|
// 被裁剪的老消息
|
|
trimmedMsgs := rest[:len(rest)-len(recentMsgs)]
|
|
var summaryText string
|
|
if a.summarizer != nil {
|
|
summaryText = a.summarizer.Summarize(trimmedMsgs)
|
|
} else {
|
|
summaryText = "[Earlier conversation history has been summarized and omitted.]"
|
|
}
|
|
result = append(result, api.Message{
|
|
Role: "system",
|
|
Content: summaryText,
|
|
})
|
|
}
|
|
result = append(result, recentMsgs...)
|
|
|
|
return trimResult{
|
|
messages: result,
|
|
tokens: systemTokens + summaryTokens + recentTokens,
|
|
}
|
|
}
|
|
|
|
// trimFull keeps messages as-is but truncates the oldest if over budget.
|
|
func (a *Assembler) trimFull(messages []api.Message, budget int) trimResult {
|
|
result := make([]api.Message, 0, len(messages))
|
|
tokens := 0
|
|
|
|
// Keep system messages, trim oldest non-system messages
|
|
systemMsgs := []api.Message{}
|
|
rest := []api.Message{}
|
|
for _, m := range messages {
|
|
if m.Role == "system" {
|
|
systemMsgs = append(systemMsgs, m)
|
|
} else {
|
|
rest = append(rest, m)
|
|
}
|
|
}
|
|
|
|
for _, m := range systemMsgs {
|
|
t := a.estimateMsgTokens(m)
|
|
tokens += t
|
|
result = append(result, m)
|
|
}
|
|
|
|
for _, m := range rest {
|
|
t := a.estimateMsgTokens(m)
|
|
if tokens+t > budget {
|
|
break
|
|
}
|
|
tokens += t
|
|
result = append(result, m)
|
|
}
|
|
|
|
return trimResult{messages: result, tokens: tokens}
|
|
}
|
|
|
|
func (a *Assembler) estimateAllTokens(messages []api.Message) int {
|
|
total := 0
|
|
for _, m := range messages {
|
|
total += a.estimateMsgTokens(m)
|
|
}
|
|
return total
|
|
}
|
|
|
|
func (a *Assembler) estimateMsgTokens(msg api.Message) int {
|
|
content, _ := msg.Content.(string)
|
|
return a.estimator.EstimateText(msg.Role) + a.estimator.EstimateText(content) + 4
|
|
}
|