- 只翻注释/文档字符串:`go/scanner` 剥注释后按 token 序列+literal 逐 token 比对,**54/54 文件 SAME**;
改动文件总行数与 HEAD 漂移 = 0(严格 1:1,无合并/拆行)
- 裁决编号与 design 节号逐字保留(03.4.1/03.4.2/03.4.3/14.4.1/14.4.5/07.4.2-4、design/04 §3.1/§3.2 等);
`chat_template_kwargs` / `enable_thinking` / `reasoning_effort` / `thinking:{type}` 字段名与形状说明未动
- 存疑译法(已登记):`persona` 在叙事语境用「角色」、标识符语境保留 `persona`(仓库既有模块本身混用);
`rung`→「级」;`provider` 存在「提供商/提供者」两种落笔(可按需一键统一)
- 复核:build/vet 绿 + 两包测试绿;gofmt 干净;残留英文注释 0 条
- 既有存量(未修,只报告):`model/anthropic_test.go` 612 行 >500;`applyAnthropicEvent` 115 行 >80
160 lines
5.5 KiB
Go
160 lines
5.5 KiB
Go
package model
|
||
|
||
// 表示 token 预算把生成切断的提供商停止/结束原因:
|
||
// Anthropic 上报 stop_reason "max_tokens",OpenAI 兼容提供商
|
||
// 上报 finish_reason "length"。两者都被原样归一化进 Response.StopReason,
|
||
// 因此链路追踪保留提供商自己的词汇。
|
||
const (
|
||
stopReasonMaxTokens = "max_tokens"
|
||
stopReasonLength = "length"
|
||
)
|
||
|
||
// 截断恢复的边界。最多允许一次带翻倍预算的重试;
|
||
// 上限防止病态预算无界增长。hint 是连重试
|
||
// 也没产出可见文本时返回的非空占位,
|
||
// 因此被截断的回合绝不会以静默空回复的形式浮出。
|
||
// 它绝不携带思考:推理不是答案。
|
||
const (
|
||
truncationRetryCap = 32768
|
||
truncationEmptyHint = "(思考过长,未能生成回复)"
|
||
)
|
||
|
||
// effectiveMaxTokens 解出一次尝试实际发送的预算:显式的
|
||
// Request.MaxTokens,否则用适配器默认值。当协议没有适配器
|
||
// 默认值时 def 可以为零,此时适用提供商默认值。
|
||
func effectiveMaxTokens(n, def int) int {
|
||
if n > 0 {
|
||
return n
|
||
}
|
||
return def
|
||
}
|
||
|
||
// retryMaxTokens 为一次性的截断重试翻倍实际生效预算,
|
||
// 上限为 truncationRetryCap。当没有什么可翻倍时(n 与 def 都是零,
|
||
// 即适用提供商默认值),它原样返回实际生效预算,从而让
|
||
// 调用方跳过那次会发出完全相同请求的重试。
|
||
func retryMaxTokens(n, def int) int {
|
||
eff := effectiveMaxTokens(n, def)
|
||
if eff <= 0 {
|
||
return 0
|
||
}
|
||
if bumped := eff * 2; bumped < truncationRetryCap {
|
||
return bumped
|
||
}
|
||
return truncationRetryCap
|
||
}
|
||
|
||
// isTruncatedStopReason 报告某个停止原因是否意味着 token 预算
|
||
// 切断了生成。
|
||
func isTruncatedStopReason(reason string) bool {
|
||
return reason == stopReasonMaxTokens || reason == stopReasonLength
|
||
}
|
||
|
||
// isEmptyBody 报告响应是否既无可见文本也无工具
|
||
// 调用。仅工具的一轮并不是空:它的空正文是正常的、属于
|
||
// 工具循环。
|
||
func isEmptyBody(r Response) bool {
|
||
return r.Content == "" && len(r.ToolCalls) == 0
|
||
}
|
||
|
||
// isTruncatedEmpty 报告某次尝试是否在产出任何可见输出之前
|
||
// 就被 token 预算切断(无文本、无工具调用)。只有这种形状
|
||
// 才触发截断重试/兜底;正常的 tool_use 回合,其正文
|
||
// 合法地为空,不会触发。
|
||
func isTruncatedEmpty(r Response) bool {
|
||
return isEmptyBody(r) && isTruncatedStopReason(r.StopReason)
|
||
}
|
||
|
||
// withEmptyBodyHint 在响应既无文本也无工具调用时填入一段
|
||
// 非空占位正文,因此被截断的回合绝不会以静默
|
||
// 空回复的形式浮出。已携带文本或工具调用的响应
|
||
// 原样返回。
|
||
func withEmptyBodyHint(r Response) Response {
|
||
if !isEmptyBody(r) {
|
||
return r
|
||
}
|
||
r.Content = truncationEmptyHint
|
||
r.Message = Message{Role: RoleAssistant, Content: []Content{{Type: ContentTypeText, Text: r.Content}}}
|
||
return r
|
||
}
|
||
|
||
// callWithTruncationRetry 执行一次非流式尝试,当第一次尝试
|
||
// 在无可见输出下被切断时,用翻倍 token 预算重试一次,
|
||
// 随后在重试仍无产出时返回非空占位。attempt 接收要发送
|
||
// 的预算。第一次尝试的上游错误会被原样返回(不重试);
|
||
// 重试失败则退回占位,
|
||
// 而不是浮出一个空回复。
|
||
func callWithTruncationRetry(maxTokens, def int, attempt func(limit int) (Response, error)) (Response, error) {
|
||
out, err := attempt(maxTokens)
|
||
if err != nil {
|
||
return Response{}, err
|
||
}
|
||
if !isTruncatedEmpty(out) {
|
||
return out, nil
|
||
}
|
||
if bumped := retryMaxTokens(maxTokens, def); bumped > effectiveMaxTokens(maxTokens, def) {
|
||
if second, retryErr := attempt(bumped); retryErr == nil {
|
||
out = second
|
||
}
|
||
}
|
||
return withEmptyBodyHint(out), nil
|
||
}
|
||
|
||
// callStreamWithTruncationRetry 是 callWithTruncationRetry 的
|
||
// 流式对位实现。attempt 接收该次尝试的预算与事件回调。
|
||
// 重试会丢弃思考事件(第一次尝试已经
|
||
// 流过它的推理,再流一次会产生重复块),
|
||
// 同时实时转发文本/工具事件;当连重试也没有可见
|
||
// 输出时,会以文本片段发出非空占位并把它作为
|
||
// 响应正文返回,因此流绝不会以静默截断收尾。
|
||
func callStreamWithTruncationRetry(
|
||
maxTokens, def int,
|
||
attempt func(limit int, onEvent func(StreamEvent)) (Response, error),
|
||
onEvent func(StreamEvent),
|
||
) (Response, error) {
|
||
out, err := attempt(maxTokens, onEvent)
|
||
if err != nil {
|
||
return Response{}, err
|
||
}
|
||
if !isTruncatedEmpty(out) {
|
||
return out, nil
|
||
}
|
||
if bumped := retryMaxTokens(maxTokens, def); bumped > effectiveMaxTokens(maxTokens, def) {
|
||
if second, retryErr := attempt(bumped, dropThinking(onEvent)); retryErr == nil {
|
||
out = second
|
||
}
|
||
}
|
||
if isEmptyBody(out) {
|
||
out = withEmptyBodyHint(out)
|
||
emitTextSegment(onEvent, out.Content)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// dropThinking 包裹 onEvent,使重试尝试不会再次发出思考
|
||
// 片段。文本与 tool_call 事件原样通过。
|
||
func dropThinking(onEvent func(StreamEvent)) func(StreamEvent) {
|
||
if onEvent == nil {
|
||
return nil
|
||
}
|
||
return func(ev StreamEvent) {
|
||
switch ev.Kind {
|
||
case StreamKindThinkingStart, StreamKindThinkingDelta, StreamKindThinkingEnd:
|
||
return
|
||
default:
|
||
onEvent(ev)
|
||
}
|
||
}
|
||
}
|
||
|
||
// emitTextSegment 发出一个单增量的文本片段,使合成的占位
|
||
// 能到达只渲染 message.* 事件的流式消费者。
|
||
func emitTextSegment(onEvent func(StreamEvent), text string) {
|
||
if onEvent == nil || text == "" {
|
||
return
|
||
}
|
||
onEvent(StreamEvent{Kind: StreamKindTextStart})
|
||
onEvent(StreamEvent{Kind: StreamKindTextDelta, Delta: text})
|
||
onEvent(StreamEvent{Kind: StreamKindTextEnd})
|
||
}
|