mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-18 15:15:08 +08:00
feat: Implement admin settings UI, enhance admin authentication with password hashing, and add new streaming runtime logic for Claude and OpenAI adapters with extensive compatibility tests.
This commit is contained in:
@@ -1,16 +1,11 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"ds2api/internal/claudeconv"
|
||||
"ds2api/internal/config"
|
||||
"ds2api/internal/prompt"
|
||||
)
|
||||
|
||||
var markdownImagePattern = regexp.MustCompile(`!\[(.*?)\]\((.*?)\)`)
|
||||
|
||||
const ClaudeDefaultModel = "claude-sonnet-4-5"
|
||||
|
||||
type Message struct {
|
||||
@@ -19,112 +14,15 @@ type Message struct {
|
||||
}
|
||||
|
||||
func MessagesPrepare(messages []map[string]any) string {
|
||||
type block struct {
|
||||
Role string
|
||||
Text string
|
||||
}
|
||||
processed := make([]block, 0, len(messages))
|
||||
for _, m := range messages {
|
||||
role, _ := m["role"].(string)
|
||||
text := normalizeContent(m["content"])
|
||||
processed = append(processed, block{Role: role, Text: text})
|
||||
}
|
||||
if len(processed) == 0 {
|
||||
return ""
|
||||
}
|
||||
merged := make([]block, 0, len(processed))
|
||||
for _, msg := range processed {
|
||||
if len(merged) > 0 && merged[len(merged)-1].Role == msg.Role {
|
||||
merged[len(merged)-1].Text += "\n\n" + msg.Text
|
||||
continue
|
||||
}
|
||||
merged = append(merged, msg)
|
||||
}
|
||||
parts := make([]string, 0, len(merged))
|
||||
for i, m := range merged {
|
||||
switch m.Role {
|
||||
case "assistant":
|
||||
parts = append(parts, "<|Assistant|>"+m.Text+"<|end▁of▁sentence|>")
|
||||
case "user", "system":
|
||||
if i > 0 {
|
||||
parts = append(parts, "<|User|>"+m.Text)
|
||||
} else {
|
||||
parts = append(parts, m.Text)
|
||||
}
|
||||
default:
|
||||
parts = append(parts, m.Text)
|
||||
}
|
||||
}
|
||||
out := strings.Join(parts, "")
|
||||
return markdownImagePattern.ReplaceAllString(out, `[${1}](${2})`)
|
||||
return prompt.MessagesPrepare(messages)
|
||||
}
|
||||
|
||||
func normalizeContent(v any) string {
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
return x
|
||||
case []any:
|
||||
parts := make([]string, 0, len(x))
|
||||
for _, item := range x {
|
||||
m, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
typeStr, _ := m["type"].(string)
|
||||
typeStr = strings.ToLower(strings.TrimSpace(typeStr))
|
||||
if typeStr == "text" || typeStr == "output_text" || typeStr == "input_text" {
|
||||
if txt, ok := m["text"].(string); ok {
|
||||
parts = append(parts, txt)
|
||||
continue
|
||||
}
|
||||
if txt, ok := m["content"].(string); ok {
|
||||
parts = append(parts, txt)
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "\n")
|
||||
default:
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
return prompt.NormalizeContent(v)
|
||||
}
|
||||
|
||||
func ConvertClaudeToDeepSeek(claudeReq map[string]any, store *config.Store) map[string]any {
|
||||
messages, _ := claudeReq["messages"].([]any)
|
||||
model, _ := claudeReq["model"].(string)
|
||||
if model == "" {
|
||||
model = ClaudeDefaultModel
|
||||
}
|
||||
mapping := store.ClaudeMapping()
|
||||
dsModel := mapping["fast"]
|
||||
if dsModel == "" {
|
||||
dsModel = "deepseek-chat"
|
||||
}
|
||||
modelLower := strings.ToLower(model)
|
||||
if strings.Contains(modelLower, "opus") || strings.Contains(modelLower, "reasoner") || strings.Contains(modelLower, "slow") {
|
||||
if slow := mapping["slow"]; slow != "" {
|
||||
dsModel = slow
|
||||
}
|
||||
}
|
||||
convertedMessages := make([]any, 0, len(messages)+1)
|
||||
if system, ok := claudeReq["system"].(string); ok && system != "" {
|
||||
convertedMessages = append(convertedMessages, map[string]any{"role": "system", "content": system})
|
||||
}
|
||||
convertedMessages = append(convertedMessages, messages...)
|
||||
|
||||
out := map[string]any{"model": dsModel, "messages": convertedMessages}
|
||||
for _, k := range []string{"temperature", "top_p", "stream"} {
|
||||
if v, ok := claudeReq[k]; ok {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
if stopSeq, ok := claudeReq["stop_sequences"]; ok {
|
||||
out["stop"] = stopSeq
|
||||
}
|
||||
return out
|
||||
return claudeconv.ConvertClaudeToDeepSeek(claudeReq, store, ClaudeDefaultModel)
|
||||
}
|
||||
|
||||
// EstimateTokens provides a rough token count approximation.
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// BuildOpenAIChatCompletion is kept for backward compatibility.
|
||||
// Prefer internal/format/openai.BuildChatCompletion for new code.
|
||||
func BuildOpenAIChatCompletion(completionID, model, finalPrompt, finalThinking, finalText string, toolNames []string) map[string]any {
|
||||
detected := ParseToolCalls(finalText, toolNames)
|
||||
finishReason := "stop"
|
||||
@@ -41,6 +43,8 @@ func BuildOpenAIChatCompletion(completionID, model, finalPrompt, finalThinking,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildOpenAIResponseObject is kept for backward compatibility.
|
||||
// Prefer internal/format/openai.BuildResponseObject for new code.
|
||||
func BuildOpenAIResponseObject(responseID, model, finalPrompt, finalThinking, finalText string, toolNames []string) map[string]any {
|
||||
detected := ParseToolCalls(finalText, toolNames)
|
||||
exposedOutputText := finalText
|
||||
@@ -101,6 +105,8 @@ func BuildOpenAIResponseObject(responseID, model, finalPrompt, finalThinking, fi
|
||||
}
|
||||
}
|
||||
|
||||
// BuildClaudeMessageResponse is kept for backward compatibility.
|
||||
// Prefer internal/format/claude.BuildMessageResponse for new code.
|
||||
func BuildClaudeMessageResponse(messageID, model string, normalizedMessages []any, finalThinking, finalText string, toolNames []string) map[string]any {
|
||||
detected := ParseToolCalls(finalText, toolNames)
|
||||
content := make([]map[string]any, 0, 4)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package util
|
||||
|
||||
// BuildOpenAIChatStreamDeltaChoice is kept for backward compatibility.
|
||||
// Prefer internal/format/openai.BuildChatStreamDeltaChoice for new code.
|
||||
func BuildOpenAIChatStreamDeltaChoice(index int, delta map[string]any) map[string]any {
|
||||
return map[string]any{
|
||||
"delta": delta,
|
||||
@@ -7,6 +9,8 @@ func BuildOpenAIChatStreamDeltaChoice(index int, delta map[string]any) map[strin
|
||||
}
|
||||
}
|
||||
|
||||
// BuildOpenAIChatStreamFinishChoice is kept for backward compatibility.
|
||||
// Prefer internal/format/openai.BuildChatStreamFinishChoice for new code.
|
||||
func BuildOpenAIChatStreamFinishChoice(index int, finishReason string) map[string]any {
|
||||
return map[string]any{
|
||||
"delta": map[string]any{},
|
||||
@@ -15,6 +19,8 @@ func BuildOpenAIChatStreamFinishChoice(index int, finishReason string) map[strin
|
||||
}
|
||||
}
|
||||
|
||||
// BuildOpenAIChatStreamChunk is kept for backward compatibility.
|
||||
// Prefer internal/format/openai.BuildChatStreamChunk for new code.
|
||||
func BuildOpenAIChatStreamChunk(completionID string, created int64, model string, choices []map[string]any, usage map[string]any) map[string]any {
|
||||
out := map[string]any{
|
||||
"id": completionID,
|
||||
@@ -29,6 +35,8 @@ func BuildOpenAIChatStreamChunk(completionID string, created int64, model string
|
||||
return out
|
||||
}
|
||||
|
||||
// BuildOpenAIChatUsage is kept for backward compatibility.
|
||||
// Prefer internal/format/openai.BuildChatUsage for new code.
|
||||
func BuildOpenAIChatUsage(finalPrompt, finalThinking, finalText string) map[string]any {
|
||||
promptTokens := EstimateTokens(finalPrompt)
|
||||
reasoningTokens := EstimateTokens(finalThinking)
|
||||
@@ -43,6 +51,8 @@ func BuildOpenAIChatUsage(finalPrompt, finalThinking, finalText string) map[stri
|
||||
}
|
||||
}
|
||||
|
||||
// BuildOpenAIResponsesCreatedPayload is kept for backward compatibility.
|
||||
// Prefer internal/format/openai.BuildResponsesCreatedPayload for new code.
|
||||
func BuildOpenAIResponsesCreatedPayload(responseID, model string) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "response.created",
|
||||
@@ -53,6 +63,8 @@ func BuildOpenAIResponsesCreatedPayload(responseID, model string) map[string]any
|
||||
}
|
||||
}
|
||||
|
||||
// BuildOpenAIResponsesTextDeltaPayload is kept for backward compatibility.
|
||||
// Prefer internal/format/openai.BuildResponsesTextDeltaPayload for new code.
|
||||
func BuildOpenAIResponsesTextDeltaPayload(responseID, delta string) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "response.output_text.delta",
|
||||
@@ -61,6 +73,8 @@ func BuildOpenAIResponsesTextDeltaPayload(responseID, delta string) map[string]a
|
||||
}
|
||||
}
|
||||
|
||||
// BuildOpenAIResponsesReasoningDeltaPayload is kept for backward compatibility.
|
||||
// Prefer internal/format/openai.BuildResponsesReasoningDeltaPayload for new code.
|
||||
func BuildOpenAIResponsesReasoningDeltaPayload(responseID, delta string) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "response.reasoning.delta",
|
||||
@@ -69,6 +83,8 @@ func BuildOpenAIResponsesReasoningDeltaPayload(responseID, delta string) map[str
|
||||
}
|
||||
}
|
||||
|
||||
// BuildOpenAIResponsesToolCallDeltaPayload is kept for backward compatibility.
|
||||
// Prefer internal/format/openai.BuildResponsesToolCallDeltaPayload for new code.
|
||||
func BuildOpenAIResponsesToolCallDeltaPayload(responseID string, toolCalls []map[string]any) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "response.output_tool_call.delta",
|
||||
@@ -77,6 +93,8 @@ func BuildOpenAIResponsesToolCallDeltaPayload(responseID string, toolCalls []map
|
||||
}
|
||||
}
|
||||
|
||||
// BuildOpenAIResponsesToolCallDonePayload is kept for backward compatibility.
|
||||
// Prefer internal/format/openai.BuildResponsesToolCallDonePayload for new code.
|
||||
func BuildOpenAIResponsesToolCallDonePayload(responseID string, toolCalls []map[string]any) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "response.output_tool_call.done",
|
||||
@@ -85,6 +103,8 @@ func BuildOpenAIResponsesToolCallDonePayload(responseID string, toolCalls []map[
|
||||
}
|
||||
}
|
||||
|
||||
// BuildOpenAIResponsesCompletedPayload is kept for backward compatibility.
|
||||
// Prefer internal/format/openai.BuildResponsesCompletedPayload for new code.
|
||||
func BuildOpenAIResponsesCompletedPayload(response map[string]any) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "response.completed",
|
||||
|
||||
Reference in New Issue
Block a user