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:
CJACK
2026-02-19 02:45:38 +08:00
parent d21aedac83
commit 7307a5cc9a
64 changed files with 4078 additions and 967 deletions

View File

@@ -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.

View File

@@ -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)

View File

@@ -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",