switched to fib backoff

This commit is contained in:
Luxferre
2026-07-30 09:53:36 +03:00
parent 1cc6ddb2e4
commit d49339d770
4 changed files with 519 additions and 5 deletions
+301 -2
View File
@@ -10,6 +10,7 @@ import (
"log"
"net/http"
"strings"
"time"
)
var httpClient = &http.Client{}
@@ -96,7 +97,12 @@ func handleModels(cm *ConfigManager, expectedTokens []string) http.HandlerFunc {
}
}
func handleChatCompletions(cm *ConfigManager, expectedTokens []string) http.HandlerFunc {
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)
@@ -141,6 +147,8 @@ func handleChatCompletions(cm *ConfigManager, expectedTokens []string) http.Hand
return
}
autodetectAndNormalizeMessages(bodyMap)
var requestedModel string
if m, ok := bodyMap["model"]; ok {
if s, ok := m.(string); ok {
@@ -186,6 +194,25 @@ func handleChatCompletions(cm *ConfigManager, expectedTokens []string) http.Hand
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
@@ -329,7 +356,12 @@ func handleChatCompletions(cm *ConfigManager, expectedTokens []string) http.Hand
}
}
func handleImageGenerations(cm *ConfigManager, expectedTokens []string) http.HandlerFunc {
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)
@@ -374,6 +406,10 @@ func handleImageGenerations(cm *ConfigManager, expectedTokens []string) http.Han
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 {
@@ -412,6 +448,25 @@ func handleImageGenerations(cm *ConfigManager, expectedTokens []string) http.Han
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
@@ -555,3 +610,247 @@ func buildURL(endpoint string, reqPath string) string {
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
}