Files
dynagate/handler.go
T
2026-07-30 09:53:36 +03:00

857 lines
24 KiB
Go

// Dynagate LLM request proxying handlers
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
)
var httpClient = &http.Client{}
func checkAuth(expectedTokens []string, r *http.Request) bool {
if expectedTokens == nil {
return true
}
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
return false
}
token := strings.TrimPrefix(authHeader, "Bearer ")
for _, expected := range expectedTokens {
if token == expected {
return true
}
}
return false
}
func sendUnauthorized(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "Incorrect API key provided.",
"type": "invalid_request_error",
"param": nil,
"code": "invalid_api_key",
},
})
}
func handleModels(cm *ConfigManager, expectedTokens []string) http.HandlerFunc {
type ModelData struct {
ID string `json:"id"`
Object string `json:"object"`
Created int64 `json:"created"`
OwnedBy string `json:"owned_by"`
}
type ModelsResponse struct {
Object string `json:"object"`
Data []ModelData `json:"data"`
}
return func(w http.ResponseWriter, r *http.Request) {
if !checkAuth(expectedTokens, r) {
sendUnauthorized(w)
return
}
if r.Method != http.MethodGet {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusMethodNotAllowed)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "Method not allowed",
"type": "invalid_request_error",
},
})
return
}
models := cm.GetUniqueModels()
data := make([]ModelData, len(models))
for i, m := range models {
data[i] = ModelData{
ID: m,
Object: "model",
Created: 1686935002,
OwnedBy: "dynagate",
}
}
resp := ModelsResponse{
Object: "list",
Data: data,
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}
}
func handleChatCompletions(cm *ConfigManager, expectedTokens []string, retryBaseDelay ...time.Duration) http.HandlerFunc {
baseDelay := 100 * time.Millisecond
if len(retryBaseDelay) > 0 {
baseDelay = retryBaseDelay[0]
}
return func(w http.ResponseWriter, r *http.Request) {
if !checkAuth(expectedTokens, r) {
sendUnauthorized(w)
return
}
if r.Method != http.MethodPost {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusMethodNotAllowed)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "Method not allowed",
"type": "invalid_request_error",
},
})
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "Failed to read request body",
"type": "invalid_request_error",
},
})
return
}
var bodyMap map[string]any
if err := json.Unmarshal(bodyBytes, &bodyMap); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "Invalid JSON in request body",
"type": "invalid_request_error",
},
})
return
}
autodetectAndNormalizeMessages(bodyMap)
var requestedModel string
if m, ok := bodyMap["model"]; ok {
if s, ok := m.(string); ok {
requestedModel = s
}
}
configs := cm.GetConfigs()
uniqueModels := cm.GetUniqueModels()
if len(configs) == 0 {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "No model configurations loaded",
"type": "gateway_error",
},
})
return
}
trialConfigs := getTrialConfigs(configs, uniqueModels, requestedModel)
if len(trialConfigs) == 0 {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "No valid trial configuration candidates",
"type": "gateway_error",
},
})
return
}
var isStream bool
if s, ok := bodyMap["stream"]; ok {
if b, ok := s.(bool); ok {
isStream = b
}
}
log.Printf("Received completion request for model %q (stream=%t). Found %d config trials.", requestedModel, isStream, len(trialConfigs))
for i, trial := range trialConfigs {
if i > 0 && baseDelay > 0 {
delay := time.Duration(fibonacci(i)) * baseDelay
log.Printf("Trial %d/%d: Fibonacci backoff delay of %v before retry...", i+1, len(trialConfigs), delay)
select {
case <-r.Context().Done():
log.Printf("Request context cancelled during retry delay before trial %d", i+1)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(499)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "Client closed request",
"type": "gateway_error",
},
})
return
case <-time.After(delay):
}
}
log.Printf("Trial %d/%d: model=%s endpoint=%s key_len=%d", i+1, len(trialConfigs), trial.Model, trial.Endpoint, len(trial.Key))
bodyMap["model"] = trial.Model
modifiedBody, err := json.Marshal(bodyMap)
if err != nil {
log.Printf("Trial %d: Failed to marshal body for %s: %v", i+1, trial.Model, err)
continue
}
targetURL := buildURL(trial.Endpoint, r.URL.Path)
outReq, err := http.NewRequestWithContext(r.Context(), "POST", targetURL, bytes.NewReader(modifiedBody))
if err != nil {
log.Printf("Trial %d: Failed to create outgoing request to %s: %v", i+1, targetURL, err)
continue
}
// Copy headers from incoming request, excluding host and auth
for k, vv := range r.Header {
kLower := strings.ToLower(k)
if kLower == "authorization" || kLower == "host" || kLower == "content-length" {
continue
}
for _, v := range vv {
outReq.Header.Add(k, v)
}
}
if trial.Key == "-blank-" {
outReq.Header.Set("Authorization", "Bearer")
} else if trial.Key != "" && trial.Key != "-" {
outReq.Header.Set("Authorization", "Bearer "+trial.Key)
}
outReq.Header.Set("Content-Type", "application/json")
if trial.Extra != "" {
var extraHeaders map[string]any
if err := json.Unmarshal([]byte(trial.Extra), &extraHeaders); err != nil {
log.Printf("Trial %d: Failed to parse extra headers JSON: %v", i+1, err)
} else {
for hk, hv := range extraHeaders {
var valStr string
switch v := hv.(type) {
case string:
valStr = v
default:
valStr = fmt.Sprintf("%v", v)
}
outReq.Header.Set(hk, valStr)
}
}
}
resp, err := httpClient.Do(outReq)
if err != nil {
log.Printf("Trial %d: Request to %s failed: %v", i+1, targetURL, err)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
errBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
resp.Body.Close()
log.Printf("Trial %d: Request to %s returned error status %d: %s", i+1, targetURL, resp.StatusCode, strings.TrimSpace(string(errBody)))
continue
}
log.Printf("Trial %d: Connection established with status %d. Proxying response.", i+1, resp.StatusCode)
if !isStream {
respBody, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
log.Printf("Trial %d: Failed to read non-streaming response body: %v", i+1, readErr)
continue
}
for k, vv := range resp.Header {
for _, v := range vv {
w.Header().Add(k, v)
}
}
w.WriteHeader(resp.StatusCode)
_, _ = w.Write(respBody)
return
}
// Handle streaming
defer resp.Body.Close()
flusher, ok := w.(http.Flusher)
if !ok {
log.Printf("Trial %d: Flusher not supported on current ResponseWriter", i+1)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "Response flusher not supported",
"type": "gateway_error",
},
})
return
}
for k, vv := range resp.Header {
for _, v := range vv {
w.Header().Add(k, v)
}
}
w.WriteHeader(resp.StatusCode)
flusher.Flush()
buf := make([]byte, 4096)
for {
n, rErr := resp.Body.Read(buf)
if n > 0 {
if _, wErr := w.Write(buf[:n]); wErr != nil {
log.Printf("Trial %d: Client disconnected or write error: %v", i+1, wErr)
return
}
flusher.Flush()
}
if rErr != nil {
if rErr != io.EOF {
log.Printf("Trial %d: Error reading response stream: %v", i+1, rErr)
}
break
}
}
return
}
log.Printf("All trials failed. Returning Bad Gateway.")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadGateway)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "All configured models and keys failed to respond.",
"type": "gateway_error",
"param": nil,
"code": "all_endpoints_failed",
},
})
}
}
func handleImageGenerations(cm *ConfigManager, expectedTokens []string, retryBaseDelay ...time.Duration) http.HandlerFunc {
baseDelay := 100 * time.Millisecond
if len(retryBaseDelay) > 0 {
baseDelay = retryBaseDelay[0]
}
return func(w http.ResponseWriter, r *http.Request) {
if !checkAuth(expectedTokens, r) {
sendUnauthorized(w)
return
}
if r.Method != http.MethodPost {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusMethodNotAllowed)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "Method not allowed",
"type": "invalid_request_error",
},
})
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "Failed to read request body",
"type": "invalid_request_error",
},
})
return
}
var bodyMap map[string]any
if err := json.Unmarshal(bodyBytes, &bodyMap); err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "Invalid JSON in request body",
"type": "invalid_request_error",
},
})
return
}
if p, ok := bodyMap["prompt"]; ok && p != nil {
bodyMap["prompt"] = normalizeContent(p)
}
var requestedModel string
if m, ok := bodyMap["model"]; ok {
if s, ok := m.(string); ok {
requestedModel = s
}
}
configs := cm.GetConfigs()
uniqueModels := cm.GetUniqueModels()
if len(configs) == 0 {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "No model configurations loaded",
"type": "gateway_error",
},
})
return
}
trialConfigs := getTrialConfigs(configs, uniqueModels, requestedModel)
if len(trialConfigs) == 0 {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "No valid trial configuration candidates",
"type": "gateway_error",
},
})
return
}
log.Printf("Received image generation request for model %q. Found %d config trials.", requestedModel, len(trialConfigs))
for i, trial := range trialConfigs {
if i > 0 && baseDelay > 0 {
delay := time.Duration(fibonacci(i)) * baseDelay
log.Printf("Trial %d/%d: Fibonacci backoff delay of %v before retry...", i+1, len(trialConfigs), delay)
select {
case <-r.Context().Done():
log.Printf("Request context cancelled during retry delay before trial %d", i+1)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(499)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "Client closed request",
"type": "gateway_error",
},
})
return
case <-time.After(delay):
}
}
log.Printf("Trial %d/%d: model=%s endpoint=%s key_len=%d", i+1, len(trialConfigs), trial.Model, trial.Endpoint, len(trial.Key))
bodyMap["model"] = trial.Model
modifiedBody, err := json.Marshal(bodyMap)
if err != nil {
log.Printf("Trial %d: Failed to marshal body for %s: %v", i+1, trial.Model, err)
continue
}
targetURL := buildURL(trial.Endpoint, r.URL.Path)
outReq, err := http.NewRequestWithContext(r.Context(), "POST", targetURL, bytes.NewReader(modifiedBody))
if err != nil {
log.Printf("Trial %d: Failed to create outgoing request to %s: %v", i+1, targetURL, err)
continue
}
for k, vv := range r.Header {
kLower := strings.ToLower(k)
if kLower == "authorization" || kLower == "host" || kLower == "content-length" {
continue
}
for _, v := range vv {
outReq.Header.Add(k, v)
}
}
if trial.Key == "-blank-" {
outReq.Header.Set("Authorization", "Bearer")
} else if trial.Key != "" && trial.Key != "-" {
outReq.Header.Set("Authorization", "Bearer "+trial.Key)
}
outReq.Header.Set("Content-Type", "application/json")
if trial.Extra != "" {
var extraHeaders map[string]any
if err := json.Unmarshal([]byte(trial.Extra), &extraHeaders); err != nil {
log.Printf("Trial %d: Failed to parse extra headers JSON: %v", i+1, err)
} else {
for hk, hv := range extraHeaders {
var valStr string
switch v := hv.(type) {
case string:
valStr = v
default:
valStr = fmt.Sprintf("%v", v)
}
outReq.Header.Set(hk, valStr)
}
}
}
resp, err := httpClient.Do(outReq)
if err != nil {
log.Printf("Trial %d: Request to %s failed: %v", i+1, targetURL, err)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
errBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
resp.Body.Close()
log.Printf("Trial %d: Request to %s returned error status %d: %s", i+1, targetURL, resp.StatusCode, strings.TrimSpace(string(errBody)))
continue
}
log.Printf("Trial %d: Connection established with status %d. Proxying response.", i+1, resp.StatusCode)
respBody, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
log.Printf("Trial %d: Failed to read response body: %v", i+1, readErr)
continue
}
for k, vv := range resp.Header {
for _, v := range vv {
w.Header().Add(k, v)
}
}
w.WriteHeader(resp.StatusCode)
_, _ = w.Write(respBody)
return
}
log.Printf("All trials failed. Returning Bad Gateway.")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadGateway)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{
"message": "All configured models and keys failed to respond.",
"type": "gateway_error",
"param": nil,
"code": "all_endpoints_failed",
},
})
}
}
func getTrialConfigs(configs []ModelConfig, uniqueModels []string, requestedModel string) []ModelConfig {
if len(configs) == 0 {
return nil
}
reqIdx := -1
for i, m := range uniqueModels {
if m == requestedModel {
reqIdx = i
break
}
}
var orderedModels []string
if reqIdx != -1 {
orderedModels = append(orderedModels, uniqueModels[reqIdx:]...)
orderedModels = append(orderedModels, uniqueModels[:reqIdx]...)
} else {
orderedModels = uniqueModels
}
var trialConfigs []ModelConfig
for _, modelName := range orderedModels {
for _, cfg := range configs {
if cfg.Model == modelName {
trialConfigs = append(trialConfigs, cfg)
}
}
}
return trialConfigs
}
func buildURL(endpoint string, reqPath string) string {
endpoint = strings.TrimSuffix(endpoint, "/")
reqPath = strings.TrimPrefix(reqPath, "/")
if strings.HasSuffix(endpoint, "/v1") {
if strings.HasPrefix(reqPath, "v1/") {
reqPath = strings.TrimPrefix(reqPath, "v1/")
}
}
return endpoint + "/" + reqPath
}
func isMediaContent(m map[string]any) bool {
if t, ok := m["type"].(string); ok {
tLower := strings.ToLower(t)
switch tLower {
case "image_url", "image", "input_audio", "audio", "file", "document", "video":
return true
}
}
for k := range m {
kLower := strings.ToLower(k)
switch kLower {
case "image_url", "input_audio", "inline_data", "file_data":
return true
}
}
return false
}
func normalizeContent(contentAny any) any {
if contentAny == nil {
return nil
}
switch v := contentAny.(type) {
case string:
return v
case []any:
if len(v) == 0 {
return ""
}
hasMedia := false
for _, item := range v {
if m, ok := item.(map[string]any); ok {
if isMediaContent(m) {
hasMedia = true
break
}
}
}
if hasMedia {
var normSlice []any
for _, item := range v {
if m, ok := item.(map[string]any); ok {
if t, ok := m["type"].(string); ok && strings.ToLower(t) == "text" {
textVal, _ := m["text"].(string)
normSlice = append(normSlice, map[string]any{
"type": "text",
"text": textVal,
})
} else {
normSlice = append(normSlice, m)
}
} else {
normSlice = append(normSlice, item)
}
}
return normSlice
}
var textParts []string
for _, item := range v {
switch elem := item.(type) {
case string:
textParts = append(textParts, elem)
case map[string]any:
if txt, ok := elem["text"].(string); ok {
textParts = append(textParts, txt)
} else if txt, ok := elem["content"].(string); ok {
textParts = append(textParts, txt)
}
}
}
var sb strings.Builder
for _, part := range textParts {
if part == "" {
continue
}
if sb.Len() > 0 {
lastChar := sb.String()[sb.Len()-1]
firstChar := part[0]
if lastChar != '\n' && lastChar != ' ' && firstChar != '\n' && firstChar != ' ' {
sb.WriteString("\n")
}
}
sb.WriteString(part)
}
return sb.String()
case map[string]any:
if isMediaContent(v) {
return []any{v}
}
if txt, ok := v["text"].(string); ok {
return txt
}
if txt, ok := v["content"].(string); ok {
return txt
}
return v
default:
return contentAny
}
}
func autodetectAndNormalizeMessages(bodyMap map[string]any) {
if bodyMap == nil {
return
}
var systemMsg map[string]any
if sysVal, ok := bodyMap["system"]; ok && sysVal != nil {
sysText := normalizeContent(sysVal)
if sysStr, isStr := sysText.(string); isStr && sysStr != "" {
systemMsg = map[string]any{
"role": "system",
"content": sysStr,
}
} else if sysSlice, isSlice := sysText.([]any); isSlice && len(sysSlice) > 0 {
systemMsg = map[string]any{
"role": "system",
"content": sysSlice,
}
}
delete(bodyMap, "system")
}
var rawMessages any
var sourceKey string
if msgs, ok := bodyMap["messages"]; ok && msgs != nil {
rawMessages = msgs
sourceKey = "messages"
} else if prompt, ok := bodyMap["prompt"]; ok && prompt != nil {
rawMessages = prompt
sourceKey = "prompt"
delete(bodyMap, "prompt")
} else if contents, ok := bodyMap["contents"]; ok && contents != nil {
rawMessages = contents
sourceKey = "contents"
delete(bodyMap, "contents")
} else if input, ok := bodyMap["input"]; ok && input != nil {
rawMessages = input
sourceKey = "input"
delete(bodyMap, "input")
}
if rawMessages == nil && systemMsg == nil {
return
}
var msgList []map[string]any
switch v := rawMessages.(type) {
case []any:
for _, item := range v {
switch elem := item.(type) {
case map[string]any:
msgList = append(msgList, elem)
case string:
msgList = append(msgList, map[string]any{
"role": "user",
"content": elem,
})
}
}
case map[string]any:
msgList = append(msgList, v)
case string:
if v != "" {
msgList = append(msgList, map[string]any{
"role": "user",
"content": v,
})
}
}
var normalizedList []any
hasSystemInList := false
for _, msg := range msgList {
role, _ := msg["role"].(string)
if role == "" {
role = "user"
}
if role == "system" {
hasSystemInList = true
}
normMsg := make(map[string]any)
for k, val := range msg {
normMsg[k] = val
}
normMsg["role"] = role
if cnt, ok := msg["content"]; ok {
normMsg["content"] = normalizeContent(cnt)
} else if parts, ok := msg["parts"]; ok {
normMsg["content"] = normalizeContent(parts)
delete(normMsg, "parts")
}
normalizedList = append(normalizedList, normMsg)
}
if systemMsg != nil {
if !hasSystemInList {
normalizedList = append([]any{systemMsg}, normalizedList...)
} else if len(normalizedList) > 0 {
if firstMsg, ok := normalizedList[0].(map[string]any); ok && firstMsg["role"] == "system" {
existingSys := normalizeContent(firstMsg["content"])
if sysStr, ok := systemMsg["content"].(string); ok {
if exStr, ok := existingSys.(string); ok && exStr != "" {
firstMsg["content"] = sysStr + "\n" + exStr
} else {
firstMsg["content"] = sysStr
}
}
}
}
}
if len(normalizedList) > 0 || sourceKey != "" {
bodyMap["messages"] = normalizedList
}
}
func fibonacci(n int) int64 {
if n <= 0 {
return 0
}
if n == 1 || n == 2 {
return 1
}
var a, b int64 = 1, 1
for i := 3; i <= n; i++ {
a, b = b, a+b
}
return b
}