Align default space and Hunyuan 3 behavior with hygate

This commit is contained in:
Luxferre
2026-09-07 11:05:20 +03:00
parent 187edabadb
commit c5ea129b44
3 changed files with 149 additions and 51 deletions
+23 -10
View File
@@ -79,7 +79,8 @@ With SOCKS5 proxy:
| Flag | Default | Description | | Flag | Default | Description |
|------|---------|-------------| |------|---------|-------------|
| `-space`, `-url` | `https://tencent-hy3.hf.space` | Target Gradio space URL | | `-space`, `-url`, `-endpoint` | `https://tencent-hy3.hf.space` | Target Gradio space URL |
| `-model` | `""` | Exposed model name override (default: auto-detected) |
| `-port` | `8080` | Port to listen on | | `-port` | `8080` | Port to listen on |
| `-host` | `0.0.0.0` | Host interface to bind to | | `-host` | `0.0.0.0` | Host interface to bind to |
| `-socks`, `-proxy`, `-socks5` | `""` | SOCKS5 proxy URL (`socks5://user:pass@host:port`) | | `-socks`, `-proxy`, `-socks5` | `""` | SOCKS5 proxy URL (`socks5://user:pass@host:port`) |
@@ -105,7 +106,19 @@ Response:
"object": "list", "object": "list",
"data": [ "data": [
{ {
"id": "digital-twin", "id": "hy3",
"object": "model",
"created": 1788756307,
"owned_by": "gradio"
},
{
"id": "hunyuan3",
"object": "model",
"created": 1788756307,
"owned_by": "gradio"
},
{
"id": "tencent/Hy3",
"object": "model", "object": "model",
"created": 1788756307, "created": 1788756307,
"owned_by": "gradio" "owned_by": "gradio"
@@ -120,7 +133,7 @@ Response:
curl http://localhost:8080/v1/chat/completions \ curl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"model": "digital-twin", "model": "hy3",
"messages": [ "messages": [
{"role": "user", "content": "What is the capital of France?"} {"role": "user", "content": "What is the capital of France?"}
] ]
@@ -133,13 +146,13 @@ Response:
"id": "chatcmpl-16425f9d-c350-47a1-9a6d-e9ce10871545", "id": "chatcmpl-16425f9d-c350-47a1-9a6d-e9ce10871545",
"object": "chat.completion", "object": "chat.completion",
"created": 1788756310, "created": 1788756310,
"model": "digital-twin", "model": "hy3",
"choices": [ "choices": [
{ {
"index": 0, "index": 0,
"message": { "message": {
"role": "assistant", "role": "assistant",
"content": "Paris is the capital of France." "content": "The capital of France is Paris."
}, },
"finish_reason": "stop" "finish_reason": "stop"
} }
@@ -158,7 +171,7 @@ Response:
curl -N http://localhost:8080/v1/chat/completions \ curl -N http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"model": "digital-twin", "model": "hy3",
"messages": [ "messages": [
{"role": "user", "content": "Count from 1 to 5."} {"role": "user", "content": "Count from 1 to 5."}
], ],
@@ -172,7 +185,7 @@ curl -N http://localhost:8080/v1/chat/completions \
curl http://localhost:8080/v1/chat/completions \ curl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"model": "digital-twin", "model": "hy3",
"messages": [ "messages": [
{"role": "user", "content": "What is the weather in Tokyo?"} {"role": "user", "content": "What is the weather in Tokyo?"}
], ],
@@ -199,7 +212,7 @@ Response:
"id": "chatcmpl-32727c62-ef2a-4866-855b-f1c7ec2b8023", "id": "chatcmpl-32727c62-ef2a-4866-855b-f1c7ec2b8023",
"object": "chat.completion", "object": "chat.completion",
"created": 1788756322, "created": 1788756322,
"model": "digital-twin", "model": "hy3",
"choices": [ "choices": [
{ {
"index": 0, "index": 0,
@@ -234,7 +247,7 @@ Response:
curl http://localhost:8080/v1/chat/completions \ curl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ -d '{
"model": "digital-twin", "model": "hy3",
"messages": [ "messages": [
{"role": "user", "content": "What is the weather in Tokyo?"}, {"role": "user", "content": "What is the weather in Tokyo?"},
{ {
@@ -279,7 +292,7 @@ Response:
"id": "chatcmpl-4903ba12-f12b-4cd3-a801-7290bc91a421", "id": "chatcmpl-4903ba12-f12b-4cd3-a801-7290bc91a421",
"object": "chat.completion", "object": "chat.completion",
"created": 1788756335, "created": 1788756335,
"model": "digital-twin", "model": "hy3",
"choices": [ "choices": [
{ {
"index": 0, "index": 0,
+58 -33
View File
@@ -28,6 +28,7 @@ var (
DefaultSpaceURL = "https://tencent-hy3.hf.space" DefaultSpaceURL = "https://tencent-hy3.hf.space"
DefaultUserAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:153.0) Gecko/20100101 Firefox/153.0" DefaultUserAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:153.0) Gecko/20100101 Firefox/153.0"
ConfiguredUserAgent string ConfiguredUserAgent string
ConfiguredModelName string
) )
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -1613,6 +1614,16 @@ func (d *SpaceDiscovery) GetModelList() []ModelItem {
var items []ModelItem var items []ModelItem
seen := make(map[string]bool) seen := make(map[string]bool)
if ConfiguredModelName != "" {
seen[ConfiguredModelName] = true
items = append(items, ModelItem{
ID: ConfiguredModelName,
Object: "model",
Created: now,
OwnedBy: "gradio",
})
}
for _, m := range d.Models { for _, m := range d.Models {
if m != "" && !seen[m] { if m != "" && !seen[m] {
seen[m] = true seen[m] = true
@@ -2183,20 +2194,11 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
toolName = lastMsg.ToolCallID toolName = lastMsg.ToolCallID
} }
if disc.IsHunyuan3 { if disc.IsHunyuan3 {
toolItem := map[string]interface{}{ if toolName != "" {
"role": "tool", lastUserMessage = fmt.Sprintf("Tool result for %s: %s", toolName, lastContent)
"content": lastContent, } else {
lastUserMessage = lastContent
} }
if lastMsg.ToolCallID != "" {
toolItem["tool_call_id"] = lastMsg.ToolCallID
} else if toolName != "" {
toolItem["tool_call_id"] = toolName
}
if lastMsg.Name != "" {
toolItem["name"] = lastMsg.Name
}
historyArray = append(historyArray, toolItem)
lastUserMessage = "Please proceed based on the tool results."
} else { } else {
if toolName != "" { if toolName != "" {
lastUserMessage = fmt.Sprintf("Tool result for %s: %s\nPlease answer the user's request based on the tool result.", toolName, lastContent) lastUserMessage = fmt.Sprintf("Tool result for %s: %s\nPlease answer the user's request based on the tool result.", toolName, lastContent)
@@ -2393,34 +2395,39 @@ func ParseGradioStreamOutput(rawJSON string) GradioOutputFrame {
// Check if v[0] is an inner slice (e.g. Hy3: [[content, reasoning, tool_calls, history]]) // Check if v[0] is an inner slice (e.g. Hy3: [[content, reasoning, tool_calls, history]])
if inner, ok := v[0].([]interface{}); ok { if inner, ok := v[0].([]interface{}); ok {
if len(inner) >= 2 { if 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
}
}
}
frame.OK = true
return frame
}
if len(inner) == 2 {
s0, ok0 := inner[0].(string) s0, ok0 := inner[0].(string)
s1, ok1 := inner[1].(string) s1, ok1 := inner[1].(string)
if ok0 && ok1 { if ok0 && ok1 {
frame.Content = s0 frame.Content = s0
frame.Reasoning = s1 frame.Reasoning = s1
if len(inner) >= 3 {
if tcSlice, ok := inner[2].([]interface{}); ok && len(tcSlice) > 0 {
b, err := json.Marshal(tcSlice)
if err == nil {
var tcs []ToolCall
if err := json.Unmarshal(b, &tcs); err == nil {
frame.ToolCalls = tcs
}
}
}
}
frame.OK = true frame.OK = true
return frame return frame
} }
// Check if inner is a chat pair: ["user msg", "assistant msg"] // Check if inner is a chat pair: ["user msg", "assistant msg"]
if len(inner) == 2 { if ok1 {
if aStr, ok := inner[1].(string); ok { frame.Content = s1
frame.Content = aStr frame.OK = true
frame.OK = true return frame
return frame
}
} }
} }
@@ -2544,7 +2551,11 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
modelName := req.Model modelName := req.Model
if modelName == "" { if modelName == "" {
modelName = disc.PrimaryModel if ConfiguredModelName != "" {
modelName = ConfiguredModelName
} else {
modelName = disc.PrimaryModel
}
} }
gradioData, err := g.BuildGradioPayload(disc, req) gradioData, err := g.BuildGradioPayload(disc, req)
@@ -2714,7 +2725,16 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
return fmt.Errorf("upstream Gradio error: %s", errMsg) return fmt.Errorf("upstream Gradio error: %s", errMsg)
} }
if frame := ParseGradioStreamOutput(dataStr); frame.OK { if frame := ParseGradioStreamOutput(dataStr); frame.OK {
latestFrame = frame 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
}
latestFrame.OK = true
} }
if currentEvent == "complete" { if currentEvent == "complete" {
break break
@@ -2982,6 +3002,8 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
func main() { func main() {
spaceFlag := flag.String("space", DefaultSpaceURL, "Target Gradio Space URL") spaceFlag := flag.String("space", DefaultSpaceURL, "Target Gradio Space URL")
flag.StringVar(spaceFlag, "url", DefaultSpaceURL, "Alias for -space") flag.StringVar(spaceFlag, "url", DefaultSpaceURL, "Alias for -space")
flag.StringVar(spaceFlag, "endpoint", DefaultSpaceURL, "Alias for -space")
modelFlag := flag.String("model", "", "Exposed model name override (default: auto-detected)")
portFlag := flag.Int("port", 8080, "Gateway HTTP server port") portFlag := flag.Int("port", 8080, "Gateway HTTP server port")
hostFlag := flag.String("host", "0.0.0.0", "Gateway HTTP server host") hostFlag := flag.String("host", "0.0.0.0", "Gateway HTTP server host")
socksFlag := flag.String("socks", "", "Optional SOCKS5 proxy URL (e.g. socks5://127.0.0.1:1080)") socksFlag := flag.String("socks", "", "Optional SOCKS5 proxy URL (e.g. socks5://127.0.0.1:1080)")
@@ -3007,6 +3029,9 @@ func main() {
if *uaFlag != "" { if *uaFlag != "" {
ConfiguredUserAgent = *uaFlag ConfiguredUserAgent = *uaFlag
} }
if *modelFlag != "" {
ConfiguredModelName = *modelFlag
}
gateway := NewGradioGateway(*spaceFlag, *socksFlag, time.Duration(*timeoutFlag)*time.Second) gateway := NewGradioGateway(*spaceFlag, *socksFlag, time.Duration(*timeoutFlag)*time.Second)
+68 -8
View File
@@ -285,6 +285,19 @@ func TestParseGradioStreamOutput(t *testing.T) {
if !frame5.OK || frame5.Content != "msg answer" || frame5.Reasoning != "msg think" { if !frame5.OK || frame5.Content != "msg answer" || frame5.Reasoning != "msg think" {
t.Errorf("unexpected frame5: %+v", frame5) t.Errorf("unexpected frame5: %+v", frame5)
} }
// 6. Hy3 with null content and reasoning
hy3NullContent := `[[null, "Thinking process...", null]]`
frame6 := ParseGradioStreamOutput(hy3NullContent)
if !frame6.OK || frame6.Content != "" || frame6.Reasoning != "Thinking process..." {
t.Errorf("unexpected frame6: %+v", frame6)
}
// 7. Hy3 with 3 elements (answer, reasoning, null tool calls)
hy3ThreeElem := `[["Mocked answer", "Mocked reasoning", null]]`
frame7 := ParseGradioStreamOutput(hy3ThreeElem)
if !frame7.OK || frame7.Content != "Mocked answer" || frame7.Reasoning != "Mocked reasoning" || len(frame7.ToolCalls) != 0 {
t.Errorf("unexpected frame7: %+v", frame7)
}
} }
func TestHunyuan3BuildPayload(t *testing.T) { func TestHunyuan3BuildPayload(t *testing.T) {
@@ -338,9 +351,9 @@ func TestHunyuan3BuildPayload(t *testing.T) {
t.Fatalf("expected 9 payload items, got %d", len(data)) t.Fatalf("expected 9 payload items, got %d", len(data))
} }
// Message parameter (0): should be prompt continuation since last was tool // Message parameter (0): should be tool result prompt matching hygate behavior
if msg, ok := data[0].(string); !ok || msg != "Please proceed based on the tool results." { if msg, ok := data[0].(string); !ok || msg != "Tool result for c1: 4" {
t.Errorf("expected continuation prompt, got %v", data[0]) t.Errorf("expected 'Tool result for c1: 4', got %v", data[0])
} }
// System parameter (1) // System parameter (1)
@@ -348,16 +361,19 @@ func TestHunyuan3BuildPayload(t *testing.T) {
t.Errorf("expected 'Be helpful', got %v", data[1]) t.Errorf("expected 'Be helpful', got %v", data[1])
} }
// History parameter (2): should contain all messages including the tool turn // History parameter (2): should contain prior turns (user, assistant with tool calls)
hist, ok := data[2].([]map[string]interface{}) hist, ok := data[2].([]map[string]interface{})
if !ok { if !ok {
t.Fatalf("expected history slice of maps, got %T", data[2]) t.Fatalf("expected history slice of maps, got %T", data[2])
} }
if len(hist) != 3 { if len(hist) != 2 {
t.Fatalf("expected 3 history items (user, assistant, tool), got %d", len(hist)) t.Fatalf("expected 2 history items (user, assistant), got %d", len(hist))
} }
if hist[2]["role"] != "tool" || hist[2]["content"] != "4" || hist[2]["tool_call_id"] != "c1" { if hist[0]["role"] != "user" || hist[0]["content"] != "2+2" {
t.Errorf("unexpected tool history entry: %+v", hist[2]) t.Errorf("unexpected user history entry: %+v", hist[0])
}
if hist[1]["role"] != "assistant" || len(hist[1]["tool_calls"].([]ToolCall)) != 1 {
t.Errorf("unexpected assistant history entry: %+v", hist[1])
} }
// ThinkLevel parameter (3) // ThinkLevel parameter (3)
@@ -375,6 +391,26 @@ func TestHunyuan3BuildPayload(t *testing.T) {
if !ok || !strings.Contains(fnStr, "calc") { if !ok || !strings.Contains(fnStr, "calc") {
t.Errorf("expected functions_json_str to contain 'calc', got %v", data[8]) t.Errorf("expected functions_json_str to contain 'calc', got %v", data[8])
} }
// Test multi-tool turn: 2 tool messages at the end
reqMulti := req
reqMulti.Messages = append(reqMulti.Messages, ChatMessage{Role: "tool", Name: "fetch", Content: "done"})
dataMulti, err := gw.BuildGradioPayload(disc, reqMulti)
if err != nil {
t.Fatalf("BuildGradioPayload failed on multi-tool: %v", err)
}
// The last tool message is data[0]
if msg, ok := dataMulti[0].(string); !ok || msg != "Tool result for fetch: done" {
t.Errorf("expected 'Tool result for fetch: done', got %v", dataMulti[0])
}
// The first tool message is in history
histMulti, _ := dataMulti[2].([]map[string]interface{})
if len(histMulti) != 3 {
t.Fatalf("expected 3 history items (user, assistant, tool 1), got %d", len(histMulti))
}
if histMulti[2]["role"] != "tool" || histMulti[2]["content"] != "4" {
t.Errorf("unexpected tool 1 in history: %+v", histMulti[2])
}
} }
func TestHunyuan3MockServerCompletion(t *testing.T) { func TestHunyuan3MockServerCompletion(t *testing.T) {
@@ -1330,3 +1366,27 @@ func TestToolChoiceHandling(t *testing.T) {
t.Errorf("expected specific function directive in instruction, got: %s", instrFn) t.Errorf("expected specific function directive in instruction, got: %s", instrFn)
} }
} }
func TestConfiguredModelName(t *testing.T) {
disc := &SpaceDiscovery{
PrimaryModel: "hy3",
Models: []string{"hy3", "hunyuan3"},
}
// Without override
ConfiguredModelName = ""
list1 := disc.GetModelList()
if len(list1) < 2 || list1[0].ID != "hy3" {
t.Errorf("expected default first model hy3, got %+v", list1)
}
// With override
ConfiguredModelName = "my-custom-hy3"
defer func() { ConfiguredModelName = "" }()
list2 := disc.GetModelList()
if len(list2) < 3 || list2[0].ID != "my-custom-hy3" {
t.Errorf("expected first model to be my-custom-hy3, got %+v", list2)
}
}