Files
Local-Cursor/internal/backend/agent/model/tool_call_id.go
FuDie0915 db73cf5a39 chore: import Local-Cursor v1.1.8 source snapshot
Includes the current working directory changes. Historical commits and release binaries are excluded to limit Gitea storage.
Source base: d7515973fbfd6dd2001fa156c483455b8081c7cb
2026-11-10 01:24:28 +08:00

127 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package modeladapter
import (
"crypto/sha256"
"encoding/hex"
"strings"
)
const (
maxProviderToolCallIDLen = 64
toolCallNamespaceHashLen = 12
toolCallValueHashLen = 12
)
// namespaceToolCallID 为 provider 原始 tool call id 增加 model-call 级别命名空间,
// 避免像 functions.Shell:0 这类跨轮复用的 id 在客户端被误判为同一个 bubble。
//
// OpenAI 等 provider 对 tool_call_id 长度有限制,因此这里使用 model_call_id 的短哈希
// 而不是完整 UUID,保证内部存储的 tool_call_id 既稳定又能安全回放给 provider。
func namespaceToolCallID(modelCallID string, rawToolCallID string) string {
raw := strings.TrimSpace(rawToolCallID)
if raw == "" {
return ""
}
if strings.Contains(raw, "::") {
return providerToolCallID(raw)
}
model := strings.TrimSpace(modelCallID)
if model == "" {
return providerToolCallID(raw)
}
return buildProviderSafeToolCallID(shortToolCallHash(model, toolCallNamespaceHashLen), raw)
}
// providerToolCallID 把内部持久化的 tool_call_id 规整成 provider 可接受的安全长度。
// 这样旧会话里已经落盘的 legacy "<modelCallID>::<rawID>" 也能继续回放。
func providerToolCallID(toolCallID string) string {
trimmed := strings.TrimSpace(toolCallID)
if trimmed == "" {
return ""
}
namespace, raw, ok := splitLegacyToolCallID(trimmed)
if ok {
return buildProviderSafeToolCallID(shortToolCallHash(namespace, toolCallNamespaceHashLen), raw)
}
if len(trimmed) <= maxProviderToolCallIDLen {
return trimmed
}
return buildProviderSafeToolCallID("", trimmed)
}
type providerToolCallDescriptor struct {
ID string `json:"id"`
Index int `json:"index,omitempty"`
Type string `json:"type"`
Function ToolCallFunctionShape `json:"function"`
}
func normalizeToolCallDescriptors(toolCalls []ToolCallDescriptor) []providerToolCallDescriptor {
if len(toolCalls) == 0 {
return nil
}
normalized := make([]providerToolCallDescriptor, 0, len(toolCalls))
for _, toolCall := range toolCalls {
item := providerToolCallDescriptor{
ID: providerToolCallID(toolCall.ID),
Index: toolCall.Index,
Type: toolCall.Type,
Function: toolCall.Function,
}
normalized = append(normalized, item)
}
return normalized
}
func buildProviderSafeToolCallID(namespace string, raw string) string {
trimmedRaw := strings.TrimSpace(raw)
if trimmedRaw == "" {
return ""
}
if namespace == "" && len(trimmedRaw) <= maxProviderToolCallIDLen && !strings.Contains(trimmedRaw, "::") {
return trimmedRaw
}
prefix := "tc"
if namespace != "" {
prefix += "_" + namespace
}
candidate := prefix + "_" + trimmedRaw
if len(candidate) <= maxProviderToolCallIDLen {
return candidate
}
rawHash := shortToolCallHash(trimmedRaw, toolCallValueHashLen)
remaining := maxProviderToolCallIDLen - len(prefix) - len(rawHash) - 2
if remaining <= 0 {
return prefix + "_" + rawHash
}
suffix := trimmedRaw
if len(suffix) > remaining {
suffix = suffix[len(suffix)-remaining:]
}
return prefix + "_" + rawHash + "_" + suffix
}
func splitLegacyToolCallID(value string) (namespace string, raw string, ok bool) {
namespace, raw, ok = strings.Cut(strings.TrimSpace(value), "::")
if !ok {
return "", "", false
}
namespace = strings.TrimSpace(namespace)
raw = strings.TrimSpace(raw)
if namespace == "" || raw == "" {
return "", "", false
}
return namespace, raw, true
}
func shortToolCallHash(value string, size int) string {
sum := sha256.Sum256([]byte(strings.TrimSpace(value)))
encoded := hex.EncodeToString(sum[:])
if size <= 0 || size > len(encoded) {
return encoded
}
return encoded[:size]
}