172 lines
5.9 KiB
Go
172 lines
5.9 KiB
Go
package app
|
||
|
||
import (
|
||
"encoding/json"
|
||
"math"
|
||
"strconv"
|
||
"strings"
|
||
"unicode/utf8"
|
||
)
|
||
|
||
const (
|
||
defaultCloudAgentContextWindowTokens = 128_000
|
||
defaultCloudAgentMaxOutputTokens = 16_384
|
||
minCloudAgentContextWindowTokens = 4_096
|
||
maxCloudAgentContextWindowTokens = 10_000_000
|
||
)
|
||
|
||
type cloudAgentContextBudget struct {
|
||
ContextWindowTokens int
|
||
MaxOutputTokens int
|
||
OverheadTokens int
|
||
InputBudgetTokens int
|
||
CompactAtTokens int
|
||
Source string
|
||
// Configured 说明这份预算里的窗口是不是"模型自己声明的值":false 表示没有解析到
|
||
// 可用窗口、上面几个数都来自兜底默认值。压力读数据此决定要不要给出窗口占用率——
|
||
// 拿默认窗口冒充真实能力(例如给未声明窗口的模型算一个"占满 80%")会误导排查。
|
||
Configured bool
|
||
}
|
||
|
||
func defaultCloudAgentContextBudget() cloudAgentContextBudget {
|
||
budget := cloudAgentContextBudgetFor(defaultCloudAgentContextWindowTokens, defaultCloudAgentMaxOutputTokens, "default")
|
||
budget.Configured = false
|
||
return budget
|
||
}
|
||
|
||
func cloudAgentContextBudgetFor(contextWindow, maxOutput int, source string) cloudAgentContextBudget {
|
||
configured := true
|
||
if contextWindow < minCloudAgentContextWindowTokens || contextWindow > maxCloudAgentContextWindowTokens {
|
||
contextWindow, configured = defaultCloudAgentContextWindowTokens, false
|
||
}
|
||
if maxOutput < 256 || maxOutput >= contextWindow {
|
||
maxOutput = min(defaultCloudAgentMaxOutputTokens, contextWindow/4)
|
||
}
|
||
// Tool schemas, provider wrappers and the final recovery turn are part of the
|
||
// request, but are not represented by the canonical message body alone. Keep
|
||
// a bounded proportional reserve so a 1M model can still use its capacity.
|
||
overhead := max(4_096, min(32_768, contextWindow/25))
|
||
inputBudget := contextWindow - maxOutput - overhead
|
||
if inputBudget < 1_024 {
|
||
inputBudget = max(1_024, contextWindow/2)
|
||
}
|
||
compactAt := min(contextWindow*80/100, inputBudget)
|
||
compactAt = max(1_024, compactAt)
|
||
return cloudAgentContextBudget{
|
||
ContextWindowTokens: contextWindow,
|
||
MaxOutputTokens: maxOutput,
|
||
OverheadTokens: overhead,
|
||
InputBudgetTokens: inputBudget,
|
||
CompactAtTokens: compactAt,
|
||
Source: source,
|
||
Configured: configured,
|
||
}
|
||
}
|
||
|
||
// cloudAgentContextBudgetForRequest resolves the capability contract that will
|
||
// actually execute the next text turn. Logical models use the safe intersection
|
||
// of every eligible text route so route selection cannot choose a smaller window
|
||
// after the context was assembled.
|
||
func (s *Service) cloudAgentContextBudgetForRequest(req CloudAgentRequest) cloudAgentContextBudget {
|
||
if s == nil || s.repo == nil {
|
||
return defaultCloudAgentContextBudget()
|
||
}
|
||
if req.LogicalModelID != "" {
|
||
if snapshot, err := s.routeCatalogSnapshot(); err == nil {
|
||
if cached, ok := snapshot.Models[req.LogicalModelID]; ok {
|
||
contextWindow, maxOutput := 0, 0
|
||
for _, route := range cached.Routes {
|
||
if normalizeCapability(route.CapabilitySpec.Capability) != "text" {
|
||
continue
|
||
}
|
||
capability, err := normalizedChannelModelCapability(&route.ChannelModel)
|
||
if err != nil || capability == nil || capability.Text == nil {
|
||
continue
|
||
}
|
||
if contextWindow == 0 || capability.Text.ContextWindowTokens < contextWindow {
|
||
contextWindow = capability.Text.ContextWindowTokens
|
||
}
|
||
if maxOutput == 0 || capability.Text.MaxOutputTokens < maxOutput {
|
||
maxOutput = capability.Text.MaxOutputTokens
|
||
}
|
||
}
|
||
if contextWindow > 0 && maxOutput > 0 {
|
||
return cloudAgentContextBudgetFor(contextWindow, maxOutput, "logical-route-intersection")
|
||
}
|
||
}
|
||
}
|
||
return defaultCloudAgentContextBudget()
|
||
}
|
||
if req.ChannelID != "" && req.ChannelModelKey != "" {
|
||
if capability := s.cloudAgentChannelTextCapability(req); capability != nil {
|
||
return cloudAgentContextBudgetFor(capability.ContextWindowTokens, capability.MaxOutputTokens, "channel-model")
|
||
}
|
||
}
|
||
return defaultCloudAgentContextBudget()
|
||
}
|
||
|
||
// cloudAgentChannelTextCapability resolves the text capability of the channel
|
||
// model this turn will actually run on. Logical models have no single channel
|
||
// model, so they return nil and keep the route-intersection path.
|
||
func (s *Service) cloudAgentChannelTextCapability(req CloudAgentRequest) *TextCapabilityConfig {
|
||
if s == nil || s.repo == nil || req.ChannelID == "" || req.ChannelModelKey == "" {
|
||
return nil
|
||
}
|
||
channelModel, err := s.repo.ChannelModelByKey(req.ChannelID, req.ChannelModelKey)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
capability, err := normalizedChannelModelCapability(channelModel)
|
||
if err != nil || capability == nil || capability.Text == nil {
|
||
return nil
|
||
}
|
||
return capability.Text
|
||
}
|
||
|
||
// cloudAgentEstimatedTokens is deliberately conservative for non-ASCII text.
|
||
// It is a planning estimate, not a provider tokenizer; underestimation would
|
||
// make a request fail only after reaching an upstream provider.
|
||
func cloudAgentEstimatedTokens(value []byte) int {
|
||
if len(value) == 0 {
|
||
return 1
|
||
}
|
||
ascii, nonASCII := 0, 0
|
||
for len(value) > 0 {
|
||
r, size := utf8.DecodeRune(value)
|
||
if r == utf8.RuneError && size == 1 {
|
||
nonASCII++
|
||
value = value[1:]
|
||
continue
|
||
}
|
||
if r < 0x80 {
|
||
ascii++
|
||
} else {
|
||
nonASCII++
|
||
}
|
||
value = value[size:]
|
||
}
|
||
return max(1, int(math.Ceil(float64(ascii)/4+float64(nonASCII)*1.5)))
|
||
}
|
||
|
||
func cloudAgentRequestEstimatedTokens(request *canonicalAgentRequest) (int, error) {
|
||
raw, err := json.Marshal(request)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return cloudAgentEstimatedTokens(raw), nil
|
||
}
|
||
|
||
func cloudAgentContextBudgetMessage(budget cloudAgentContextBudget) string {
|
||
return "用户指令与当前执行事实超过模型输入预算(" + formatTokenCount(budget.InputBudgetTokens) + " Token,能力来源:" + strings.TrimSpace(budget.Source) + "),请缩小本轮范围"
|
||
}
|
||
|
||
func formatTokenCount(value int) string {
|
||
if value < 1_000 {
|
||
return strconv.Itoa(value)
|
||
}
|
||
if value%1_000 == 0 {
|
||
return strconv.Itoa(value/1_000) + "K"
|
||
}
|
||
return strconv.Itoa(value/1_000) + "K"
|
||
}
|