mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-05 08:55:28 +08:00
Unify Claude count_tokens, legacy stream accounting, and legacy render usage with preserved prompt text so Claude stops falling back to lossy message formatting.
123 lines
3.5 KiB
Go
123 lines
3.5 KiB
Go
package claude
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"ds2api/internal/config"
|
|
"ds2api/internal/prompt"
|
|
"ds2api/internal/promptcompat"
|
|
"ds2api/internal/util"
|
|
)
|
|
|
|
type claudeNormalizedRequest struct {
|
|
Standard promptcompat.StandardRequest
|
|
NormalizedMessages []any
|
|
}
|
|
|
|
func normalizeClaudeRequest(store ConfigReader, req map[string]any) (claudeNormalizedRequest, error) {
|
|
model, _ := req["model"].(string)
|
|
messagesRaw, _ := req["messages"].([]any)
|
|
if strings.TrimSpace(model) == "" || len(messagesRaw) == 0 {
|
|
return claudeNormalizedRequest{}, fmt.Errorf("request must include 'model' and 'messages'")
|
|
}
|
|
if _, ok := req["max_tokens"]; !ok {
|
|
req["max_tokens"] = 8192
|
|
}
|
|
normalizedMessages := normalizeClaudeMessages(messagesRaw)
|
|
payload := cloneMap(req)
|
|
payload["messages"] = normalizedMessages
|
|
toolsRequested, _ := req["tools"].([]any)
|
|
payload["messages"] = injectClaudeToolPrompt(payload, normalizedMessages, toolsRequested)
|
|
|
|
dsPayload := convertClaudeToDeepSeek(payload, store)
|
|
dsModel, _ := dsPayload["model"].(string)
|
|
_, searchEnabled, ok := config.GetModelConfig(dsModel)
|
|
if !ok {
|
|
searchEnabled = false
|
|
}
|
|
thinkingEnabled := util.ResolveThinkingEnabled(req, false)
|
|
if config.IsNoThinkingModel(dsModel) {
|
|
thinkingEnabled = false
|
|
}
|
|
finalPrompt := prompt.MessagesPrepareWithThinking(toMessageMaps(dsPayload["messages"]), thinkingEnabled)
|
|
toolNames := extractClaudeToolNames(toolsRequested)
|
|
if len(toolNames) == 0 && len(toolsRequested) > 0 {
|
|
toolNames = []string{"__any_tool__"}
|
|
}
|
|
|
|
return claudeNormalizedRequest{
|
|
Standard: promptcompat.StandardRequest{
|
|
Surface: "anthropic_messages",
|
|
RequestedModel: strings.TrimSpace(model),
|
|
ResolvedModel: dsModel,
|
|
ResponseModel: strings.TrimSpace(model),
|
|
Messages: payload["messages"].([]any),
|
|
PromptTokenText: finalPrompt,
|
|
ToolsRaw: toolsRequested,
|
|
FinalPrompt: finalPrompt,
|
|
ToolNames: toolNames,
|
|
Stream: util.ToBool(req["stream"]),
|
|
Thinking: thinkingEnabled,
|
|
Search: searchEnabled,
|
|
},
|
|
NormalizedMessages: normalizedMessages,
|
|
}, nil
|
|
}
|
|
|
|
func injectClaudeToolPrompt(payload map[string]any, normalizedMessages []any, tools []any) []any {
|
|
if len(tools) == 0 {
|
|
return normalizedMessages
|
|
}
|
|
toolPrompt := strings.TrimSpace(buildClaudeToolPrompt(tools))
|
|
if toolPrompt == "" {
|
|
return normalizedMessages
|
|
}
|
|
|
|
// Prefer top-level Anthropic-style system prompt when available.
|
|
if systemText, ok := payload["system"].(string); ok && strings.TrimSpace(systemText) != "" {
|
|
payload["system"] = mergeSystemPrompt(systemText, toolPrompt)
|
|
return normalizedMessages
|
|
}
|
|
|
|
messages := cloneAnySlice(normalizedMessages)
|
|
for i := range messages {
|
|
msg, ok := messages[i].(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
role, _ := msg["role"].(string)
|
|
if !strings.EqualFold(strings.TrimSpace(role), "system") {
|
|
continue
|
|
}
|
|
copied := cloneMap(msg)
|
|
copied["content"] = mergeSystemPrompt(strings.TrimSpace(fmt.Sprintf("%v", copied["content"])), toolPrompt)
|
|
messages[i] = copied
|
|
return messages
|
|
}
|
|
|
|
return append([]any{map[string]any{"role": "system", "content": toolPrompt}}, messages...)
|
|
}
|
|
|
|
func mergeSystemPrompt(base, extra string) string {
|
|
base = strings.TrimSpace(base)
|
|
extra = strings.TrimSpace(extra)
|
|
switch {
|
|
case base == "":
|
|
return extra
|
|
case extra == "":
|
|
return base
|
|
default:
|
|
return base + "\n\n" + extra
|
|
}
|
|
}
|
|
|
|
func cloneAnySlice(in []any) []any {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]any, len(in))
|
|
copy(out, in)
|
|
return out
|
|
}
|