146 lines
4.9 KiB
Go
146 lines
4.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
|
|
}
|
|
|
|
func defaultCloudAgentContextBudget() cloudAgentContextBudget {
|
|
return cloudAgentContextBudgetFor(defaultCloudAgentContextWindowTokens, defaultCloudAgentMaxOutputTokens, "default")
|
|
}
|
|
|
|
func cloudAgentContextBudgetFor(contextWindow, maxOutput int, source string) cloudAgentContextBudget {
|
|
if contextWindow < minCloudAgentContextWindowTokens || contextWindow > maxCloudAgentContextWindowTokens {
|
|
contextWindow = defaultCloudAgentContextWindowTokens
|
|
}
|
|
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)
|
|
}
|
|
return cloudAgentContextBudget{
|
|
ContextWindowTokens: contextWindow,
|
|
MaxOutputTokens: maxOutput,
|
|
OverheadTokens: overhead,
|
|
InputBudgetTokens: inputBudget,
|
|
CompactAtTokens: max(1_024, inputBudget*85/100),
|
|
Source: source,
|
|
}
|
|
}
|
|
|
|
// 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 channelModel, err := s.repo.ChannelModelByKey(req.ChannelID, req.ChannelModelKey); err == nil {
|
|
if capability, err := normalizedChannelModelCapability(channelModel); err == nil && capability != nil && capability.Text != nil {
|
|
return cloudAgentContextBudgetFor(capability.Text.ContextWindowTokens, capability.Text.MaxOutputTokens, "channel-model")
|
|
}
|
|
}
|
|
}
|
|
return defaultCloudAgentContextBudget()
|
|
}
|
|
|
|
// 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"
|
|
}
|