Skip to content

Commit 131cfad

Browse files
committed
fix: 修复空文本块和stop_reason判断问题
主要修复: 1. handlers.go: 移除预先发送的空content_block_start,避免空文本块违规 - 依赖sse_state_manager的自动启动机制,只在有实际内容时才生成块 - 确保每个content_block都有实际内容 2. stop_reason_manager.go: 修复stop_reason判断逻辑 - 根据Claude API规范,只要消息包含tool_use块就应返回"tool_use" - 修复之前只检查活跃工具导致的错误判断 3. converter/codewhisperer.go: 优化消息处理逻辑 - 改进user消息缓冲处理流程 4. parser/tool_lifecycle_manager.go: 清理未使用的方法 - 删除allToolsCompleted()方法 这些修复确保完全符合Claude API规范,避免SSE事件序列违规
1 parent 7cde05d commit 131cfad

File tree

5 files changed

+60
-25
lines changed

5 files changed

+60
-25
lines changed

converter/codewhisperer.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,9 @@ func BuildCodeWhispererRequest(anthropicReq types.AnthropicRequest, ctx *gin.Con
387387
if msg.Role == "user" {
388388
// 收集user消息到缓冲区
389389
userMessagesBuffer = append(userMessagesBuffer, msg)
390-
} else if msg.Role == "assistant" {
390+
continue
391+
}
392+
if msg.Role == "assistant" {
391393
// 遇到assistant,处理之前累积的user消息
392394
if len(userMessagesBuffer) > 0 {
393395
// 合并所有累积的user消息

parser/tool_lifecycle_manager.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -345,11 +345,6 @@ func (tlm *ToolLifecycleManager) GetCompletedTools() map[string]*ToolExecution {
345345
return result
346346
}
347347

348-
// allToolsCompleted 检查是否所有工具都已完成
349-
func (tlm *ToolLifecycleManager) allToolsCompleted() bool {
350-
return len(tlm.activeTools) == 0
351-
}
352-
353348
// getOrAssignBlockIndex 获取或分配块索引
354349
func (tlm *ToolLifecycleManager) getOrAssignBlockIndex(toolID string) int {
355350
if index, exists := tlm.blockIndexMap[toolID]; exists {
@@ -379,7 +374,7 @@ func (tlm *ToolLifecycleManager) generateTextIntroduction(firstTool ToolCall) []
379374
// 修复:删除重复的content_block_start和content_block_stop
380375
// 原因:block[0]已在sendInitialEvents()中启动(handlers.go:148-156),会在sendFinalEvents()中关闭
381376
// 这里只需要发送content_block_delta来添加介绍文本即可
382-
//
377+
//
383378
// 之前的问题:
384379
// 1. sendInitialEvents发送content_block_start(index:0) → SSEStateManager标记block[0].Started=true
385380
// 2. generateTextIntroduction再次发送content_block_start(index:0) → 违规!block已started但未stopped

server/handlers.go

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,15 @@ func handleGenericStreamRequest(c *gin.Context, anthropicReq types.AnthropicRequ
123123

124124
// createAnthropicStreamEvents 创建Anthropic流式初始事件
125125
func createAnthropicStreamEvents(messageId string, inputTokens int, model string) []map[string]any {
126-
// 创建完整的初始事件序列,包括content_block_start
127-
// 这确保符合Claude API规范的完整SSE事件序列
126+
// 创建基础初始事件序列,不包含content_block_start
127+
//
128+
// 关键修复:移除预先发送的空文本块
129+
// 问题:如果预先发送content_block_start(text),但上游只返回tool_use没有文本,
130+
// 会导致空文本块(start -> stop 之间没有delta),违反Claude API规范
131+
//
132+
// 解决方案:依赖sse_state_manager.handleContentBlockDelta()中的自动启动机制
133+
// 只有在实际收到内容(文本或工具)时才动态生成content_block_start
134+
// 这确保每个content_block都有实际内容
128135
events := []map[string]any{
129136
{
130137
"type": "message_start",
@@ -145,14 +152,6 @@ func createAnthropicStreamEvents(messageId string, inputTokens int, model string
145152
{
146153
"type": "ping",
147154
},
148-
{
149-
"type": "content_block_start",
150-
"index": 0,
151-
"content_block": map[string]any{
152-
"type": "text",
153-
"text": "",
154-
},
155-
},
156155
}
157156
return events
158157
}

server/stop_reason_manager.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,23 @@ func (srm *StopReasonManager) DetermineStopReason() string {
5555
return "max_tokens"
5656
}
5757

58-
// 规则2: 检查是否有活跃的工具调用等待执行
59-
// 根据Claude规范:只有当Claude主动调用工具且期待工具执行时才使用tool_use
60-
if srm.hasActiveToolCalls {
61-
logger.Debug("确定stop_reason: tool_use - 检测到活跃工具调用")
58+
// 规则2: 检查是否有工具调用(活跃或已完成)
59+
// *** 关键修复:根据Claude规范,只要消息包含tool_use块,stop_reason就应该是tool_use ***
60+
// 根据 Anthropic API 文档 (https://docs.anthropic.com/en/api/messages-streaming):
61+
// stop_reason: "tool_use" - The model wants to use a tool
62+
//
63+
// 只要消息中包含任何 tool_use 内容块(无论是正在流式传输还是已完成),
64+
// stop_reason 就应该是 "tool_use"。这与工具的"生命周期状态"无关。
65+
//
66+
// 之前的BUG: 只检查 hasActiveToolCalls(正在流式传输的工具)
67+
// 问题场景: 工具块关闭后被移到 completedTools,导致 hasActiveToolCalls=false,
68+
// 错误返回 "end_turn" 而非 "tool_use"
69+
//
70+
// 修复: 检查 hasActiveToolCalls OR hasCompletedTools
71+
if srm.hasActiveToolCalls || srm.hasCompletedTools {
72+
logger.Debug("确定stop_reason: tool_use - 消息包含工具调用",
73+
logger.Bool("has_active", srm.hasActiveToolCalls),
74+
logger.Bool("has_completed", srm.hasCompletedTools))
6275
return "tool_use"
6376
}
6477

server/stream_processor.go

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type StreamProcessorContext struct {
4646

4747
// 工具调用跟踪
4848
toolUseIdByBlockIndex map[int]string
49+
completedToolUseIds map[string]bool // 已完成的工具ID集合(用于stop_reason判断)
4950

5051
// 原始数据缓冲
5152
rawDataBuffer *strings.Builder
@@ -72,6 +73,7 @@ func NewStreamProcessorContext(
7273
tokenEstimator: utils.NewTokenEstimator(),
7374
compliantParser: parser.NewCompliantEventStreamParser(false),
7475
toolUseIdByBlockIndex: make(map[int]string),
76+
completedToolUseIds: make(map[string]bool),
7577
rawDataBuffer: utils.GetStringBuilder(),
7678
}
7779
}
@@ -99,6 +101,14 @@ func (ctx *StreamProcessorContext) Cleanup() {
99101
ctx.toolUseIdByBlockIndex = nil
100102
}
101103

104+
// 清理已完成工具集合
105+
if ctx.completedToolUseIds != nil {
106+
for k := range ctx.completedToolUseIds {
107+
delete(ctx.completedToolUseIds, k)
108+
}
109+
ctx.completedToolUseIds = nil
110+
}
111+
102112
// 清空文本聚合状态
103113
ctx.pendingText = ""
104114
ctx.lastFlushedText = ""
@@ -217,6 +227,9 @@ func (ctx *StreamProcessorContext) sendInitialEvents(eventCreator func(string, i
217227
// 直接使用上下文中的 inputTokens(已经通过 TokenEstimator 精确计算)
218228
initialEvents := eventCreator(ctx.messageID, ctx.inputTokens, ctx.req.Model)
219229

230+
// 注意:初始事件现在只包含 message_start 和 ping
231+
// content_block_start 会在收到实际内容时由 sse_state_manager 自动生成
232+
// 这避免了发送空内容块(如果上游只返回 tool_use 而没有文本)
220233
for _, event := range initialEvents {
221234
// 使用状态管理器发送事件
222235
if err := ctx.sseStateManager.SendEvent(ctx.c, ctx.sender, event); err != nil {
@@ -269,6 +282,12 @@ func (ctx *StreamProcessorContext) processToolUseStop(dataMap map[string]any) {
269282
}
270283

271284
if toolId, exists := ctx.toolUseIdByBlockIndex[idx]; exists && toolId != "" {
285+
// *** 关键修复:在删除前先记录到已完成工具集合 ***
286+
// 问题:直接删除导致sendFinalEvents()中len(toolUseIdByBlockIndex)==0
287+
// 结果:stop_reason错误判断为end_turn而非tool_use
288+
// 解决:先添加到completedToolUseIds,保持工具调用的证据
289+
ctx.completedToolUseIds[toolId] = true
290+
272291
logger.Debug("工具执行完成",
273292
logger.String("tool_id", toolId),
274293
logger.Int("block_index", idx))
@@ -367,10 +386,17 @@ func (ctx *StreamProcessorContext) sendFinalEvents() error {
367386
}
368387

369388
// 更新工具调用状态
370-
ctx.stopReasonManager.UpdateToolCallStatus(
371-
len(ctx.toolUseIdByBlockIndex) > 0,
372-
len(ctx.toolUseIdByBlockIndex) > 0,
373-
)
389+
// 使用已完成工具集合来判断,因为toolUseIdByBlockIndex在stop时已被清空
390+
hasActiveTools := len(ctx.toolUseIdByBlockIndex) > 0
391+
hasCompletedTools := len(ctx.completedToolUseIds) > 0
392+
393+
logger.Debug("更新工具调用状态",
394+
logger.Bool("has_active_tools", hasActiveTools),
395+
logger.Bool("has_completed_tools", hasCompletedTools),
396+
logger.Int("active_count", len(ctx.toolUseIdByBlockIndex)),
397+
logger.Int("completed_count", len(ctx.completedToolUseIds)))
398+
399+
ctx.stopReasonManager.UpdateToolCallStatus(hasActiveTools, hasCompletedTools)
374400

375401
// 计算输出tokens(使用TokenEstimator统一算法)
376402
content := ctx.rawDataBuffer.String()[:utils.IntMin(ctx.totalOutputChars*4, ctx.rawDataBuffer.Len())]

0 commit comments

Comments
 (0)