mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-04 00:15:28 +08:00
198 lines
5.4 KiB
Go
198 lines
5.4 KiB
Go
package admin
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"ds2api/internal/config"
|
|
"ds2api/internal/devcapture"
|
|
"ds2api/internal/rawsample"
|
|
)
|
|
|
|
func (h *Handler) captureRawSample(w http.ResponseWriter, r *http.Request) {
|
|
if h.OpenAI == nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]any{"detail": "OpenAI handler is not configured"})
|
|
return
|
|
}
|
|
|
|
var req map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": "invalid json"})
|
|
return
|
|
}
|
|
|
|
payload, sampleID, apiKey, err := prepareRawSampleCaptureRequest(h.Store, req)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()})
|
|
return
|
|
}
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]any{"detail": "failed to encode capture request"})
|
|
return
|
|
}
|
|
|
|
traceID := rawsample.NormalizeSampleID(sampleID)
|
|
if traceID == "" {
|
|
traceID = rawsample.DefaultSampleID("capture")
|
|
}
|
|
|
|
before := devcapture.Global().Snapshot()
|
|
rec := httptest.NewRecorder()
|
|
captureReq := httptest.NewRequest(http.MethodPost, "/v1/chat/completions?__trace_id="+url.QueryEscape(traceID), bytes.NewReader(body))
|
|
captureReq.Header.Set("Authorization", "Bearer "+apiKey)
|
|
captureReq.Header.Set("Content-Type", "application/json")
|
|
h.OpenAI.ChatCompletions(rec, captureReq)
|
|
after := devcapture.Global().Snapshot()
|
|
|
|
if rec.Code >= http.StatusBadRequest {
|
|
copyHeader(w.Header(), rec.Header())
|
|
w.WriteHeader(rec.Code)
|
|
_, _ = io.Copy(w, bytes.NewReader(rec.Body.Bytes()))
|
|
return
|
|
}
|
|
|
|
captureEntry, ok := selectNewestCaptureEntry(before, after)
|
|
if !ok {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]any{"detail": "no upstream capture was recorded"})
|
|
return
|
|
}
|
|
|
|
saved, err := rawsample.Persist(rawsample.PersistOptions{
|
|
RootDir: config.RawStreamSampleRoot(),
|
|
SampleID: sampleID,
|
|
Source: "admin/dev/raw-samples/capture",
|
|
Request: payload,
|
|
Capture: captureSummaryFromEntry(captureEntry),
|
|
UpstreamBody: []byte(captureEntry.ResponseBody),
|
|
})
|
|
if err != nil {
|
|
writeJSON(w, http.StatusInternalServerError, map[string]any{"detail": err.Error()})
|
|
return
|
|
}
|
|
|
|
copyHeader(w.Header(), rec.Header())
|
|
w.Header().Set("X-Ds2-Sample-Id", saved.SampleID)
|
|
w.Header().Set("X-Ds2-Sample-Dir", saved.Dir)
|
|
w.Header().Set("X-Ds2-Sample-Meta", saved.MetaPath)
|
|
w.Header().Set("X-Ds2-Sample-Upstream", saved.UpstreamPath)
|
|
w.WriteHeader(rec.Code)
|
|
_, _ = io.Copy(w, bytes.NewReader(rec.Body.Bytes()))
|
|
}
|
|
|
|
func prepareRawSampleCaptureRequest(store ConfigStore, req map[string]any) (map[string]any, string, string, error) {
|
|
payload := cloneMap(req)
|
|
sampleID := strings.TrimSpace(fieldString(payload, "sample_id"))
|
|
apiKey := strings.TrimSpace(fieldString(payload, "api_key"))
|
|
|
|
for _, k := range []string{"sample_id", "api_key", "promote_default", "persist", "source"} {
|
|
delete(payload, k)
|
|
}
|
|
|
|
if apiKey == "" {
|
|
if store == nil {
|
|
return nil, "", "", fmt.Errorf("no api key provided")
|
|
}
|
|
keys := store.Keys()
|
|
if len(keys) == 0 {
|
|
return nil, "", "", fmt.Errorf("no api key available")
|
|
}
|
|
apiKey = strings.TrimSpace(keys[0])
|
|
}
|
|
|
|
if model := strings.TrimSpace(fieldString(payload, "model")); model == "" {
|
|
payload["model"] = "deepseek-chat"
|
|
}
|
|
if _, ok := payload["stream"]; !ok {
|
|
payload["stream"] = true
|
|
}
|
|
|
|
if messagesRaw, ok := payload["messages"].([]any); !ok || len(messagesRaw) == 0 {
|
|
message := strings.TrimSpace(fieldString(payload, "message"))
|
|
if message == "" {
|
|
message = "你好"
|
|
}
|
|
payload["messages"] = []map[string]any{{"role": "user", "content": message}}
|
|
}
|
|
delete(payload, "message")
|
|
|
|
if sampleID == "" {
|
|
model := strings.TrimSpace(fieldString(payload, "model"))
|
|
if model == "" {
|
|
model = "capture"
|
|
}
|
|
sampleID = rawsample.DefaultSampleID(model)
|
|
}
|
|
|
|
return payload, sampleID, apiKey, nil
|
|
}
|
|
|
|
func selectNewestCaptureEntry(before, after []devcapture.Entry) (devcapture.Entry, bool) {
|
|
beforeIDs := make(map[string]struct{}, len(before))
|
|
for _, entry := range before {
|
|
beforeIDs[entry.ID] = struct{}{}
|
|
}
|
|
candidates := make([]devcapture.Entry, 0, len(after))
|
|
for _, entry := range after {
|
|
if _, ok := beforeIDs[entry.ID]; ok {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(entry.ResponseBody) == "" {
|
|
continue
|
|
}
|
|
candidates = append(candidates, entry)
|
|
}
|
|
if len(candidates) == 0 {
|
|
candidates = append(candidates, after...)
|
|
}
|
|
if len(candidates) == 0 {
|
|
return devcapture.Entry{}, false
|
|
}
|
|
best := candidates[0]
|
|
bestScore := len(best.ResponseBody)
|
|
for _, entry := range candidates[1:] {
|
|
score := len(entry.ResponseBody)
|
|
if entry.CreatedAt > best.CreatedAt || (entry.CreatedAt == best.CreatedAt && score > bestScore) {
|
|
best = entry
|
|
bestScore = score
|
|
}
|
|
}
|
|
return best, true
|
|
}
|
|
|
|
func captureSummaryFromEntry(entry devcapture.Entry) rawsample.CaptureSummary {
|
|
return rawsample.CaptureSummary{
|
|
Label: strings.TrimSpace(entry.Label),
|
|
URL: strings.TrimSpace(entry.URL),
|
|
StatusCode: entry.StatusCode,
|
|
ResponseBytes: len(entry.ResponseBody),
|
|
}
|
|
}
|
|
|
|
func copyHeader(dst, src http.Header) {
|
|
for k, vv := range src {
|
|
dst.Del(k)
|
|
for _, v := range vv {
|
|
dst.Add(k, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func cloneMap(in map[string]any) map[string]any {
|
|
if len(in) == 0 {
|
|
return map[string]any{}
|
|
}
|
|
out := make(map[string]any, len(in))
|
|
for k, v := range in {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|