2025-12-31 08:50:12 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-01-02 17:47:49 +08:00
|
|
|
|
"bytes"
|
2025-12-31 08:50:12 +08:00
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ParsedRequest 保存网关请求的预解析结果
|
|
|
|
|
|
//
|
|
|
|
|
|
// 性能优化说明:
|
|
|
|
|
|
// 原实现在多个位置重复解析请求体(Handler、Service 各解析一次):
|
|
|
|
|
|
// 1. gateway_handler.go 解析获取 model 和 stream
|
|
|
|
|
|
// 2. gateway_service.go 再次解析获取 system、messages、metadata
|
|
|
|
|
|
// 3. GenerateSessionHash 又一次解析获取会话哈希所需字段
|
|
|
|
|
|
//
|
|
|
|
|
|
// 新实现一次解析,多处复用:
|
|
|
|
|
|
// 1. 在 Handler 层统一调用 ParseGatewayRequest 一次性解析
|
|
|
|
|
|
// 2. 将解析结果 ParsedRequest 传递给 Service 层
|
|
|
|
|
|
// 3. 避免重复 json.Unmarshal,减少 CPU 和内存开销
|
|
|
|
|
|
type ParsedRequest struct {
|
|
|
|
|
|
Body []byte // 原始请求体(保留用于转发)
|
|
|
|
|
|
Model string // 请求的模型名称
|
|
|
|
|
|
Stream bool // 是否为流式请求
|
|
|
|
|
|
MetadataUserID string // metadata.user_id(用于会话亲和)
|
|
|
|
|
|
System any // system 字段内容
|
|
|
|
|
|
Messages []any // messages 数组
|
2025-12-31 10:17:38 +08:00
|
|
|
|
HasSystem bool // 是否包含 system 字段(包含 null 也视为显式传入)
|
2025-12-31 08:50:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ParseGatewayRequest 解析网关请求体并返回结构化结果
|
|
|
|
|
|
// 性能优化:一次解析提取所有需要的字段,避免重复 Unmarshal
|
|
|
|
|
|
func ParseGatewayRequest(body []byte) (*ParsedRequest, error) {
|
|
|
|
|
|
var req map[string]any
|
|
|
|
|
|
if err := json.Unmarshal(body, &req); err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
parsed := &ParsedRequest{
|
|
|
|
|
|
Body: body,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if rawModel, exists := req["model"]; exists {
|
|
|
|
|
|
model, ok := rawModel.(string)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return nil, fmt.Errorf("invalid model field type")
|
|
|
|
|
|
}
|
|
|
|
|
|
parsed.Model = model
|
|
|
|
|
|
}
|
|
|
|
|
|
if rawStream, exists := req["stream"]; exists {
|
|
|
|
|
|
stream, ok := rawStream.(bool)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return nil, fmt.Errorf("invalid stream field type")
|
|
|
|
|
|
}
|
|
|
|
|
|
parsed.Stream = stream
|
|
|
|
|
|
}
|
|
|
|
|
|
if metadata, ok := req["metadata"].(map[string]any); ok {
|
|
|
|
|
|
if userID, ok := metadata["user_id"].(string); ok {
|
|
|
|
|
|
parsed.MetadataUserID = userID
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-31 10:17:38 +08:00
|
|
|
|
// system 字段只要存在就视为显式提供(即使为 null),
|
|
|
|
|
|
// 以避免客户端传 null 时被默认 system 误注入。
|
|
|
|
|
|
if system, ok := req["system"]; ok {
|
2025-12-31 08:50:12 +08:00
|
|
|
|
parsed.HasSystem = true
|
|
|
|
|
|
parsed.System = system
|
|
|
|
|
|
}
|
|
|
|
|
|
if messages, ok := req["messages"].([]any); ok {
|
|
|
|
|
|
parsed.Messages = messages
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return parsed, nil
|
|
|
|
|
|
}
|
2026-01-02 17:47:49 +08:00
|
|
|
|
|
|
|
|
|
|
// FilterThinkingBlocks removes thinking blocks from request body
|
|
|
|
|
|
// Returns filtered body or original body if filtering fails (fail-safe)
|
|
|
|
|
|
// This prevents 400 errors from invalid thinking block signatures
|
2026-01-03 06:32:51 -08:00
|
|
|
|
//
|
|
|
|
|
|
// Strategy:
|
|
|
|
|
|
// - When thinking.type != "enabled": Remove all thinking blocks
|
|
|
|
|
|
// - When thinking.type == "enabled": Only remove thinking blocks without valid signatures
|
|
|
|
|
|
// (blocks with missing/empty/dummy signatures that would cause 400 errors)
|
2026-01-02 17:47:49 +08:00
|
|
|
|
func FilterThinkingBlocks(body []byte) []byte {
|
2026-01-03 17:07:54 -08:00
|
|
|
|
return filterThinkingBlocksInternal(body, false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 18:05:15 -08:00
|
|
|
|
// FilterThinkingBlocksForRetry removes thinking blocks from HISTORICAL messages for retry scenarios.
|
|
|
|
|
|
// This is used when upstream returns signature-related 400 errors.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Key insight:
|
|
|
|
|
|
// - User's thinking.type = "enabled" should be PRESERVED (user's intent)
|
|
|
|
|
|
// - Only HISTORICAL assistant messages have thinking blocks with signatures
|
|
|
|
|
|
// - These signatures may be invalid when switching accounts/platforms
|
|
|
|
|
|
// - New responses will generate fresh thinking blocks without signature issues
|
|
|
|
|
|
//
|
|
|
|
|
|
// Strategy:
|
|
|
|
|
|
// - Keep thinking.type = "enabled" (preserve user intent)
|
|
|
|
|
|
// - Remove thinking/redacted_thinking blocks from historical assistant messages
|
|
|
|
|
|
// - Ensure no message has empty content after filtering
|
2026-01-03 17:07:54 -08:00
|
|
|
|
func FilterThinkingBlocksForRetry(body []byte) []byte {
|
2026-01-03 18:05:15 -08:00
|
|
|
|
// Fast path: check for presence of thinking-related keys in messages
|
2026-01-03 17:07:54 -08:00
|
|
|
|
if !bytes.Contains(body, []byte(`"type":"thinking"`)) &&
|
|
|
|
|
|
!bytes.Contains(body, []byte(`"type": "thinking"`)) &&
|
|
|
|
|
|
!bytes.Contains(body, []byte(`"type":"redacted_thinking"`)) &&
|
2026-01-03 18:05:15 -08:00
|
|
|
|
!bytes.Contains(body, []byte(`"type": "redacted_thinking"`)) {
|
2026-01-03 17:07:54 -08:00
|
|
|
|
return body
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var req map[string]any
|
|
|
|
|
|
if err := json.Unmarshal(body, &req); err != nil {
|
|
|
|
|
|
return body
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 18:05:15 -08:00
|
|
|
|
// DO NOT modify thinking.type - preserve user's intent to use thinking mode
|
|
|
|
|
|
// The issue is with historical message signatures, not the thinking mode itself
|
|
|
|
|
|
|
2026-01-03 17:07:54 -08:00
|
|
|
|
messages, ok := req["messages"].([]any)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return body
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
modified := false
|
2026-01-03 18:05:15 -08:00
|
|
|
|
newMessages := make([]any, 0, len(messages))
|
|
|
|
|
|
|
2026-01-03 17:07:54 -08:00
|
|
|
|
for _, msg := range messages {
|
|
|
|
|
|
msgMap, ok := msg.(map[string]any)
|
|
|
|
|
|
if !ok {
|
2026-01-03 18:05:15 -08:00
|
|
|
|
newMessages = append(newMessages, msg)
|
2026-01-03 17:07:54 -08:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 18:05:15 -08:00
|
|
|
|
role, _ := msgMap["role"].(string)
|
2026-01-03 17:07:54 -08:00
|
|
|
|
content, ok := msgMap["content"].([]any)
|
|
|
|
|
|
if !ok {
|
2026-01-03 18:05:15 -08:00
|
|
|
|
// String content or other format - keep as is
|
|
|
|
|
|
newMessages = append(newMessages, msg)
|
2026-01-03 17:07:54 -08:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
newContent := make([]any, 0, len(content))
|
|
|
|
|
|
modifiedThisMsg := false
|
|
|
|
|
|
|
|
|
|
|
|
for _, block := range content {
|
|
|
|
|
|
blockMap, ok := block.(map[string]any)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
newContent = append(newContent, block)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
blockType, _ := blockMap["type"].(string)
|
|
|
|
|
|
|
2026-01-03 18:05:15 -08:00
|
|
|
|
// Remove thinking/redacted_thinking blocks from historical messages
|
|
|
|
|
|
// These have signatures that may be invalid across different accounts
|
|
|
|
|
|
if blockType == "thinking" || blockType == "redacted_thinking" {
|
2026-01-03 17:07:54 -08:00
|
|
|
|
modifiedThisMsg = true
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
newContent = append(newContent, block)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if modifiedThisMsg {
|
|
|
|
|
|
modified = true
|
2026-01-03 18:05:15 -08:00
|
|
|
|
// Handle empty content after filtering
|
|
|
|
|
|
if len(newContent) == 0 {
|
|
|
|
|
|
// For assistant messages, skip entirely (remove from conversation)
|
|
|
|
|
|
// For user messages, add placeholder to avoid empty content error
|
|
|
|
|
|
if role == "user" {
|
|
|
|
|
|
newContent = append(newContent, map[string]any{
|
|
|
|
|
|
"type": "text",
|
|
|
|
|
|
"text": "(content removed)",
|
|
|
|
|
|
})
|
|
|
|
|
|
msgMap["content"] = newContent
|
|
|
|
|
|
newMessages = append(newMessages, msgMap)
|
|
|
|
|
|
}
|
|
|
|
|
|
// Skip assistant messages with empty content (don't append)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
msgMap["content"] = newContent
|
2026-01-03 17:07:54 -08:00
|
|
|
|
}
|
2026-01-03 18:05:15 -08:00
|
|
|
|
newMessages = append(newMessages, msgMap)
|
2026-01-03 17:07:54 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 18:05:15 -08:00
|
|
|
|
if modified {
|
|
|
|
|
|
req["messages"] = newMessages
|
2026-01-03 17:07:54 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
newBody, err := json.Marshal(req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return body
|
|
|
|
|
|
}
|
|
|
|
|
|
return newBody
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// filterThinkingBlocksInternal removes invalid thinking blocks from request
|
|
|
|
|
|
// Strategy:
|
|
|
|
|
|
// - When thinking.type != "enabled": Remove all thinking blocks
|
|
|
|
|
|
// - When thinking.type == "enabled": Only remove thinking blocks without valid signatures
|
|
|
|
|
|
func filterThinkingBlocksInternal(body []byte, _ bool) []byte {
|
2026-01-02 17:47:49 +08:00
|
|
|
|
// Fast path: if body doesn't contain "thinking", skip parsing
|
2026-01-03 06:32:51 -08:00
|
|
|
|
if !bytes.Contains(body, []byte(`"type":"thinking"`)) &&
|
|
|
|
|
|
!bytes.Contains(body, []byte(`"type": "thinking"`)) &&
|
|
|
|
|
|
!bytes.Contains(body, []byte(`"type":"redacted_thinking"`)) &&
|
|
|
|
|
|
!bytes.Contains(body, []byte(`"type": "redacted_thinking"`)) &&
|
|
|
|
|
|
!bytes.Contains(body, []byte(`"thinking":`)) &&
|
|
|
|
|
|
!bytes.Contains(body, []byte(`"thinking" :`)) {
|
2026-01-02 17:47:49 +08:00
|
|
|
|
return body
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var req map[string]any
|
|
|
|
|
|
if err := json.Unmarshal(body, &req); err != nil {
|
2026-01-03 17:07:54 -08:00
|
|
|
|
return body
|
2026-01-02 17:47:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 06:32:51 -08:00
|
|
|
|
// Check if thinking is enabled
|
|
|
|
|
|
thinkingEnabled := false
|
|
|
|
|
|
if thinking, ok := req["thinking"].(map[string]any); ok {
|
|
|
|
|
|
if thinkType, ok := thinking["type"].(string); ok && thinkType == "enabled" {
|
|
|
|
|
|
thinkingEnabled = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-02 17:47:49 +08:00
|
|
|
|
messages, ok := req["messages"].([]any)
|
|
|
|
|
|
if !ok {
|
2026-01-03 17:07:54 -08:00
|
|
|
|
return body
|
2026-01-02 17:47:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
filtered := false
|
|
|
|
|
|
for _, msg := range messages {
|
|
|
|
|
|
msgMap, ok := msg.(map[string]any)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 06:32:51 -08:00
|
|
|
|
role, _ := msgMap["role"].(string)
|
2026-01-02 17:47:49 +08:00
|
|
|
|
content, ok := msgMap["content"].([]any)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
newContent := make([]any, 0, len(content))
|
|
|
|
|
|
filteredThisMessage := false
|
2026-01-03 06:32:51 -08:00
|
|
|
|
|
2026-01-02 17:47:49 +08:00
|
|
|
|
for _, block := range content {
|
|
|
|
|
|
blockMap, ok := block.(map[string]any)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
newContent = append(newContent, block)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
blockType, _ := blockMap["type"].(string)
|
2026-01-03 06:32:51 -08:00
|
|
|
|
|
|
|
|
|
|
if blockType == "thinking" || blockType == "redacted_thinking" {
|
|
|
|
|
|
// When thinking is enabled and this is an assistant message,
|
2026-01-03 17:07:54 -08:00
|
|
|
|
// only keep thinking blocks with valid signatures
|
2026-01-03 06:32:51 -08:00
|
|
|
|
if thinkingEnabled && role == "assistant" {
|
|
|
|
|
|
signature, _ := blockMap["signature"].(string)
|
|
|
|
|
|
if signature != "" && signature != "skip_thought_signature_validator" {
|
|
|
|
|
|
newContent = append(newContent, block)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-02 17:47:49 +08:00
|
|
|
|
filtered = true
|
|
|
|
|
|
filteredThisMessage = true
|
2026-01-03 06:32:51 -08:00
|
|
|
|
continue
|
2026-01-02 17:47:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-03 17:07:54 -08:00
|
|
|
|
// Handle blocks without type discriminator but with "thinking" key
|
2026-01-02 17:47:49 +08:00
|
|
|
|
if blockType == "" {
|
2026-01-03 17:07:54 -08:00
|
|
|
|
if _, hasThinking := blockMap["thinking"]; hasThinking {
|
2026-01-02 17:47:49 +08:00
|
|
|
|
filtered = true
|
|
|
|
|
|
filteredThisMessage = true
|
2026-01-03 06:32:51 -08:00
|
|
|
|
continue
|
2026-01-02 17:47:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
newContent = append(newContent, block)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if filteredThisMessage {
|
|
|
|
|
|
msgMap["content"] = newContent
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !filtered {
|
2026-01-03 17:07:54 -08:00
|
|
|
|
return body
|
2026-01-02 17:47:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
newBody, err := json.Marshal(req)
|
|
|
|
|
|
if err != nil {
|
2026-01-03 17:07:54 -08:00
|
|
|
|
return body
|
2026-01-02 17:47:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
return newBody
|
|
|
|
|
|
}
|