feat: use Halvo78 playground as primary default endpoint with automatic failover

This commit is contained in:
Luxferre
2026-09-07 07:22:01 +03:00
parent 404779f8e8
commit f52e07f700
3 changed files with 146 additions and 32 deletions
+41 -16
View File
@@ -32,6 +32,7 @@ var (
ConfiguredModel string
EnableThinkingDefault = true
DefaultEndpoints = []string{
"https://halvo78-qwen3-8-flash-next-playground.hf.space",
"https://wanyamaelis-qwen3-8-27b.hf.space",
"https://apathy-exe-qwen3-8-27b.hf.space",
"https://apathy-exe-qwen3-8-flash-next.hf.space",
@@ -516,7 +517,7 @@ func EffectiveModelID(reqModel string, defaultModel string) string {
if defaultModel != "" {
return defaultModel
}
return "Qwen/Qwen3.8-27B"
return "Qwen/Qwen3.8-Flash-Next"
default:
return clean
}
@@ -1251,7 +1252,7 @@ func parseEndpointList(rawList []string) []*EndpointNode {
func NewQwenService(endpoints []string, modelName, mode, token, apiKey, baseURL, socksProxy string, enableThinking, autoFailover bool) *QwenService {
if modelName == "" {
modelName = "Qwen/Qwen3.8-27B"
modelName = "Qwen/Qwen3.8-Flash-Next"
}
if mode == "" {
mode = "auto"
@@ -1288,19 +1289,19 @@ func (s *QwenService) ListModels() []ModelItem {
now := time.Now().Unix()
primaryID := s.modelName
if primaryID == "" {
primaryID = "Qwen/Qwen3.8-27B"
primaryID = "Qwen/Qwen3.8-Flash-Next"
}
candidates := []string{
primaryID,
"Qwen/Qwen3.8-Flash-Next",
"Qwen/Qwen3.8-27B",
"Qwen/Qwen3.8-27B-Uncensored",
"Qwen/Qwen3.8-Flash-Next",
"qwen3.8-27b",
"qwen3.8-27b-uncensored",
"qwen3.8-flash-next",
"qwen-flash-next",
"qwen-flash",
"qwen3.8-27b",
"qwen3.8-27b-uncensored",
"qwen",
}
@@ -1400,6 +1401,9 @@ func (s *QwenService) markEndpointFailure(node *EndpointNode, err error) {
if strings.Contains(errStr, "zerogpu") || strings.Contains(errStr, "quota") || strings.Contains(errStr, "429") {
node.CooldownUntil = time.Now().Add(5 * time.Minute)
log.Printf("Endpoint %s hit quota/rate limit, cooling down until %s", node.URL, node.CooldownUntil.Format("15:04:05"))
} else if strings.Contains(errStr, "sandbox") {
node.CooldownUntil = time.Now().Add(5 * time.Minute)
log.Printf("Endpoint %s is in sandbox mode (live inference unavailable), cooling down until %s", node.URL, node.CooldownUntil.Format("15:04:05"))
} else {
// For other transient errors, cool down for 30 seconds
node.CooldownUntil = time.Now().Add(30 * time.Second)
@@ -2089,6 +2093,10 @@ func (s *QwenService) chatGradioChatResponse(endpointURL string, w http.Response
}
}
if s.autoFailover && (strings.Contains(finalRawText, "QSA Micro-block Reasoning") || strings.Contains(finalRawText, "Sandbox Response") || strings.Contains(finalRawText, "To connect to a live inference engine")) {
return fmt.Errorf("upstream space %s is in sandbox simulation mode (live inference not configured)", endpointURL)
}
cleanedReasoning, cleanedContent := SeparateReasoningAndContent(finalRawText)
toolCalls, remContent, hasToolCalls := DetectToolCalls(cleanedContent)
@@ -2114,27 +2122,27 @@ func (s *QwenService) chatGradioChatResponse(endpointURL string, w http.Response
}
// Streaming completion
flusher, _ := w.(http.Flusher)
streamer := NewStreamer(w, flusher, completionID, createdTime, resolvedModel)
streamer.Role()
reader := bufio.NewReader(streamResp.Body)
var currentEvent string
var streamer *Streamer
var emittedReasoning string
var emittedContent string
toolFilter := &StreamToolCallFilter{}
onContentChunk := func(text string) {
if text != "" {
if text != "" && streamer != nil {
streamer.Content(text)
}
}
onToolCallChunk := func(tc ToolCall) {
streamer.ToolCallDelta(tc)
if streamer != nil {
streamer.ToolCallDelta(tc)
}
}
reader := bufio.NewReader(streamResp.Body)
var currentEvent string
for {
line, err := reader.ReadString('\n')
if err != nil {
@@ -2153,10 +2161,23 @@ func (s *QwenService) chatGradioChatResponse(endpointURL string, w http.Response
if strings.HasPrefix(line, "data: ") {
dataJSON := strings.TrimPrefix(line, "data: ")
if currentEvent == "error" {
break
if streamer != nil {
break
}
return fmt.Errorf("gradio upstream error: %s", dataJSON)
}
if fullAssistantText, ok := parseAssistantText(dataJSON); ok {
if s.autoFailover && (strings.Contains(fullAssistantText, "QSA Micro-block Reasoning") || strings.Contains(fullAssistantText, "Sandbox Response") || strings.Contains(fullAssistantText, "To connect to a live inference engine")) {
return fmt.Errorf("upstream space %s is in sandbox simulation mode (live inference not configured)", endpointURL)
}
if streamer == nil {
flusher, _ := w.(http.Flusher)
streamer = NewStreamer(w, flusher, completionID, createdTime, resolvedModel)
streamer.Role()
}
currentReasoning, currentContent := SeparateReasoningAndContent(fullAssistantText)
// Stream reasoning tokens incrementally
@@ -2176,6 +2197,10 @@ func (s *QwenService) chatGradioChatResponse(endpointURL string, w http.Response
}
}
if streamer == nil {
return fmt.Errorf("upstream space %s produced no valid output", endpointURL)
}
// Flush remaining buffer in tool filter
toolFilter.Flush(onContentChunk, onToolCallChunk)
@@ -2207,7 +2232,7 @@ func main() {
autoFailover := flag.Bool("failover", true, "Enable automatic failover across endpoints on error or quota limit")
flag.BoolVar(autoFailover, "auto-failover", true, "Alias for -failover")
defaultModelVal := "Qwen/Qwen3.8-27B"
defaultModelVal := "Qwen/Qwen3.8-Flash-Next"
if envModel := os.Getenv("QFLASH_MODEL"); envModel != "" {
defaultModelVal = envModel
}