Restore hygate parity for Hunyuan3 and fix thinking separation

This commit is contained in:
Luxferre
2026-09-07 15:38:42 +03:00
parent 21c11c5fdd
commit 36bbe2bf86
2 changed files with 499 additions and 93 deletions
+215 -93
View File
@@ -3215,16 +3215,19 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
// Select protocol
isCallV2 := false
if strings.HasPrefix(discovery.GradioVersion, "6.") {
isCallV2 = true
}
hasSnippet := false
if bestEndpointInfo != nil && bestEndpointInfo.CodeSnippets != nil {
if bashSnippet, ok := bestEndpointInfo.CodeSnippets["bash"].(string); ok {
if bashSnippet, ok := bestEndpointInfo.CodeSnippets["bash"].(string); ok && bashSnippet != "" {
hasSnippet = true
if strings.Contains(bashSnippet, "/call/v2/") {
isCallV2 = true
}
}
}
if !hasSnippet && strings.HasPrefix(discovery.GradioVersion, "6.") {
isCallV2 = true
}
if strings.HasPrefix(discovery.GradioVersion, "3.") {
discovery.Protocol = "predict"
@@ -3299,11 +3302,12 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
if strings.Contains(pName, "function") || strings.Contains(pName, "tool") || strings.Contains(pLabel, "tool") || strings.Contains(pLabel, "function") || strings.Contains(cLabel, "tool") || strings.Contains(cLabel, "function") {
mapping.ParamType = "tools"
discovery.FunctionsJSONIndex = idx
} else if strings.Contains(pName, "preserved") || strings.Contains(cLabel, "preserved") {
mapping.ParamType = "preserved_thinking"
discovery.PreservedThinkingIndex = idx
} else if strings.Contains(pName, "think_level") || strings.Contains(pName, "thinking") || strings.Contains(pLabel, "think") || strings.Contains(cLabel, "think") {
mapping.ParamType = "think_level"
discovery.ThinkLevelIndex = idx
} else if strings.Contains(pName, "preserved") || strings.Contains(cLabel, "preserved") {
discovery.PreservedThinkingIndex = idx
} else if strings.Contains(pName, "system") || strings.Contains(pLabel, "system") || strings.Contains(cLabel, "system") || strings.Contains(cLabel, "instruction") {
mapping.ParamType = "system_prompt"
discovery.SystemIndex = idx
@@ -3532,6 +3536,9 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
if discovery.PrimaryModel == "gradio-chat" || discovery.PrimaryModel == "" {
discovery.PrimaryModel = "hy3"
}
if discovery.Protocol == "call_v2" {
discovery.Protocol = "call"
}
}
// 9. Tool Calling Support Picture Resolution
@@ -3991,6 +3998,10 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
data[disc.ThinkLevelIndex] = thinkLevel
}
if disc.PreservedThinkingIndex >= 0 && disc.PreservedThinkingIndex != disc.MessageIndex && disc.PreservedThinkingIndex != disc.HistoryIndex && disc.PreservedThinkingIndex != disc.SystemIndex && disc.PreservedThinkingIndex < len(data) {
data[disc.PreservedThinkingIndex] = nil
}
if disc.TempIndex >= 0 && disc.TempIndex != disc.MessageIndex && disc.TempIndex != disc.HistoryIndex && disc.TempIndex < len(data) {
if req.Temperature != nil {
data[disc.TempIndex] = *req.Temperature
@@ -4046,39 +4057,136 @@ type GradioOutputFrame struct {
OK bool
}
func toInt(v interface{}) (int, bool) {
switch n := v.(type) {
case int:
return n, true
case int64:
return int(n), true
case float64:
return int(n), true
case json.Number:
i, err := n.Int64()
return int(i), err == nil
default:
return 0, false
}
}
func isReasoningPath(path interface{}) bool {
switch p := path.(type) {
case []interface{}:
for i, elem := range p {
if s, ok := elem.(string); ok {
sLow := strings.ToLower(s)
if strings.Contains(sLow, "reason") || strings.Contains(sLow, "thought") || strings.Contains(sLow, "think") {
return true
}
}
if num, ok := toInt(elem); ok {
if len(p) == 1 && num == 1 {
return true
}
if len(p) == 2 && i == 1 && num == 1 {
if p0, ok0 := toInt(p[0]); ok0 && p0 == 0 {
return true
}
}
}
}
case string:
sLow := strings.ToLower(p)
if strings.Contains(sLow, "reason") || strings.Contains(sLow, "thought") || strings.Contains(sLow, "think") {
return true
}
}
return false
}
func isContentPath(path interface{}) bool {
switch p := path.(type) {
case []interface{}:
if len(p) == 0 {
return true
}
for _, elem := range p {
if s, ok := elem.(string); ok {
sLow := strings.ToLower(s)
if strings.Contains(sLow, "content") || strings.Contains(sLow, "text") || strings.Contains(sLow, "value") {
return true
}
}
}
if num, ok := toInt(p[0]); ok && num == 0 {
if len(p) == 1 {
return true
}
if len(p) >= 2 {
if p1, ok1 := toInt(p[1]); ok1 && p1 == 0 {
return true
}
if s1, ok1 := p[1].(string); ok1 && (strings.Contains(s1, "content") || strings.Contains(s1, "text")) {
return true
}
}
}
case string:
sLow := strings.ToLower(p)
if strings.Contains(sLow, "content") || strings.Contains(sLow, "text") || strings.Contains(sLow, "value") {
return true
}
}
return false
}
// extractGradioDiffDelta extracts delta string from Gradio 6 streaming diff operations
func extractGradioDiffDelta(v []interface{}) (string, bool) {
if len(v) == 0 {
return "", false
}
if len(v) >= 3 {
if op, ok := v[0].(string); ok && (op == "append" || op == "add") {
if delta, ok := v[2].(string); ok {
return delta, true
func extractGradioDiffDelta(v []interface{}) (contentDelta string, reasoningDelta string, ok bool) {
var diffItems [][]interface{}
var findOps func(items []interface{})
findOps = func(items []interface{}) {
if len(items) >= 3 {
if op, ok := items[0].(string); ok && (op == "append" || op == "add") {
diffItems = append(diffItems, items)
return
}
}
for _, it := range items {
if sub, ok := it.([]interface{}); ok {
findOps(sub)
}
}
}
for _, item := range v {
if innerList, ok := item.([]interface{}); ok && len(innerList) > 0 {
for _, opItem := range innerList {
if opArr, ok := opItem.([]interface{}); ok && len(opArr) >= 3 {
if op, ok := opArr[0].(string); ok && (op == "append" || op == "add") {
if delta, ok := opArr[2].(string); ok {
return delta, true
}
}
}
findOps(v)
if len(diffItems) == 0 {
return "", "", false
}
hasMatch := false
for _, item := range diffItems {
delta, isStr := item[2].(string)
if !isStr || delta == "" {
continue
}
path := item[1]
if isReasoningPath(path) {
if reasoningDelta == "" {
reasoningDelta = delta
hasMatch = true
}
if len(innerList) >= 3 {
if op, ok := innerList[0].(string); ok && (op == "append" || op == "add") {
if delta, ok := innerList[2].(string); ok {
return delta, true
}
}
} else if isContentPath(path) {
if contentDelta == "" {
contentDelta = delta
hasMatch = true
}
} else {
if contentDelta == "" {
contentDelta = delta
hasMatch = true
}
}
}
return "", false
return contentDelta, reasoningDelta, hasMatch
}
// ParseGradioStreamOutput extracts structured content, reasoning, and tool calls from Gradio output
@@ -4109,30 +4217,33 @@ func ParseGradioStreamOutput(rawJSON string) (frame GradioOutputFrame) {
return frame
}
if delta, ok := extractGradioDiffDelta(v); ok {
frame.Content = delta
if cDelta, rDelta, ok := extractGradioDiffDelta(v); ok {
frame.Content = cDelta
frame.Reasoning = rDelta
frame.IsDelta = true
frame.OK = true
return frame
}
// 1. Check if v[0] is an inner slice with len >= 3 (e.g. Hy3: [[content, reasoning, tool_calls, history]])
if inner, ok := v[0].([]interface{}); ok && len(inner) >= 3 {
s0, _ := inner[0].(string)
s1, _ := inner[1].(string)
frame.Content = s0
frame.Reasoning = s1
if inner[2] != nil {
b, err := json.Marshal(inner[2])
if err == nil {
var tcs []ToolCall
if json.Unmarshal(b, &tcs) == nil && len(tcs) > 0 {
frame.ToolCalls = tcs
// 1. Check if v[0] is an inner slice with len >= 2 (e.g. Hy3: [[content, reasoning, tool_calls, history]])
if inner, ok := v[0].([]interface{}); ok && len(inner) >= 2 {
s0, ok0 := inner[0].(string)
s1, ok1 := inner[1].(string)
if ok0 || ok1 {
frame.Content = s0
frame.Reasoning = s1
if len(inner) >= 3 && inner[2] != nil {
b, err := json.Marshal(inner[2])
if err == nil {
var tcs []ToolCall
if json.Unmarshal(b, &tcs) == nil && len(tcs) > 0 {
frame.ToolCalls = tcs
}
}
}
frame.OK = true
return frame
}
frame.OK = true
return frame
}
// 2. Check if any element of v is a Chatbot message list or Chatbot pair list
@@ -4270,22 +4381,6 @@ func ParseGradioStreamOutput(rawJSON string) (frame GradioOutputFrame) {
return frame
}
// 4. Check if v[0] is an inner pair [content, reasoning]
if inner, ok := v[0].([]interface{}); ok && len(inner) == 2 {
s0, ok0 := inner[0].(string)
s1, ok1 := inner[1].(string)
if ok0 && ok1 {
frame.Content = s0
frame.Reasoning = s1
frame.OK = true
return frame
}
if ok1 {
frame.Content = s1
frame.OK = true
return frame
}
}
// 5. Check if v[0] is a non-empty string or single output
if s, ok := v[0].(string); ok && (s != "" || len(v) == 1) {
@@ -4618,15 +4713,16 @@ func (g *GradioGateway) executeQueueCompletion(w http.ResponseWriter, r *http.Re
if frame.OK {
if frame.IsDelta {
latestFrame.Content += frame.Content
latestFrame.Reasoning += frame.Reasoning
} else {
if len(frame.ToolCalls) > 0 {
latestFrame.Content = frame.Content
} else if frame.Content != "" || latestFrame.Content == "" {
latestFrame.Content = frame.Content
}
}
if frame.Reasoning != "" || latestFrame.Reasoning == "" {
latestFrame.Reasoning = frame.Reasoning
if frame.Reasoning != "" || latestFrame.Reasoning == "" {
latestFrame.Reasoning = frame.Reasoning
}
}
if len(frame.ToolCalls) > 0 {
latestFrame.ToolCalls = frame.ToolCalls
@@ -4668,7 +4764,7 @@ func (g *GradioGateway) executeQueueCompletion(w http.ResponseWriter, r *http.Re
var prevContent string
var prevReasoning string
prevToolArgs := make(map[int]string)
nativeReasoningSeen := false
nativeReasoningSeen := disc.IsHunyuan3
nativeToolCallsSeen := false
var streamErr error
@@ -4710,14 +4806,23 @@ func (g *GradioGateway) executeQueueCompletion(w http.ResponseWriter, r *http.Re
// 1. Native reasoning handling
if frame.Reasoning != "" || nativeReasoningSeen {
nativeReasoningSeen = true
if len(frame.Reasoning) > len(prevReasoning) && strings.HasPrefix(frame.Reasoning, prevReasoning) {
delta := frame.Reasoning[len(prevReasoning):]
streamer.Reasoning(delta)
prevReasoning = frame.Reasoning
} else if prevReasoning == "" && frame.Reasoning != "" {
streamer.Reasoning(frame.Reasoning)
var deltaReasoning string
if frame.IsDelta {
deltaReasoning = frame.Reasoning
prevReasoning += deltaReasoning
} else {
if strings.HasPrefix(frame.Reasoning, prevReasoning) {
deltaReasoning = frame.Reasoning[len(prevReasoning):]
} else if prevReasoning == "" {
deltaReasoning = frame.Reasoning
} else {
deltaReasoning = frame.Reasoning
}
prevReasoning = frame.Reasoning
}
if deltaReasoning != "" {
streamer.Reasoning(deltaReasoning)
}
}
// 2. Native tool call handling
@@ -4929,8 +5034,10 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
return json.Marshal(v2Payload)
}
payloadMap := map[string]interface{}{
"data": gradioData,
"session_hash": GenerateUUID(),
"data": gradioData,
}
if !disc.IsHunyuan3 {
payloadMap["session_hash"] = GenerateUUID()
}
return json.Marshal(payloadMap)
}
@@ -5115,13 +5222,18 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
return fmt.Errorf("upstream Gradio error: %s", errMsg)
}
if frame := ParseGradioStreamOutput(dataStr); frame.OK {
if len(frame.ToolCalls) > 0 {
latestFrame.Content = frame.Content
} else if frame.Content != "" || latestFrame.Content == "" {
latestFrame.Content = frame.Content
}
if frame.Reasoning != "" || latestFrame.Reasoning == "" {
latestFrame.Reasoning = frame.Reasoning
if frame.IsDelta {
latestFrame.Content += frame.Content
latestFrame.Reasoning += frame.Reasoning
} else {
if len(frame.ToolCalls) > 0 {
latestFrame.Content = frame.Content
} else if frame.Content != "" || latestFrame.Content == "" {
latestFrame.Content = frame.Content
}
if frame.Reasoning != "" || latestFrame.Reasoning == "" {
latestFrame.Reasoning = frame.Reasoning
}
}
if len(frame.ToolCalls) > 0 {
latestFrame.ToolCalls = frame.ToolCalls
@@ -5161,7 +5273,7 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
var prevContent string
var prevReasoning string
prevToolArgs := make(map[int]string)
nativeReasoningSeen := false
nativeReasoningSeen := disc.IsHunyuan3
nativeToolCallsSeen := false
currentEvent := ""
var streamErr error
@@ -5213,14 +5325,19 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
if frame.Reasoning != "" || nativeReasoningSeen {
nativeReasoningSeen = true
var deltaReasoning string
if strings.HasPrefix(frame.Reasoning, prevReasoning) {
deltaReasoning = frame.Reasoning[len(prevReasoning):]
} else if prevReasoning == "" {
if frame.IsDelta {
deltaReasoning = frame.Reasoning
prevReasoning += deltaReasoning
} else {
deltaReasoning = frame.Reasoning
if strings.HasPrefix(frame.Reasoning, prevReasoning) {
deltaReasoning = frame.Reasoning[len(prevReasoning):]
} else if prevReasoning == "" {
deltaReasoning = frame.Reasoning
} else {
deltaReasoning = frame.Reasoning
}
prevReasoning = frame.Reasoning
}
prevReasoning = frame.Reasoning
if deltaReasoning != "" {
streamer.Reasoning(deltaReasoning)
}
@@ -5269,14 +5386,19 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
// 3. Content handling
currentText := frame.Content
var delta string
if strings.HasPrefix(currentText, prevContent) {
delta = currentText[len(prevContent):]
} else if prevContent == "" {
if frame.IsDelta {
delta = currentText
prevContent += delta
} else {
delta = currentText
if strings.HasPrefix(currentText, prevContent) {
delta = currentText[len(prevContent):]
} else if prevContent == "" {
delta = currentText
} else {
delta = currentText
}
prevContent = currentText
}
prevContent = currentText
if delta != "" {
if nativeReasoningSeen || nativeToolCallsSeen {