File size: 1,920 Bytes
6d60378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
// truncation.go 工具调用的残缺参数检测(吸收参考仓库 sse.ts:158-167
// isTruncatedArguments 语义)。
//
// 背景:SSE 流被截断(连接中断 / finish_reason==length)时,工具调用的 arguments
// 会只剩半截 JSON。此时网关若把脏参数原样交给客户端,客户端解析会报非法 JSON 并卡死会话。
// 参考仓库的处置是丢弃残缺调用并触发重试(报告 max-tokens),而非补成 {} 伪造合法外观。
//
// 关键区分:只把「非空但无法解析」视为截断。空串是合法的无参数工具;能解析但类型不对
// (标量 / 数组)属于模型输出错误,交给客户端 schema 校验回传即可,不在此判定。
package upstream

import (
	"encoding/json"
	"strings"
)

// isTruncatedArguments 判定工具参数字符串是否因分片丢失而残缺(区别于「该工具本就无参数」)。
//   - 空串 / 纯空白 → false(合法无参工具);
//   - 非空但 JSON 解析失败 → true(截断);
//   - 能解析(含 null/标量/数组等任何合法 JSON)→ false。
func isTruncatedArguments(raw string) bool {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return false
	}
	var v any
	return json.Unmarshal([]byte(trimmed), &v) != nil
}

// dropTruncatedToolCalls 过滤出 arguments 完整的 tool_call(返回新 slice)。
// 只依据 isTruncatedArguments 判定,不改动任何保留的调用(正例零改动)。
func dropTruncatedToolCalls(calls []map[string]any) []map[string]any {
	kept := make([]map[string]any, 0, len(calls))
	for _, call := range calls {
		fn, _ := call["function"].(map[string]any)
		if fn == nil {
			kept = append(kept, call)
			continue
		}
		args, _ := fn["arguments"].(string)
		if isTruncatedArguments(args) {
			continue
		}
		kept = append(kept, call)
	}
	return kept
}