@@ -117,3 +117,185 @@ func estimateToolDefinitionTokens(tools []llm.ToolDefinition) int64 {
117117 }
118118 return tokens
119119}
120+
121+ // resolveToolResultTokenBudget 计算当前用户轮次的全部工具结果可使用的模型输入预算。
122+ // 新批次先使用该上限,回灌前再对同轮全部结果统一分配,不额外透支有效上下文。
123+ func resolveToolResultTokenBudget (
124+ generateInput llm.GenerateInput ,
125+ messages []llm.Message ,
126+ pendingAssistant llm.Message ,
127+ modelName string ,
128+ capabilitiesJSON string ,
129+ ) int64 {
130+ budgetMessages := toolResultPayloadPlaceholders (prioritizeCurrentToolMessages (messages ))
131+ placeholderResults := make ([]llm.ToolResult , 0 , len (pendingAssistant .ToolCalls ))
132+ for _ , call := range pendingAssistant .ToolCalls {
133+ placeholderResults = append (placeholderResults , llm.ToolResult {
134+ ToolCallID : call .ToolCallID ,
135+ ToolName : call .ToolName ,
136+ OutputJSON : "{}" ,
137+ })
138+ }
139+ budgetMessages = append (
140+ budgetMessages ,
141+ pendingAssistant ,
142+ llm.Message {Role : "tool" , ToolResults : placeholderResults },
143+ )
144+ available := int64 (llm .EffectiveContextBudgetFromCapabilities (modelName , capabilitiesJSON )) -
145+ estimateToolFollowUpInputTokens (generateInput , budgetMessages )
146+ if available < 0 {
147+ return 0
148+ }
149+ return available
150+ }
151+
152+ // rebalanceToolFollowUpResults 在完整工具回灌请求超预算时,统一压缩当前轮的全部工具结果。
153+ func rebalanceToolFollowUpResults (
154+ generateInput llm.GenerateInput ,
155+ messages []llm.Message ,
156+ modelName string ,
157+ capabilitiesJSON string ,
158+ ) ([]llm.Message , bool ) {
159+ effectiveBudget := int64 (llm .EffectiveContextBudgetFromCapabilities (modelName , capabilitiesJSON ))
160+ if estimateToolFollowUpInputTokens (generateInput , messages ) <= effectiveBudget {
161+ return messages , false
162+ }
163+
164+ _ , currentUserIndex := toolHistoryBounds (messages )
165+ if currentUserIndex < 0 {
166+ return messages , false
167+ }
168+ fixedMessages := toolResultPayloadPlaceholders (messages )
169+ resultBudget := effectiveBudget - estimateToolFollowUpInputTokens (generateInput , fixedMessages )
170+ if resultBudget < 0 {
171+ resultBudget = 0
172+ }
173+
174+ type resultRef struct {
175+ messageIndex int
176+ resultIndex int
177+ }
178+ result := append ([]llm.Message (nil ), messages ... )
179+ refs := make ([]resultRef , 0 )
180+ slots := make ([]toolExecutionSlot , 0 )
181+ for messageIndex := currentUserIndex + 1 ; messageIndex < len (result ); messageIndex ++ {
182+ if len (result [messageIndex ].ToolResults ) == 0 {
183+ continue
184+ }
185+ result [messageIndex ].ToolResults = append ([]llm.ToolResult (nil ), result [messageIndex ].ToolResults ... )
186+ for resultIndex , toolResult := range result [messageIndex ].ToolResults {
187+ refs = append (refs , resultRef {messageIndex : messageIndex , resultIndex : resultIndex })
188+ slots = append (slots , toolExecutionSlot {result : toolResult })
189+ }
190+ }
191+ if len (slots ) == 0 {
192+ return messages , false
193+ }
194+
195+ enforceToolResultAggregateBudget (slots , resultBudget )
196+ changed := false
197+ for index , ref := range refs {
198+ if result [ref .messageIndex ].ToolResults [ref .resultIndex ] != slots [index ].result {
199+ changed = true
200+ result [ref .messageIndex ].ToolResults [ref .resultIndex ] = slots [index ].result
201+ }
202+ }
203+ if ! changed {
204+ return messages , false
205+ }
206+ return result , true
207+ }
208+
209+ // toolResultPayloadPlaceholders 保留工具结果的协议结构,但移除可变正文以计算固定上下文开销。
210+ func toolResultPayloadPlaceholders (messages []llm.Message ) []llm.Message {
211+ result := append ([]llm.Message (nil ), messages ... )
212+ for messageIndex := range result {
213+ if len (result [messageIndex ].ToolResults ) == 0 {
214+ continue
215+ }
216+ placeholders := make ([]llm.ToolResult , 0 , len (result [messageIndex ].ToolResults ))
217+ for _ , toolResult := range result [messageIndex ].ToolResults {
218+ placeholders = append (placeholders , llm.ToolResult {
219+ ToolCallID : toolResult .ToolCallID ,
220+ ToolName : toolResult .ToolName ,
221+ OutputJSON : "{}" ,
222+ Status : toolResult .Status ,
223+ })
224+ }
225+ result [messageIndex ].ToolResults = placeholders
226+ }
227+ return result
228+ }
229+
230+ // trimToolFollowUpHistory 仅在工具回灌请求超预算时删除最老的完整历史轮次。
231+ func trimToolFollowUpHistory (
232+ generateInput llm.GenerateInput ,
233+ messages []llm.Message ,
234+ modelName string ,
235+ capabilitiesJSON string ,
236+ ) ([]llm.Message , bool ) {
237+ effectiveBudget := int64 (llm .EffectiveContextBudgetFromCapabilities (modelName , capabilitiesJSON ))
238+ estimatedTokens := estimateToolFollowUpInputTokens (generateInput , messages )
239+ if estimatedTokens <= effectiveBudget {
240+ return messages , false
241+ }
242+
243+ systemEnd , currentUserIndex := toolHistoryBounds (messages )
244+ if currentUserIndex <= systemEnd {
245+ return messages , false
246+ }
247+ for cutFrom := systemEnd ; cutFrom < currentUserIndex ; cutFrom ++ {
248+ estimatedTokens -= estimateMessageTokens (messages [cutFrom ])
249+ nextIndex := cutFrom + 1
250+ if nextIndex < currentUserIndex && messages [nextIndex ].Role != "user" {
251+ continue
252+ }
253+ if estimatedTokens <= effectiveBudget || nextIndex == currentUserIndex {
254+ trimmed := make ([]llm.Message , 0 , systemEnd + len (messages )- nextIndex )
255+ trimmed = append (trimmed , messages [:systemEnd ]... )
256+ trimmed = append (trimmed , messages [nextIndex :]... )
257+ return trimmed , true
258+ }
259+ }
260+ return messages , false
261+ }
262+
263+ // prioritizeCurrentToolMessages 返回系统指令和当前用户轮次,供工具结果计算最大可用预算。
264+ func prioritizeCurrentToolMessages (messages []llm.Message ) []llm.Message {
265+ systemEnd , currentUserIndex := toolHistoryBounds (messages )
266+ if currentUserIndex <= systemEnd {
267+ return append ([]llm.Message (nil ), messages ... )
268+ }
269+ result := make ([]llm.Message , 0 , systemEnd + len (messages )- currentUserIndex )
270+ result = append (result , messages [:systemEnd ]... )
271+ result = append (result , messages [currentUserIndex :]... )
272+ return result
273+ }
274+
275+ // toolHistoryBounds 定位系统前缀结束位置和当前轮用户消息。
276+ func toolHistoryBounds (messages []llm.Message ) (int , int ) {
277+ systemEnd := 0
278+ for systemEnd < len (messages ) && messages [systemEnd ].Role == "system" {
279+ systemEnd ++
280+ }
281+ currentUserIndex := - 1
282+ for index := len (messages ) - 1 ; index >= systemEnd ; index -- {
283+ if messages [index ].Role == "user" {
284+ currentUserIndex = index
285+ break
286+ }
287+ }
288+ return systemEnd , currentUserIndex
289+ }
290+
291+ // estimateToolFollowUpInputTokens 按全量请求形状估算工具回灌输入。
292+ func estimateToolFollowUpInputTokens (generateInput llm.GenerateInput , messages []llm.Message ) int64 {
293+ budgetMessages := messages
294+ if strings .TrimSpace (generateInput .Instructions ) != "" {
295+ _ , budgetMessages = extractOpenAIResponsesInstructions (messages )
296+ }
297+ budgetInput := generateInput
298+ budgetInput .Messages = budgetMessages
299+ budgetInput .PreviousResponseID = ""
300+ return estimateGenerateInputTokens (budgetInput )
301+ }
0 commit comments