| 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) |
| defaultThinkingEnabled, searchEnabled, ok := config.GetModelConfig(dsModel) |
| if !ok { |
| searchEnabled = false |
| } |
| thinkingEnabled := util.ResolveThinkingEnabled(req, defaultThinkingEnabled) |
| 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 |
| } |
|
|
| |
| 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 |
| } |
|
|