omniforge support added

This commit is contained in:
Luxferre
2026-09-21 21:14:24 +03:00
parent f2b16174fa
commit 361f61aa03
2 changed files with 1233 additions and 134 deletions
+493 -70
View File
@@ -125,6 +125,7 @@ type ChatCompletionRequest struct {
Temperature *float64 `json:"temperature,omitempty"` Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"top_p,omitempty"` TopP *float64 `json:"top_p,omitempty"`
ReasoningEffort string `json:"reasoning_effort,omitempty"` ReasoningEffort string `json:"reasoning_effort,omitempty"`
WebSearch *bool `json:"web_search,omitempty"`
} }
type ChatCompletionResponseChoice struct { type ChatCompletionResponseChoice struct {
@@ -406,6 +407,10 @@ func DoWithFibonacciRetry(client *http.Client, makeReq func() (*http.Request, er
if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusMethodNotAllowed { if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusMethodNotAllowed {
break break
} }
bodyLower := strings.ToLower(string(respBody))
if strings.Contains(bodyLower, "space is paused") {
break
}
if _, ok := extractFailedGeneration(string(respBody)); ok { if _, ok := extractFailedGeneration(string(respBody)); ok {
break break
} }
@@ -1853,17 +1858,41 @@ func finalizeOutput(frame GradioOutputFrame) (finalContent interface{}, reasonin
return finalContent, reasoning, toolCalls, finishReason return finalContent, reasoning, toolCalls, finishReason
} }
var (
thinkTagRegex = regexp.MustCompile(`(?is)<think>([\s\S]*?)(?:<\/think>|$)`)
detailsThinkingRegex = regexp.MustCompile(`(?is)<details(?:\s+[^>]*)?>\s*<summary>\s*(?:thinking|thought|thinking\s+process|reasoning)\s*<\/summary>([\s\S]*?)(?:<\/details>|$)`)
)
func ExtractThinking(content string) (string, string) { func ExtractThinking(content string) (string, string) {
if loc := detailsThinkingRegex.FindStringSubmatchIndex(content); len(loc) >= 4 {
reasoning := content[loc[2]:loc[3]]
rem := content[:loc[0]] + content[loc[1]:]
reasoning = strings.TrimSpace(reasoning)
rem = strings.TrimPrefix(rem, "\n\n")
rem = strings.TrimPrefix(rem, "\n")
rem = strings.TrimSpace(rem)
return rem, reasoning
}
if strings.Contains(content, "<think>") && strings.Contains(content, "</think>") { if strings.Contains(content, "<think>") && strings.Contains(content, "</think>") {
start := strings.Index(content, "<think>") start := strings.Index(content, "<think>")
end := strings.Index(content, "</think>") end := strings.Index(content, "</think>")
if start < end { if start < end {
reasoning := content[start+len("<think>") : end] reasoning := content[start+len("<think>") : end]
rem := content[:start] + content[end+len("</think>"):] rem := content[:start] + content[end+len("</think>"):]
reasoning = strings.TrimSpace(reasoning)
rem = strings.TrimPrefix(rem, "\n\n") rem = strings.TrimPrefix(rem, "\n\n")
rem = strings.TrimPrefix(rem, "\n") rem = strings.TrimPrefix(rem, "\n")
rem = strings.TrimSpace(rem)
return rem, reasoning return rem, reasoning
} }
} else if loc := thinkTagRegex.FindStringSubmatchIndex(content); len(loc) >= 4 {
reasoning := content[loc[2]:loc[3]]
rem := content[:loc[0]] + content[loc[1]:]
reasoning = strings.TrimSpace(reasoning)
rem = strings.TrimPrefix(rem, "\n\n")
rem = strings.TrimPrefix(rem, "\n")
rem = strings.TrimSpace(rem)
return rem, reasoning
} }
return content, "" return content, ""
} }
@@ -2079,7 +2108,10 @@ func isProtocolOrInputError(errMsg string) bool {
strings.Contains(lower, "forbidden") || strings.Contains(lower, "forbidden") ||
strings.Contains(lower, "quota exceeded") || strings.Contains(lower, "quota exceeded") ||
strings.Contains(lower, "payment required") || strings.Contains(lower, "payment required") ||
strings.Contains(lower, "rate limit") { strings.Contains(lower, "rate limit") ||
strings.Contains(lower, "provider is busy") ||
strings.Contains(lower, "rate-limited") ||
strings.Contains(lower, "unavailable") {
return false return false
} }
return true return true
@@ -2596,6 +2628,8 @@ type GradioConfigResponse struct {
type HFSpaceCardData struct { type HFSpaceCardData struct {
Title string `json:"title"` Title string `json:"title"`
ShortDescription string `json:"short_description"` ShortDescription string `json:"short_description"`
Models []string `json:"models"`
SdkVersion string `json:"sdk_version"`
} }
type HFSpaceInfoResponse struct { type HFSpaceInfoResponse struct {
@@ -2627,6 +2661,8 @@ type SpaceDiscovery struct {
FnIndex int `json:"fn_index"` // -1 if not applicable FnIndex int `json:"fn_index"` // -1 if not applicable
Models []string `json:"models"` Models []string `json:"models"`
PrimaryModel string `json:"primary_model"` PrimaryModel string `json:"primary_model"`
ModelIndex int `json:"model_index"` // -1 if none
IsOmniForge bool `json:"is_omniforge"`
TotalInputs int `json:"total_inputs"` TotalInputs int `json:"total_inputs"`
RawTotalInputs int `json:"raw_total_inputs,omitempty"` RawTotalInputs int `json:"raw_total_inputs,omitempty"`
RawDefaultInputs []interface{} `json:"raw_default_inputs,omitempty"` RawDefaultInputs []interface{} `json:"raw_default_inputs,omitempty"`
@@ -2645,6 +2681,7 @@ type SpaceDiscovery struct {
FunctionsJSONIndex int `json:"functions_json_index"` // -1 if none FunctionsJSONIndex int `json:"functions_json_index"` // -1 if none
PreservedThinkingIndex int `json:"preserved_thinking_index"` // -1 if none PreservedThinkingIndex int `json:"preserved_thinking_index"` // -1 if none
WebSearchIndex int `json:"web_search_index"` // -1 if none WebSearchIndex int `json:"web_search_index"` // -1 if none
ImageIndex int `json:"image_index"` // -1 if none
IsHunyuan3 bool `json:"is_hunyuan3"` IsHunyuan3 bool `json:"is_hunyuan3"`
HistoryFormat string `json:"history_format"` // "messages", "pairs", "gradio_messages", "none" HistoryFormat string `json:"history_format"` // "messages", "pairs", "gradio_messages", "none"
ToolCallMode string `json:"tool_call_mode"` // "native_slot", "prompt_augmented_system", "prompt_augmented_first_turn", "prompt_augmented_single_prompt" ToolCallMode string `json:"tool_call_mode"` // "native_slot", "prompt_augmented_system", "prompt_augmented_first_turn", "prompt_augmented_single_prompt"
@@ -2672,6 +2709,9 @@ func (d *SpaceDiscovery) Summary() string {
if len(d.Models) > 0 { if len(d.Models) > 0 {
sb.WriteString(fmt.Sprintf("Exposed Models: %s\n", strings.Join(d.Models, ", "))) sb.WriteString(fmt.Sprintf("Exposed Models: %s\n", strings.Join(d.Models, ", ")))
} }
if d.ModelIndex >= 0 {
sb.WriteString(fmt.Sprintf("Model Slot Index: %d\n", d.ModelIndex))
}
sb.WriteString(fmt.Sprintf("History Format: %s\n", d.HistoryFormat)) sb.WriteString(fmt.Sprintf("History Format: %s\n", d.HistoryFormat))
sb.WriteString(fmt.Sprintf("Tool Call Support: %s\n", d.ToolCallMode)) sb.WriteString(fmt.Sprintf("Tool Call Support: %s\n", d.ToolCallMode))
sb.WriteString(fmt.Sprintf("Total Input Slots: %d\n", d.TotalInputs)) sb.WriteString(fmt.Sprintf("Total Input Slots: %d\n", d.TotalInputs))
@@ -2765,6 +2805,15 @@ func (d *SpaceDiscovery) MatchesModel(requested string) bool {
return true return true
} }
if d.ModelIndex >= 0 && d.ModelIndex < len(d.ParamMappings) {
for _, ch := range d.ParamMappings[d.ModelIndex].Choices {
chClean := strings.ToLower(ch)
if chClean == cleanReq || strings.HasSuffix(chClean, "/"+cleanReq) || strings.Contains(chClean, cleanReq) {
return true
}
}
}
return cleanReq == "default" || cleanReq == "gradio" || cleanReq == "gradio-chat" return cleanReq == "default" || cleanReq == "gradio" || cleanReq == "gradio-chat"
} }
@@ -2782,6 +2831,8 @@ func NewDefaultSpaceDiscovery(spaceURL string) *SpaceDiscovery {
Endpoint: "/chat_fn", Endpoint: "/chat_fn",
CleanEndpoint: "chat_fn", CleanEndpoint: "chat_fn",
FnIndex: -1, FnIndex: -1,
ModelIndex: -1,
IsOmniForge: false,
TotalInputs: 1, TotalInputs: 1,
HistoryIndex: -1, HistoryIndex: -1,
MessageIndex: 0, MessageIndex: 0,
@@ -2795,6 +2846,7 @@ func NewDefaultSpaceDiscovery(spaceURL string) *SpaceDiscovery {
FunctionsJSONIndex: -1, FunctionsJSONIndex: -1,
PreservedThinkingIndex: -1, PreservedThinkingIndex: -1,
WebSearchIndex: -1, WebSearchIndex: -1,
ImageIndex: -1,
HistoryFormat: "messages", HistoryFormat: "messages",
ToolCallMode: "prompt_augmented_single_prompt", ToolCallMode: "prompt_augmented_single_prompt",
LastDiscovered: time.Now(), LastDiscovered: time.Now(),
@@ -3043,10 +3095,14 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
} }
discovery := NewDefaultSpaceDiscovery(cleanURL) discovery := NewDefaultSpaceDiscovery(cleanURL)
if strings.Contains(strings.ToLower(cleanURL), "omniforge") {
discovery.IsOmniForge = true
}
// 1. Try fetching /gradio_api/info or /info // 1. Try fetching /gradio_api/info or /info
var infoResp GradioAPIInfoResponse var infoResp GradioAPIInfoResponse
infoFetched := false infoFetched := false
var linkHeader string
for _, path := range []string{"/gradio_api/info", "/info"} { for _, path := range []string{"/gradio_api/info", "/info"} {
infoURL := cleanURL + path infoURL := cleanURL + path
@@ -3054,7 +3110,11 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
if err == nil { if err == nil {
req.Header.Set("User-Agent", userAgent) req.Header.Set("User-Agent", userAgent)
resp, err := client.Do(req) resp, err := client.Do(req)
if err == nil && resp.StatusCode == http.StatusOK { if err == nil {
if l := resp.Header.Get("Link"); l != "" && linkHeader == "" {
linkHeader = l
}
if resp.StatusCode == http.StatusOK {
if json.NewDecoder(resp.Body).Decode(&infoResp) == nil { if json.NewDecoder(resp.Body).Decode(&infoResp) == nil {
infoFetched = true infoFetched = true
if path == "/gradio_api/info" { if path == "/gradio_api/info" {
@@ -3063,6 +3123,7 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
discovery.APIPrefix = "" discovery.APIPrefix = ""
} }
} }
}
resp.Body.Close() resp.Body.Close()
if infoFetched { if infoFetched {
break break
@@ -3083,7 +3144,11 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
if err == nil { if err == nil {
req.Header.Set("User-Agent", userAgent) req.Header.Set("User-Agent", userAgent)
resp, err := client.Do(req) resp, err := client.Do(req)
if err == nil && resp.StatusCode == http.StatusOK { if err == nil {
if l := resp.Header.Get("Link"); l != "" && linkHeader == "" {
linkHeader = l
}
if resp.StatusCode == http.StatusOK {
if json.NewDecoder(resp.Body).Decode(&configResp) == nil { if json.NewDecoder(resp.Body).Decode(&configResp) == nil {
configFetched = true configFetched = true
if configResp.APIPrefix != "" { if configResp.APIPrefix != "" {
@@ -3091,6 +3156,10 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
} }
if configResp.Title != "" { if configResp.Title != "" {
discovery.Title = configResp.Title discovery.Title = configResp.Title
if strings.Contains(strings.ToLower(configResp.Title), "omniforge") {
discovery.IsOmniForge = true
}
}
} }
} }
resp.Body.Close() resp.Body.Close()
@@ -3106,13 +3175,36 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
// 3. Inspect Hugging Face Space Metadata if hosted on HF // 3. Inspect Hugging Face Space Metadata if hosted on HF
parsedURL, _ := url.Parse(cleanURL) parsedURL, _ := url.Parse(cleanURL)
if parsedURL != nil && (strings.HasSuffix(parsedURL.Host, ".hf.space") || strings.Contains(parsedURL.Host, "huggingface.co")) { if parsedURL != nil && (strings.HasSuffix(parsedURL.Host, ".hf.space") || strings.Contains(parsedURL.Host, "huggingface.co")) {
subdomain := strings.TrimSuffix(parsedURL.Host, ".hf.space") if linkHeader == "" {
req, err := http.NewRequest("GET", cleanURL, nil)
if err == nil {
req.Header.Set("User-Agent", userAgent)
resp, _ := client.Do(req)
if resp != nil {
if l := resp.Header.Get("Link"); l != "" {
linkHeader = l
}
resp.Body.Close()
}
}
}
var owner, name string var owner, name string
hfLinkRegex := regexp.MustCompile(`(?i)https:\/\/huggingface\.co\/spaces\/([^\/>\s]+)\/([^\/>\s;]+)`)
if linkHeader != "" {
if m := hfLinkRegex.FindStringSubmatch(linkHeader); len(m) >= 3 {
owner = m[1]
name = m[2]
}
}
if owner == "" || name == "" {
subdomain := strings.TrimSuffix(parsedURL.Host, ".hf.space")
dashIdx := strings.Index(subdomain, "-") dashIdx := strings.Index(subdomain, "-")
if dashIdx != -1 { if dashIdx != -1 {
owner = subdomain[:dashIdx] owner = subdomain[:dashIdx]
name = subdomain[dashIdx+1:] name = subdomain[dashIdx+1:]
} }
}
if owner != "" && name != "" { if owner != "" && name != "" {
hfAPIURL := fmt.Sprintf("https://huggingface.co/api/spaces/%s/%s", owner, name) hfAPIURL := fmt.Sprintf("https://huggingface.co/api/spaces/%s/%s", owner, name)
@@ -3130,10 +3222,32 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
if cleanM != m { if cleanM != m {
discovery.Models = append(discovery.Models, cleanM) discovery.Models = append(discovery.Models, cleanM)
} }
if slashIdx := strings.LastIndex(cleanM, "/"); slashIdx != -1 {
shortM := cleanM[slashIdx+1:]
discovery.Models = append(discovery.Models, shortM, strings.ToLower(shortM))
}
}
for _, m := range hfResp.CardData.Models {
discovery.Models = append(discovery.Models, m)
cleanM := strings.TrimPrefix(m, "openai/")
cleanM = strings.TrimPrefix(cleanM, "models/")
if cleanM != m {
discovery.Models = append(discovery.Models, cleanM)
}
if slashIdx := strings.LastIndex(cleanM, "/"); slashIdx != -1 {
shortM := cleanM[slashIdx+1:]
discovery.Models = append(discovery.Models, shortM, strings.ToLower(shortM))
}
}
if name != "" {
discovery.Models = append(discovery.Models, name, strings.ToLower(name))
} }
if hfResp.CardData.Title != "" && discovery.Title == "" { if hfResp.CardData.Title != "" && discovery.Title == "" {
discovery.Title = hfResp.CardData.Title discovery.Title = hfResp.CardData.Title
} }
if hfResp.CardData.SdkVersion != "" && (discovery.GradioVersion == "" || discovery.GradioVersion == "unknown") {
discovery.GradioVersion = hfResp.CardData.SdkVersion
}
if len(discovery.Models) > 0 { if len(discovery.Models) > 0 {
discovery.PrimaryModel = discovery.Models[0] discovery.PrimaryModel = discovery.Models[0]
} }
@@ -3289,6 +3403,7 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
discovery.ParamMappings = nil discovery.ParamMappings = nil
discovery.MessageIndex = -1 discovery.MessageIndex = -1
paramIdx := 0
for idx, compID := range bestMatchingDep.Inputs { for idx, compID := range bestMatchingDep.Inputs {
mapping := SpaceParamMapping{ mapping := SpaceParamMapping{
InputIndex: idx, InputIndex: idx,
@@ -3296,39 +3411,21 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
ParamType: "other", ParamType: "other",
} }
var pName, pLabel, pComp string var cType, cLabel string
if bestEndpointInfo != nil && idx < len(bestEndpointInfo.Parameters) {
p := bestEndpointInfo.Parameters[idx]
pName = strings.ToLower(p.ParameterName)
pLabel = strings.ToLower(p.Label)
pComp = strings.ToLower(p.Component)
if p.ParameterDefault != nil && discovery.DefaultInputs[idx] == nil {
discovery.DefaultInputs[idx] = p.ParameterDefault
mapping.DefaultValue = p.ParameterDefault
}
mapping.Label = p.ParameterName
mapping.ParamName = p.ParameterName
if p.Component != "" {
mapping.ComponentType = p.Component
}
}
if comp, exists := compMap[compID]; exists { if comp, exists := compMap[compID]; exists {
if mapping.ComponentType == "" { cType = strings.ToLower(comp.Type)
mapping.ComponentType = comp.Type mapping.ComponentType = comp.Type
}
cType := strings.ToLower(comp.Type)
cLabel := ""
if comp.Props != nil { if comp.Props != nil {
if l, ok := comp.Props["label"].(string); ok { if l, ok := comp.Props["label"].(string); ok {
cLabel = strings.ToLower(l) cLabel = strings.ToLower(l)
if mapping.Label == "" {
mapping.Label = l mapping.Label = l
} }
} if val, ok := comp.Props["value"]; ok && val != nil {
if val, ok := comp.Props["value"]; ok {
discovery.DefaultInputs[idx] = val discovery.DefaultInputs[idx] = val
mapping.DefaultValue = val mapping.DefaultValue = val
} else if defVal, ok := comp.Props["default_value"]; ok {
discovery.DefaultInputs[idx] = defVal
mapping.DefaultValue = defVal
} }
if chList, ok := comp.Props["choices"].([]interface{}); ok { if chList, ok := comp.Props["choices"].([]interface{}); ok {
for _, ch := range chList { for _, ch := range chList {
@@ -3342,8 +3439,48 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
} }
} }
} }
if bestEndpointInfo != nil && idx < len(bestEndpointInfo.Parameters) && len(mapping.Choices) == 0 { }
p := bestEndpointInfo.Parameters[idx]
isInternalState := false
if comp, exists := compMap[compID]; exists {
if comp.SkipAPI {
isInternalState = true
} else if cType == "state" {
if bestEndpointInfo != nil && paramIdx < len(bestEndpointInfo.Parameters) && strings.ToLower(bestEndpointInfo.Parameters[paramIdx].Component) != "state" {
isInternalState = true
} else if bestEndpointInfo != nil && paramIdx >= len(bestEndpointInfo.Parameters) {
isInternalState = true
}
}
} else if cType == "state" {
isInternalState = true
}
var pName, pLabel, pComp string
var p *GradioParamInfo
if !isInternalState && bestEndpointInfo != nil && paramIdx < len(bestEndpointInfo.Parameters) {
p = &bestEndpointInfo.Parameters[paramIdx]
paramIdx++
pName = strings.ToLower(p.ParameterName)
pLabel = strings.ToLower(p.Label)
pComp = strings.ToLower(p.Component)
if p.ParameterDefault != nil && (discovery.DefaultInputs[idx] == nil || discovery.DefaultInputs[idx] == "") {
discovery.DefaultInputs[idx] = p.ParameterDefault
mapping.DefaultValue = p.ParameterDefault
}
if mapping.Label == "" {
mapping.Label = p.ParameterName
}
mapping.ParamName = p.ParameterName
if mapping.ComponentType == "" && p.Component != "" {
mapping.ComponentType = p.Component
}
}
if isInternalState {
mapping.ParamType = "state"
} else {
if p != nil && len(mapping.Choices) == 0 {
if typeMap, ok := p.Type.(map[string]interface{}); ok { if typeMap, ok := p.Type.(map[string]interface{}); ok {
if enumArr, ok := typeMap["enum"].([]interface{}); ok { if enumArr, ok := typeMap["enum"].([]interface{}); ok {
for _, e := range enumArr { for _, e := range enumArr {
@@ -3355,6 +3492,24 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
} }
} }
isObjectOrDict := false
if p != nil {
bType, _ := json.Marshal(p.Type)
bPyType, _ := json.Marshal(p.PythonType)
pTypeStr := strings.ToLower(string(bType))
pPyTypeStr := strings.ToLower(string(bPyType))
if strings.Contains(pTypeStr, "object") || strings.Contains(pPyTypeStr, "dict") {
isObjectOrDict = true
}
}
if comp, exists := compMap[compID]; exists && comp.Props != nil {
if apiInfo, ok := comp.Props["_api_info"].(map[string]interface{}); ok {
if t, ok := apiInfo["type"].(string); ok && t == "object" {
isObjectOrDict = true
}
}
}
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") { 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" mapping.ParamType = "tools"
discovery.FunctionsJSONIndex = idx discovery.FunctionsJSONIndex = idx
@@ -3367,7 +3522,7 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
} else if strings.Contains(pName, "system") || strings.Contains(pLabel, "system") || strings.Contains(cLabel, "system") || strings.Contains(cLabel, "instruction") { } else if strings.Contains(pName, "system") || strings.Contains(pLabel, "system") || strings.Contains(cLabel, "system") || strings.Contains(cLabel, "instruction") {
mapping.ParamType = "system_prompt" mapping.ParamType = "system_prompt"
discovery.SystemIndex = idx discovery.SystemIndex = idx
if comp.Props != nil { if comp, exists := compMap[compID]; exists && comp.Props != nil {
if val, ok := comp.Props["value"].(string); ok && strings.TrimSpace(val) != "" { if val, ok := comp.Props["value"].(string); ok && strings.TrimSpace(val) != "" {
discovery.DefaultSystemPrompt = strings.TrimSpace(val) discovery.DefaultSystemPrompt = strings.TrimSpace(val)
} }
@@ -3375,14 +3530,37 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
} else if strings.Contains(pName, "history") || strings.Contains(pLabel, "history") || strings.Contains(cLabel, "history") || strings.Contains(cLabel, "chat") || cType == "chatbot" { } else if strings.Contains(pName, "history") || strings.Contains(pLabel, "history") || strings.Contains(cLabel, "history") || strings.Contains(cLabel, "chat") || cType == "chatbot" {
mapping.ParamType = "history" mapping.ParamType = "history"
discovery.HistoryIndex = idx discovery.HistoryIndex = idx
} else if cType == "multimodaltextbox" || strings.Contains(pComp, "multimodal") { } else if cType == "image" || strings.Contains(pComp, "image") || strings.Contains(pName, "image") || strings.Contains(pLabel, "image") || strings.Contains(cLabel, "image") {
mapping.ParamType = "image"
discovery.ImageIndex = idx
} else if cType == "multimodaltextbox" || strings.Contains(pComp, "multimodal") || ((strings.Contains(pName, "message") || strings.Contains(pLabel, "message") || strings.Contains(cLabel, "message")) && isObjectOrDict) {
mapping.ParamType = "message" mapping.ParamType = "message"
discovery.MessageIndex = idx discovery.MessageIndex = idx
discovery.MessageIsMultimodal = true discovery.MessageIsMultimodal = true
} else if strings.Contains(pName, "message") || strings.Contains(pLabel, "message") || strings.Contains(cLabel, "message") || strings.Contains(cLabel, "prompt") || strings.Contains(cLabel, "query") || (discovery.MessageIndex == -1 && idx == 0 && (cType == "textbox" || pComp == "textbox")) { } else if strings.Contains(pName, "message") || strings.Contains(pLabel, "message") || strings.Contains(cLabel, "message") || strings.Contains(cLabel, "prompt") || strings.Contains(cLabel, "query") || (discovery.MessageIndex == -1 && idx == 0 && (cType == "textbox" || pComp == "textbox")) {
mapping.ParamType = "message" mapping.ParamType = "message"
discovery.MessageIndex = idx discovery.MessageIndex = idx
discovery.MessageIsMultimodal = false discovery.MessageIsMultimodal = isObjectOrDict
} else if (strings.Contains(pName, "model") || strings.Contains(pLabel, "model") || strings.Contains(cLabel, "model")) && (cType == "dropdown" || cType == "radio" || len(mapping.Choices) > 0 || cType == "textbox") {
mapping.ParamType = "model"
discovery.ModelIndex = idx
if len(mapping.Choices) > 0 {
for _, ch := range mapping.Choices {
if ch != "" {
discovery.Models = append(discovery.Models, ch)
if slashIdx := strings.LastIndex(ch, "/"); slashIdx != -1 {
discovery.Models = append(discovery.Models, ch[slashIdx+1:], strings.ToLower(ch[slashIdx+1:]))
}
}
}
if discovery.PrimaryModel == "" || discovery.PrimaryModel == "gradio-chat" {
if defStr, ok := mapping.DefaultValue.(string); ok && defStr != "" {
discovery.PrimaryModel = defStr
} else {
discovery.PrimaryModel = mapping.Choices[0]
}
}
}
} else if strings.Contains(pName, "temp") || strings.Contains(cLabel, "temp") { } else if strings.Contains(pName, "temp") || strings.Contains(cLabel, "temp") {
mapping.ParamType = "temperature" mapping.ParamType = "temperature"
discovery.TempIndex = idx discovery.TempIndex = idx
@@ -3398,8 +3576,10 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
} else if strings.Contains(pName, "search") || strings.Contains(pLabel, "search") || strings.Contains(cLabel, "search") || strings.Contains(pName, "browse") || strings.Contains(pLabel, "browse") || strings.Contains(cLabel, "browse") || strings.Contains(pName, "web") || strings.Contains(pLabel, "web") || strings.Contains(cLabel, "web") { } else if strings.Contains(pName, "search") || strings.Contains(pLabel, "search") || strings.Contains(cLabel, "search") || strings.Contains(pName, "browse") || strings.Contains(pLabel, "browse") || strings.Contains(cLabel, "browse") || strings.Contains(pName, "web") || strings.Contains(pLabel, "web") || strings.Contains(cLabel, "web") {
mapping.ParamType = "web_search" mapping.ParamType = "web_search"
discovery.WebSearchIndex = idx discovery.WebSearchIndex = idx
} else if cType == "state" { }
mapping.ParamType = "state"
if strings.Contains(cLabel, "omniforge") || strings.Contains(pLabel, "omniforge") {
discovery.IsOmniForge = true
} }
} }
discovery.ParamMappings = append(discovery.ParamMappings, mapping) discovery.ParamMappings = append(discovery.ParamMappings, mapping)
@@ -3413,6 +3593,13 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
if strings.ToLower(comp.Type) == "multimodaltextbox" { if strings.ToLower(comp.Type) == "multimodaltextbox" {
discovery.MessageIsMultimodal = true discovery.MessageIsMultimodal = true
} }
if comp.Props != nil {
if apiInfo, ok := comp.Props["_api_info"].(map[string]interface{}); ok {
if t, ok := apiInfo["type"].(string); ok && t == "object" {
discovery.MessageIsMultimodal = true
}
}
}
} }
} }
} }
@@ -3424,18 +3611,21 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
// and trailing inputs in bestMatchingDep.Inputs are unexposed server-side state components, // and trailing inputs in bestMatchingDep.Inputs are unexposed server-side state components,
// do not pad them with null so Gradio preserves its internal server state. // do not pad them with null so Gradio preserves its internal server state.
if bestEndpointInfo != nil && len(bestEndpointInfo.Parameters) > 0 && len(bestMatchingDep.Inputs) > len(bestEndpointInfo.Parameters) { if bestEndpointInfo != nil && len(bestEndpointInfo.Parameters) > 0 && len(bestMatchingDep.Inputs) > len(bestEndpointInfo.Parameters) {
allTrailingAreState := true allTrailingAreUnexposedState := true
for i := len(bestEndpointInfo.Parameters); i < len(bestMatchingDep.Inputs); i++ { for i := len(bestEndpointInfo.Parameters); i < len(bestMatchingDep.Inputs); i++ {
cID := bestMatchingDep.Inputs[i] cID := bestMatchingDep.Inputs[i]
if comp, exists := compMap[cID]; exists { if comp, exists := compMap[cID]; exists {
cType := strings.ToLower(comp.Type) cType := strings.ToLower(comp.Type)
if cType != "state" && cType != "browserstate" { if !comp.SkipAPI && cType != "state" {
allTrailingAreState = false allTrailingAreUnexposedState = false
break
}
} else {
allTrailingAreUnexposedState = false
break break
} }
} }
} if allTrailingAreUnexposedState {
if allTrailingAreState {
discovery.TotalInputs = len(bestEndpointInfo.Parameters) discovery.TotalInputs = len(bestEndpointInfo.Parameters)
discovery.DefaultInputs = discovery.DefaultInputs[:discovery.TotalInputs] discovery.DefaultInputs = discovery.DefaultInputs[:discovery.TotalInputs]
if len(discovery.ParamMappings) > discovery.TotalInputs { if len(discovery.ParamMappings) > discovery.TotalInputs {
@@ -3468,6 +3658,9 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
if discovery.WebSearchIndex >= discovery.TotalInputs { if discovery.WebSearchIndex >= discovery.TotalInputs {
discovery.WebSearchIndex = -1 discovery.WebSearchIndex = -1
} }
if discovery.ModelIndex >= discovery.TotalInputs {
discovery.ModelIndex = -1
}
} }
} }
} }
@@ -3517,7 +3710,16 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
DefaultValue: p.ParameterDefault, DefaultValue: p.ParameterDefault,
} }
if strings.Contains(pComp, "multimodal") { isObjectOrDict := false
bType, _ := json.Marshal(p.Type)
bPyType, _ := json.Marshal(p.PythonType)
pTypeStr := strings.ToLower(string(bType))
pPyTypeStr := strings.ToLower(string(bPyType))
if strings.Contains(pTypeStr, "object") || strings.Contains(pPyTypeStr, "dict") {
isObjectOrDict = true
}
if strings.Contains(pComp, "multimodal") || ((strings.Contains(pName, "message") || strings.Contains(pLabel, "message")) && isObjectOrDict) {
if discovery.MessageIndex == -1 || strings.Contains(pLabel, "message") || strings.Contains(pName, "message") { if discovery.MessageIndex == -1 || strings.Contains(pLabel, "message") || strings.Contains(pName, "message") {
mapping.ParamType = "message" mapping.ParamType = "message"
discovery.MessageIndex = idx discovery.MessageIndex = idx
@@ -3534,19 +3736,18 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
} else if strings.Contains(pName, "history") || strings.Contains(pLabel, "history") || strings.Contains(pName, "chat") || strings.Contains(pName, "messages") || strings.Contains(pName, "conversation") || pComp == "chatbot" { } else if strings.Contains(pName, "history") || strings.Contains(pLabel, "history") || strings.Contains(pName, "chat") || strings.Contains(pName, "messages") || strings.Contains(pName, "conversation") || pComp == "chatbot" {
discovery.HistoryIndex = idx discovery.HistoryIndex = idx
mapping.ParamType = "history" mapping.ParamType = "history"
bType, _ := json.Marshal(p.Type) if strings.Contains(pPyTypeStr, "list[tuple[") || strings.Contains(pPyTypeStr, "list[list[") || strings.Contains(pTypeStr, "tuple") {
bPyType, _ := json.Marshal(p.PythonType)
pPyType := strings.ToLower(string(bPyType))
bTypeStr := strings.ToLower(string(bType))
if strings.Contains(pPyType, "list[tuple[") || strings.Contains(pPyType, "list[list[") || strings.Contains(bTypeStr, "tuple") {
discovery.HistoryFormat = "pairs" discovery.HistoryFormat = "pairs"
} else if strings.Contains(pPyType, "textmessage") || strings.Contains(pPyType, "dict(text: str") || strings.Contains(bTypeStr, "textmessage") || strings.Contains(bTypeStr, "chatbotdatamessages") { } else if strings.Contains(pPyTypeStr, "textmessage") || strings.Contains(pPyTypeStr, "dict(text: str") || strings.Contains(pTypeStr, "textmessage") || strings.Contains(pTypeStr, "chatbotdatamessages") {
discovery.HistoryFormat = "gradio_messages" discovery.HistoryFormat = "gradio_messages"
} }
} else if pComp == "image" || strings.Contains(pName, "image") || strings.Contains(pLabel, "image") {
discovery.ImageIndex = idx
mapping.ParamType = "image"
} else if strings.Contains(pLabel, "message") || strings.Contains(pName, "message") || (strings.Contains(pLabel, "prompt") && !strings.Contains(pLabel, "system")) || (strings.Contains(pName, "prompt") && !strings.Contains(pName, "system")) || strings.Contains(pLabel, "query") || strings.Contains(pName, "query") || strings.Contains(pLabel, "question") || strings.Contains(pName, "question") || (discovery.MessageIndex == -1 && (pComp == "textbox" || (idx == 0 && pComp != "chatbot"))) { } else if strings.Contains(pLabel, "message") || strings.Contains(pName, "message") || (strings.Contains(pLabel, "prompt") && !strings.Contains(pLabel, "system")) || (strings.Contains(pName, "prompt") && !strings.Contains(pName, "system")) || strings.Contains(pLabel, "query") || strings.Contains(pName, "query") || strings.Contains(pLabel, "question") || strings.Contains(pName, "question") || (discovery.MessageIndex == -1 && (pComp == "textbox" || (idx == 0 && pComp != "chatbot"))) {
discovery.MessageIndex = idx discovery.MessageIndex = idx
mapping.ParamType = "message" mapping.ParamType = "message"
discovery.MessageIsMultimodal = false discovery.MessageIsMultimodal = isObjectOrDict
} else if strings.Contains(pName, "think_level") || strings.Contains(pName, "thinking_level") || strings.Contains(pLabel, "think") { } else if strings.Contains(pName, "think_level") || strings.Contains(pName, "thinking_level") || strings.Contains(pLabel, "think") {
discovery.ThinkLevelIndex = idx discovery.ThinkLevelIndex = idx
mapping.ParamType = "think_level" mapping.ParamType = "think_level"
@@ -3595,7 +3796,9 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
// 7. Resolve default history format if not set by type inspection // 7. Resolve default history format if not set by type inspection
if discovery.HistoryIndex != -1 { if discovery.HistoryIndex != -1 {
if discovery.HistoryFormat == "" || discovery.HistoryFormat == "messages" { if discovery.HistoryFormat == "" || discovery.HistoryFormat == "messages" {
if strings.HasPrefix(discovery.GradioVersion, "5.") || strings.HasPrefix(discovery.GradioVersion, "6.") { if discovery.Flavor == "server" {
discovery.HistoryFormat = "messages"
} else if strings.HasPrefix(discovery.GradioVersion, "5.") || strings.HasPrefix(discovery.GradioVersion, "6.") {
discovery.HistoryFormat = "gradio_messages" discovery.HistoryFormat = "gradio_messages"
} else { } else {
discovery.HistoryFormat = "pairs" discovery.HistoryFormat = "pairs"
@@ -3606,7 +3809,8 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
} }
// 8. Hunyuan3 / Tencent Hy3 overrides // 8. Hunyuan3 / Tencent Hy3 overrides
if discovery.FunctionsJSONIndex != -1 || discovery.ThinkLevelIndex != -1 || strings.Contains(cleanURL, "hy3") || strings.Contains(cleanURL, "hunyuan") { isHy3Space := strings.Contains(cleanURL, "hy3") || strings.Contains(cleanURL, "hunyuan") || (discovery.FunctionsJSONIndex != -1 && discovery.PreservedThinkingIndex != -1)
if isHy3Space {
discovery.IsHunyuan3 = true discovery.IsHunyuan3 = true
discovery.HistoryFormat = "messages" discovery.HistoryFormat = "messages"
discovery.Models = append(discovery.Models, "hy3", "hunyuan3", "tencent/Hy3") discovery.Models = append(discovery.Models, "hy3", "hunyuan3", "tencent/Hy3")
@@ -3623,6 +3827,8 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
discovery.ToolCallMode = "native_slot" discovery.ToolCallMode = "native_slot"
} else if discovery.SystemIndex >= 0 { } else if discovery.SystemIndex >= 0 {
discovery.ToolCallMode = "prompt_augmented_system" discovery.ToolCallMode = "prompt_augmented_system"
} else if discovery.IsOmniForge {
discovery.ToolCallMode = "prompt_augmented_first_turn"
} else if discovery.HistoryIndex >= 0 { } else if discovery.HistoryIndex >= 0 {
discovery.ToolCallMode = "prompt_augmented_first_turn" discovery.ToolCallMode = "prompt_augmented_first_turn"
} else { } else {
@@ -3873,7 +4079,7 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
var promptMessageText string var promptMessageText string
isToolReturn := strings.HasPrefix(lastUserMessage, "<tool_response>") || strings.HasPrefix(lastUserMessage, "Tool result") isToolReturn := strings.HasPrefix(lastUserMessage, "<tool_response>") || strings.HasPrefix(lastUserMessage, "Tool result")
if disc.HistoryIndex != -1 { if disc.HistoryIndex != -1 && !disc.IsOmniForge {
if disc.SystemIndex == -1 { if disc.SystemIndex == -1 {
if len(nonSystem) > 1 && toolInstruction != "" { if len(nonSystem) > 1 && toolInstruction != "" {
if !isToolReturn { if !isToolReturn {
@@ -3892,7 +4098,7 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
} }
} }
} else { } else {
// Single message space: compose multi-turn history into the prompt // Single message space or OmniForge: compose multi-turn history into the prompt
if len(nonSystem) <= 1 { if len(nonSystem) <= 1 {
if systemPromptStr != "" && len(nonSystem) == 1 { if systemPromptStr != "" && len(nonSystem) == 1 {
if toolInstruction != "" && !isToolReturn { if toolInstruction != "" && !isToolReturn {
@@ -3946,11 +4152,11 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
} }
} }
// Extract multimodal files if message input is multimodal // Extract multimodal files if message input is multimodal or image slot is present
var messageFiles []interface{} var messageFiles []interface{}
if disc.MessageIsMultimodal && len(nonSystem) > 0 { var primaryImage interface{}
lastMsg := nonSystem[len(nonSystem)-1] for _, m := range req.Messages {
if parts, ok := lastMsg.Content.([]interface{}); ok { if parts, ok := m.Content.([]interface{}); ok {
for _, p := range parts { for _, p := range parts {
if itemMap, ok := p.(map[string]interface{}); ok { if itemMap, ok := p.(map[string]interface{}); ok {
if itemMap["type"] == "image_url" { if itemMap["type"] == "image_url" {
@@ -3963,11 +4169,15 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
imgURL = iuStr imgURL = iuStr
} }
if imgURL != "" { if imgURL != "" {
messageFiles = append(messageFiles, map[string]interface{}{ fileData := map[string]interface{}{
"path": imgURL, "path": imgURL,
"url": imgURL, "url": imgURL,
"meta": map[string]interface{}{"_type": "gradio.FileData"}, "meta": map[string]interface{}{"_type": "gradio.FileData"},
}) }
messageFiles = append(messageFiles, fileData)
if primaryImage == nil {
primaryImage = fileData
}
} }
} }
} }
@@ -3991,8 +4201,14 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
} }
} }
if disc.ImageIndex >= 0 && disc.ImageIndex != disc.MessageIndex && disc.ImageIndex != disc.HistoryIndex && disc.ImageIndex < len(data) {
data[disc.ImageIndex] = primaryImage
}
if disc.HistoryIndex >= 0 && disc.HistoryIndex < len(data) { if disc.HistoryIndex >= 0 && disc.HistoryIndex < len(data) {
if disc.HistoryFormat == "pairs" { if disc.IsOmniForge {
data[disc.HistoryIndex] = []map[string]interface{}{}
} else if disc.HistoryFormat == "pairs" {
var pairs [][]string var pairs [][]string
for i := 0; i < len(historyArray); i += 2 { for i := 0; i < len(historyArray); i += 2 {
u := "" u := ""
@@ -4044,6 +4260,9 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
"content": promptMessageText, "content": promptMessageText,
}) })
} }
if historyArray == nil {
historyArray = []map[string]interface{}{}
}
data[disc.HistoryIndex] = historyArray data[disc.HistoryIndex] = historyArray
} }
} }
@@ -4058,6 +4277,31 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
} }
if disc.ThinkLevelIndex >= 0 && disc.ThinkLevelIndex != disc.MessageIndex && disc.ThinkLevelIndex != disc.HistoryIndex && disc.ThinkLevelIndex < len(data) { if disc.ThinkLevelIndex >= 0 && disc.ThinkLevelIndex != disc.MessageIndex && disc.ThinkLevelIndex != disc.HistoryIndex && disc.ThinkLevelIndex < len(data) {
isCheckbox := false
if disc.ThinkLevelIndex < len(disc.ParamMappings) {
cType := strings.ToLower(disc.ParamMappings[disc.ThinkLevelIndex].ComponentType)
if cType == "checkbox" || cType == "bool" || cType == "boolean" {
isCheckbox = true
}
}
if !isCheckbox {
if _, isBool := data[disc.ThinkLevelIndex].(bool); isBool {
isCheckbox = true
}
}
if isCheckbox {
thinkBool := true
if req.ReasoningEffort != "" {
effort := strings.ToLower(req.ReasoningEffort)
if effort == "none" || effort == "off" || effort == "no_think" || effort == "0" || effort == "false" {
thinkBool = false
} else {
thinkBool = true
}
}
data[disc.ThinkLevelIndex] = thinkBool
} else {
thinkLevel := "high" thinkLevel := "high"
if req.ReasoningEffort != "" { if req.ReasoningEffort != "" {
effort := strings.ToLower(req.ReasoningEffort) effort := strings.ToLower(req.ReasoningEffort)
@@ -4074,6 +4318,7 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
} }
data[disc.ThinkLevelIndex] = thinkLevel data[disc.ThinkLevelIndex] = thinkLevel
} }
}
if disc.PreservedThinkingIndex >= 0 && disc.PreservedThinkingIndex != disc.MessageIndex && disc.PreservedThinkingIndex != disc.HistoryIndex && disc.PreservedThinkingIndex != disc.SystemIndex && disc.PreservedThinkingIndex < len(data) { if disc.PreservedThinkingIndex >= 0 && disc.PreservedThinkingIndex != disc.MessageIndex && disc.PreservedThinkingIndex != disc.HistoryIndex && disc.PreservedThinkingIndex != disc.SystemIndex && disc.PreservedThinkingIndex < len(data) {
data[disc.PreservedThinkingIndex] = nil data[disc.PreservedThinkingIndex] = nil
@@ -4122,7 +4367,51 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
data[disc.FunctionsJSONIndex] = functionsJSONStr data[disc.FunctionsJSONIndex] = functionsJSONStr
} }
if disc.WebSearchIndex >= 0 && disc.WebSearchIndex != disc.MessageIndex && disc.WebSearchIndex != disc.HistoryIndex && disc.WebSearchIndex != disc.SystemIndex && disc.WebSearchIndex < len(data) && len(req.Tools) > 0 { if disc.WebSearchIndex >= 0 && disc.WebSearchIndex != disc.MessageIndex && disc.WebSearchIndex != disc.HistoryIndex && disc.WebSearchIndex != disc.SystemIndex && disc.WebSearchIndex < len(data) {
if req.WebSearch != nil {
if *req.WebSearch {
var mapping *SpaceParamMapping
for i := range disc.ParamMappings {
if disc.ParamMappings[i].InputIndex == disc.WebSearchIndex {
mapping = &disc.ParamMappings[i]
break
}
}
if mapping != nil && len(mapping.Choices) > 0 {
for _, choice := range mapping.Choices {
cLower := strings.ToLower(choice)
if cLower == "enable" || cLower == "on" || cLower == "enabled" || strings.Contains(cLower, "search") {
data[disc.WebSearchIndex] = choice
break
}
}
} else {
data[disc.WebSearchIndex] = true
}
} else {
var mapping *SpaceParamMapping
for i := range disc.ParamMappings {
if disc.ParamMappings[i].InputIndex == disc.WebSearchIndex {
mapping = &disc.ParamMappings[i]
break
}
}
disabledSet := false
if mapping != nil && len(mapping.Choices) > 0 {
for _, choice := range mapping.Choices {
cLower := strings.ToLower(choice)
if cLower == "direct" || cLower == "off" || cLower == "disabled" || cLower == "none" || cLower == "false" || cLower == "no" || strings.Contains(cLower, "direct") || strings.Contains(cLower, "no search") || strings.Contains(cLower, "disable") {
data[disc.WebSearchIndex] = choice
disabledSet = true
break
}
}
}
if !disabledSet {
data[disc.WebSearchIndex] = false
}
}
} else if len(req.Tools) > 0 {
var mapping *SpaceParamMapping var mapping *SpaceParamMapping
for i := range disc.ParamMappings { for i := range disc.ParamMappings {
if disc.ParamMappings[i].InputIndex == disc.WebSearchIndex { if disc.ParamMappings[i].InputIndex == disc.WebSearchIndex {
@@ -4153,6 +4442,69 @@ func (g *GradioGateway) BuildGradioPayload(disc *SpaceDiscovery, req ChatComplet
} }
} }
} }
}
if disc.ModelIndex >= 0 && disc.ModelIndex != disc.MessageIndex && disc.ModelIndex < len(data) {
var mapping *SpaceParamMapping
for i := range disc.ParamMappings {
if disc.ParamMappings[i].InputIndex == disc.ModelIndex {
mapping = &disc.ParamMappings[i]
break
}
}
targetModel := req.Model
if targetModel == "" {
targetModel = ConfiguredModelName
}
if targetModel != "" && mapping != nil && len(mapping.Choices) > 0 {
matchedChoice := ""
for _, ch := range mapping.Choices {
if ch == targetModel {
matchedChoice = ch
break
}
}
if matchedChoice == "" {
for _, ch := range mapping.Choices {
if strings.EqualFold(ch, targetModel) {
matchedChoice = ch
break
}
}
}
if matchedChoice == "" {
for _, ch := range mapping.Choices {
if strings.HasSuffix(strings.ToLower(ch), "/"+strings.ToLower(targetModel)) || strings.EqualFold(ch, targetModel) {
matchedChoice = ch
break
}
}
}
if matchedChoice == "" {
for _, ch := range mapping.Choices {
if strings.Contains(strings.ToLower(ch), strings.ToLower(targetModel)) {
matchedChoice = ch
break
}
}
}
if matchedChoice != "" {
data[disc.ModelIndex] = matchedChoice
}
} else if targetModel != "" && (mapping == nil || len(mapping.Choices) == 0) {
data[disc.ModelIndex] = targetModel
}
}
for idx := range data {
if idx < len(disc.ParamMappings) {
pType := strings.ToLower(disc.ParamMappings[idx].ComponentType)
pName := strings.ToLower(disc.ParamMappings[idx].ParamName)
if (pType == "browserstate" || strings.Contains(pName, "memory")) && data[idx] == nil {
data[idx] = []interface{}{}
}
}
}
return data, nil return data, nil
} }
@@ -4302,6 +4654,13 @@ func extractGradioDiffDelta(v []interface{}) (contentDelta string, reasoningDelt
func ParseGradioStreamOutput(rawJSON string) (frame GradioOutputFrame) { func ParseGradioStreamOutput(rawJSON string) (frame GradioOutputFrame) {
isNativeTuple := false isNativeTuple := false
defer func() { defer func() {
if !isNativeTuple && frame.OK && frame.Reasoning == "" && frame.Content != "" {
clean, reasoning := ExtractThinking(frame.Content)
if clean != frame.Content || reasoning != "" {
frame.Content = clean
frame.Reasoning = reasoning
}
}
if !isNativeTuple && frame.OK && len(frame.ToolCalls) == 0 && frame.Content != "" { if !isNativeTuple && frame.OK && len(frame.ToolCalls) == 0 && frame.Content != "" {
tcs, clean, has := DetectToolCalls(frame.Content) tcs, clean, has := DetectToolCalls(frame.Content)
if has && len(tcs) > 0 { if has && len(tcs) > 0 {
@@ -4337,6 +4696,14 @@ func ParseGradioStreamOutput(rawJSON string) (frame GradioOutputFrame) {
// 1. Check if v[0] is an inner slice with len >= 2 (e.g. Hy3: [[content, reasoning, tool_calls, history]]) // 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 { if inner, ok := v[0].([]interface{}); ok && len(inner) >= 2 {
isChatbotItem := false
if inner[0] != nil {
switch inner[0].(type) {
case map[string]interface{}, []interface{}:
isChatbotItem = true
}
}
if !isChatbotItem {
isNativeTuple = true isNativeTuple = true
s0, _ := inner[0].(string) s0, _ := inner[0].(string)
s1, _ := inner[1].(string) s1, _ := inner[1].(string)
@@ -4354,6 +4721,7 @@ func ParseGradioStreamOutput(rawJSON string) (frame GradioOutputFrame) {
frame.OK = true frame.OK = true
return frame return frame
} }
}
// 2. Check if any element of v is a Chatbot message list or Chatbot pair list // 2. Check if any element of v is a Chatbot message list or Chatbot pair list
for _, elem := range v { for _, elem := range v {
@@ -4742,6 +5110,9 @@ func (g *GradioGateway) executeQueueCompletion(w http.ResponseWriter, r *http.Re
} }
if resp == nil { if resp == nil {
if lastErr != nil && strings.Contains(strings.ToLower(lastErr.Error()), "space is paused") {
return fmt.Errorf("upstream space is paused: %w", lastErr)
}
log.Printf("Gradio /queue/join unavailable (%v), falling back to /run/predict protocol...", lastErr) log.Printf("Gradio /queue/join unavailable (%v), falling back to /run/predict protocol...", lastErr)
return g.executePredictCompletion(w, r, disc, gradioData, req, completionID, createdTime, modelName, effUA) return g.executePredictCompletion(w, r, disc, gradioData, req, completionID, createdTime, modelName, effUA)
} }
@@ -4880,7 +5251,7 @@ func (g *GradioGateway) executeQueueCompletion(w http.ResponseWriter, r *http.Re
var prevReasoning string var prevReasoning string
lastToolCallArgs := make(map[string]string) lastToolCallArgs := make(map[string]string)
emittedToolCallIDs := make(map[string]bool) emittedToolCallIDs := make(map[string]bool)
nativeReasoningSeen := disc.IsHunyuan3 || disc.ThinkLevelIndex >= 0 || disc.PreservedThinkingIndex >= 0 nativeReasoningSeen := disc.IsHunyuan3 || disc.PreservedThinkingIndex >= 0
nativeToolCallsSeen := false nativeToolCallsSeen := false
var streamErr error var streamErr error
@@ -4922,7 +5293,12 @@ func (g *GradioGateway) executeQueueCompletion(w http.ResponseWriter, r *http.Re
if isNativeToolCalling { if isNativeToolCalling {
// 1. Native reasoning // 1. Native reasoning
if len(frame.Reasoning) > len(prevReasoning) { if len(frame.Reasoning) > len(prevReasoning) {
deltaReasoning := frame.Reasoning[len(prevReasoning):] var deltaReasoning string
if strings.HasPrefix(frame.Reasoning, prevReasoning) {
deltaReasoning = frame.Reasoning[len(prevReasoning):]
} else {
deltaReasoning = frame.Reasoning
}
prevReasoning = frame.Reasoning prevReasoning = frame.Reasoning
streamer.Reasoning(deltaReasoning) streamer.Reasoning(deltaReasoning)
} else if frame.IsDelta && frame.Reasoning != "" { } else if frame.IsDelta && frame.Reasoning != "" {
@@ -4963,7 +5339,12 @@ func (g *GradioGateway) executeQueueCompletion(w http.ResponseWriter, r *http.Re
// 3. Native content // 3. Native content
if len(frame.Content) > len(prevContent) { if len(frame.Content) > len(prevContent) {
cDelta := frame.Content[len(prevContent):] var cDelta string
if strings.HasPrefix(frame.Content, prevContent) {
cDelta = frame.Content[len(prevContent):]
} else {
cDelta = frame.Content
}
prevContent = frame.Content prevContent = frame.Content
streamer.Content(cDelta) streamer.Content(cDelta)
} else if frame.IsDelta && frame.Content != "" { } else if frame.IsDelta && frame.Content != "" {
@@ -4978,8 +5359,10 @@ func (g *GradioGateway) executeQueueCompletion(w http.ResponseWriter, r *http.Re
deltaReasoning = frame.Reasoning deltaReasoning = frame.Reasoning
prevReasoning += deltaReasoning prevReasoning += deltaReasoning
} else { } else {
if len(frame.Reasoning) > len(prevReasoning) { if len(frame.Reasoning) > len(prevReasoning) && strings.HasPrefix(frame.Reasoning, prevReasoning) {
deltaReasoning = frame.Reasoning[len(prevReasoning):] deltaReasoning = frame.Reasoning[len(prevReasoning):]
} else if !strings.HasPrefix(frame.Reasoning, prevReasoning) {
deltaReasoning = frame.Reasoning
} }
prevReasoning = frame.Reasoning prevReasoning = frame.Reasoning
} }
@@ -5026,8 +5409,10 @@ func (g *GradioGateway) executeQueueCompletion(w http.ResponseWriter, r *http.Re
delta = frame.Content delta = frame.Content
prevContent += delta prevContent += delta
} else { } else {
if len(currentText) > len(prevContent) { if len(currentText) > len(prevContent) && strings.HasPrefix(currentText, prevContent) {
delta = currentText[len(prevContent):] delta = currentText[len(prevContent):]
} else if !strings.HasPrefix(currentText, prevContent) {
delta = currentText
} }
prevContent = currentText prevContent = currentText
} }
@@ -5186,6 +5571,9 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
if pName == "" { if pName == "" {
pName = fmt.Sprintf("param_%d", idx) pName = fmt.Sprintf("param_%d", idx)
} }
if val == nil && idx < len(disc.ParamMappings) && disc.ParamMappings[idx].DefaultValue != nil {
val = disc.ParamMappings[idx].DefaultValue
}
v2Payload[pName] = val v2Payload[pName] = val
} }
return json.Marshal(v2Payload) return json.Marshal(v2Payload)
@@ -5227,6 +5615,9 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
resp, err = DoWithFibonacciRetry(g.client, makeCallReq, 3) resp, err = DoWithFibonacciRetry(g.client, makeCallReq, 3)
} }
if err != nil { if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "space is paused") {
return fmt.Errorf("upstream space is paused: %w", err)
}
if fg, ok := extractFailedGeneration(err.Error()); ok { if fg, ok := extractFailedGeneration(err.Error()); ok {
if tcs, _, has := DetectToolCalls(fg); has && len(tcs) > 0 { if tcs, _, has := DetectToolCalls(fg); has && len(tcs) > 0 {
if !req.Stream { if !req.Stream {
@@ -5261,6 +5652,9 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
} }
resp, err = DoWithFibonacciRetry(g.client, makeAltReq, 3) resp, err = DoWithFibonacciRetry(g.client, makeAltReq, 3)
if err != nil { if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "space is paused") {
return fmt.Errorf("upstream space is paused: %w", err)
}
if fg, ok := extractFailedGeneration(err.Error()); ok { if fg, ok := extractFailedGeneration(err.Error()); ok {
if tcs, _, has := DetectToolCalls(fg); has && len(tcs) > 0 { if tcs, _, has := DetectToolCalls(fg); has && len(tcs) > 0 {
if !req.Stream { if !req.Stream {
@@ -5310,6 +5704,21 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
} }
streamResp, err := DoWithFibonacciRetry(g.client, makeStreamReq, 5) streamResp, err := DoWithFibonacciRetry(g.client, makeStreamReq, 5)
if err != nil && isV2 {
altStreamURL := fmt.Sprintf("%s%s/%s/%s/%s", disc.SpaceURL, disc.APIPrefix, callPath, disc.CleanEndpoint, joinRes.EventID)
if altStreamURL != streamURL {
makeAltStreamReq := func() (*http.Request, error) {
r, err := http.NewRequest("GET", altStreamURL, nil)
if err != nil {
return nil, err
}
r.Header.Set("Accept", "text/event-stream")
r.Header.Set("User-Agent", effUA)
return r, nil
}
streamResp, err = DoWithFibonacciRetry(g.client, makeAltStreamReq, 3)
}
}
if err != nil { if err != nil {
if fg, ok := extractFailedGeneration(err.Error()); ok { if fg, ok := extractFailedGeneration(err.Error()); ok {
if tcs, _, has := DetectToolCalls(fg); has && len(tcs) > 0 { if tcs, _, has := DetectToolCalls(fg); has && len(tcs) > 0 {
@@ -5430,7 +5839,7 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
var prevReasoning string var prevReasoning string
lastToolCallArgs := make(map[string]string) lastToolCallArgs := make(map[string]string)
emittedToolCallIDs := make(map[string]bool) emittedToolCallIDs := make(map[string]bool)
nativeReasoningSeen := disc.IsHunyuan3 || disc.ThinkLevelIndex >= 0 || disc.PreservedThinkingIndex >= 0 nativeReasoningSeen := disc.IsHunyuan3 || disc.PreservedThinkingIndex >= 0
nativeToolCallsSeen := false nativeToolCallsSeen := false
currentEvent := "" currentEvent := ""
var streamErr error var streamErr error
@@ -5481,7 +5890,12 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
if isNativeToolCalling { if isNativeToolCalling {
// 1. Native reasoning // 1. Native reasoning
if len(frame.Reasoning) > len(prevReasoning) { if len(frame.Reasoning) > len(prevReasoning) {
deltaReasoning := frame.Reasoning[len(prevReasoning):] var deltaReasoning string
if strings.HasPrefix(frame.Reasoning, prevReasoning) {
deltaReasoning = frame.Reasoning[len(prevReasoning):]
} else {
deltaReasoning = frame.Reasoning
}
prevReasoning = frame.Reasoning prevReasoning = frame.Reasoning
streamer.Reasoning(deltaReasoning) streamer.Reasoning(deltaReasoning)
} else if frame.IsDelta && frame.Reasoning != "" { } else if frame.IsDelta && frame.Reasoning != "" {
@@ -5522,7 +5936,12 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
// 3. Native content // 3. Native content
if len(frame.Content) > len(prevContent) { if len(frame.Content) > len(prevContent) {
cDelta := frame.Content[len(prevContent):] var cDelta string
if strings.HasPrefix(frame.Content, prevContent) {
cDelta = frame.Content[len(prevContent):]
} else {
cDelta = frame.Content
}
prevContent = frame.Content prevContent = frame.Content
streamer.Content(cDelta) streamer.Content(cDelta)
} else if frame.IsDelta && frame.Content != "" { } else if frame.IsDelta && frame.Content != "" {
@@ -5537,8 +5956,10 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
deltaReasoning = frame.Reasoning deltaReasoning = frame.Reasoning
prevReasoning += deltaReasoning prevReasoning += deltaReasoning
} else { } else {
if len(frame.Reasoning) > len(prevReasoning) { if len(frame.Reasoning) > len(prevReasoning) && strings.HasPrefix(frame.Reasoning, prevReasoning) {
deltaReasoning = frame.Reasoning[len(prevReasoning):] deltaReasoning = frame.Reasoning[len(prevReasoning):]
} else if !strings.HasPrefix(frame.Reasoning, prevReasoning) {
deltaReasoning = frame.Reasoning
} }
prevReasoning = frame.Reasoning prevReasoning = frame.Reasoning
} }
@@ -5585,8 +6006,10 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
delta = currentText delta = currentText
prevContent += delta prevContent += delta
} else { } else {
if len(currentText) > len(prevContent) { if len(currentText) > len(prevContent) && strings.HasPrefix(currentText, prevContent) {
delta = currentText[len(prevContent):] delta = currentText[len(prevContent):]
} else if !strings.HasPrefix(currentText, prevContent) {
delta = currentText
} }
prevContent = currentText prevContent = currentText
} }
+676
View File
@@ -4058,4 +4058,680 @@ func TestQueueToolCallingStreamingParity(t *testing.T) {
} }
} }
func TestServerFlavorMultimodalDictMessageAndHistory(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/config" {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"version": "6.20.0",
"api_prefix": "/gradio_api",
"mode": "server",
"components": []map[string]interface{}{
{
"id": 1,
"type": "api",
"props": map[string]interface{}{
"label": "1st",
"_api_info": map[string]interface{}{"type": "object"},
},
},
{
"id": 2,
"type": "api",
"props": map[string]interface{}{
"label": "2nd",
"_api_info": map[string]interface{}{"type": "array"},
},
},
},
"dependencies": []map[string]interface{}{
{
"id": 0,
"api_name": "chat",
"inputs": []int{1, 2},
"outputs": []int{1},
"types": map[string]interface{}{"generator": true},
},
},
})
return
}
if r.URL.Path == "/info" || r.URL.Path == "/gradio_api/info" {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"named_endpoints": map[string]interface{}{
"/chat": map[string]interface{}{
"parameters": []map[string]interface{}{
{
"parameter_name": "message",
"component": "Api",
"type": map[string]interface{}{"type": "object"},
"python_type": map[string]interface{}{"type": "dict(str, Any)"},
},
{
"parameter_name": "history",
"component": "Api",
"type": map[string]interface{}{"type": "array"},
"python_type": map[string]interface{}{"type": "list[Any]"},
},
},
},
},
})
return
}
http.NotFound(w, r)
}))
defer ts.Close()
disc, err := InspectSpace(http.DefaultClient, ts.URL, "test-agent")
if err != nil {
t.Fatalf("InspectSpace failed: %v", err)
}
if disc.Flavor != "server" {
t.Errorf("expected Flavor 'server', got %q", disc.Flavor)
}
if !disc.MessageIsMultimodal {
t.Errorf("expected MessageIsMultimodal=true for dict message, got false")
}
if disc.HistoryFormat != "messages" {
t.Errorf("expected HistoryFormat='messages' for server flavor, got %q", disc.HistoryFormat)
}
gw := &GradioGateway{}
req := ChatCompletionRequest{
Model: "test-model",
Messages: []ChatMessage{
{Role: "user", Content: "Hello world"},
},
}
payload, err := gw.BuildGradioPayload(disc, req)
if err != nil {
t.Fatalf("BuildGradioPayload failed: %v", err)
}
if len(payload) != 2 {
t.Fatalf("expected 2 payload slots, got %d", len(payload))
}
// Slot 0: Multimodal dictionary with "text" and "files"
msgDict, ok := payload[0].(map[string]interface{})
if !ok {
t.Fatalf("expected slot 0 to be map[string]interface{}, got %T", payload[0])
}
if msgDict["text"] != "Hello world" {
t.Errorf("expected text='Hello world', got %v", msgDict["text"])
}
if files, ok := msgDict["files"].([]interface{}); !ok || len(files) != 0 {
t.Errorf("expected files to be empty slice, got %v", msgDict["files"])
}
// Slot 1: History must be empty slice, never nil
histArr, ok := payload[1].([]map[string]interface{})
if !ok {
t.Fatalf("expected slot 1 to be []map[string]interface{}, got %T", payload[1])
}
if histArr == nil {
t.Errorf("expected history to be non-nil slice, got nil")
}
if len(histArr) != 0 {
t.Errorf("expected 0 history items for single turn, got %d", len(histArr))
}
}
func TestGradio6ChatInterfaceInspectionAndPayload(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/config" {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"version": "6.28.0",
"api_prefix": "/gradio_api",
"mode": "blocks",
"components": []map[string]interface{}{
{"id": 12, "type": "textbox", "props": map[string]interface{}{"label": "message"}},
{"id": 24, "type": "state", "props": map[string]interface{}{"label": "state"}},
{"id": 4, "type": "image", "props": map[string]interface{}{"label": "Image (optional)"}},
{"id": 5, "type": "checkbox", "props": map[string]interface{}{"label": "Web search", "value": true}},
{"id": 6, "type": "checkbox", "props": map[string]interface{}{"label": "Thinking mode", "value": false}},
{"id": 7, "type": "slider", "props": map[string]interface{}{"label": "Temperature", "value": 0.7}},
{"id": 8, "type": "slider", "props": map[string]interface{}{"label": "Top-p", "value": 0.9}},
{"id": 9, "type": "slider", "props": map[string]interface{}{"label": "Max tokens", "value": 8192}},
},
"dependencies": []map[string]interface{}{
{
"id": 0,
"api_name": "chat",
"inputs": []int{12, 24, 4, 5, 6, 7, 8, 9},
"outputs": []int{12},
"types": map[string]interface{}{"generator": true},
},
},
})
return
}
if r.URL.Path == "/info" || r.URL.Path == "/gradio_api/info" {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"named_endpoints": map[string]interface{}{
"/chat": map[string]interface{}{
"parameters": []map[string]interface{}{
{"parameter_name": "message", "component": "Textbox", "type": map[string]interface{}{"type": "string"}},
{"parameter_name": "image", "component": "Image", "type": map[string]interface{}{"type": "string"}},
{"parameter_name": "web_search", "component": "Checkbox", "type": map[string]interface{}{"type": "boolean"}, "default": true},
{"parameter_name": "enable_thinking", "component": "Checkbox", "type": map[string]interface{}{"type": "boolean"}, "default": false},
{"parameter_name": "temperature", "component": "Slider", "type": map[string]interface{}{"type": "number"}, "default": 0.7},
{"parameter_name": "top_p", "component": "Slider", "type": map[string]interface{}{"type": "number"}, "default": 0.9},
{"parameter_name": "max_tokens", "component": "Slider", "type": map[string]interface{}{"type": "integer"}, "default": 8192},
},
},
},
})
return
}
http.NotFound(w, r)
}))
defer ts.Close()
disc, err := InspectSpace(http.DefaultClient, ts.URL, "test-agent")
if err != nil {
t.Fatalf("InspectSpace failed: %v", err)
}
if disc.MessageIndex != 0 {
t.Errorf("expected MessageIndex=0, got %d", disc.MessageIndex)
}
if disc.ImageIndex != 2 {
t.Errorf("expected ImageIndex=2, got %d", disc.ImageIndex)
}
if disc.WebSearchIndex != 3 {
t.Errorf("expected WebSearchIndex=3, got %d", disc.WebSearchIndex)
}
if disc.ThinkLevelIndex != 4 {
t.Errorf("expected ThinkLevelIndex=4, got %d", disc.ThinkLevelIndex)
}
if disc.TempIndex != 5 {
t.Errorf("expected TempIndex=5, got %d", disc.TempIndex)
}
if disc.TopPIndex != 6 {
t.Errorf("expected TopPIndex=6, got %d", disc.TopPIndex)
}
if disc.MaxTokensIndex != 7 {
t.Errorf("expected MaxTokensIndex=7, got %d", disc.MaxTokensIndex)
}
if disc.IsHunyuan3 {
t.Errorf("expected IsHunyuan3=false for generic Gradio space, got true")
}
gw := &GradioGateway{}
webSearchFalse := false
tempVal := 0.5
topPVal := 0.8
req := ChatCompletionRequest{
Model: "qwen",
Messages: []ChatMessage{
{
Role: "user",
Content: []interface{}{
map[string]interface{}{"type": "text", "text": "Describe this photo"},
map[string]interface{}{"type": "image_url", "image_url": map[string]interface{}{"url": "https://example.com/cat.png"}},
},
},
},
ReasoningEffort: "high",
WebSearch: &webSearchFalse,
Temperature: &tempVal,
TopP: &topPVal,
MaxTokens: 2048,
}
payload, err := gw.BuildGradioPayload(disc, req)
if err != nil {
t.Fatalf("BuildGradioPayload failed: %v", err)
}
if len(payload) != 8 {
t.Fatalf("expected 8 payload items, got %d", len(payload))
}
// Slot 0: prompt text
if msg, ok := payload[0].(string); !ok || !strings.Contains(msg, "Describe this photo") {
t.Errorf("expected slot 0 to contain 'Describe this photo', got %v", payload[0])
}
// Slot 2: primary image FileData
imgMap, ok := payload[2].(map[string]interface{})
if !ok {
t.Fatalf("expected slot 2 to be FileData map, got %T", payload[2])
}
if imgMap["url"] != "https://example.com/cat.png" {
t.Errorf("expected image url 'https://example.com/cat.png', got %v", imgMap["url"])
}
// Slot 3: web search disabled
if ws, ok := payload[3].(bool); !ok || ws != false {
t.Errorf("expected slot 3 (web_search) to be false, got %v", payload[3])
}
// Slot 4: thinking mode checkbox enabled for "high" effort
if think, ok := payload[4].(bool); !ok || think != true {
t.Errorf("expected slot 4 (enable_thinking) to be true, got %v", payload[4])
}
// Slot 5: temperature
if temp, ok := payload[5].(float64); !ok || temp != 0.5 {
t.Errorf("expected slot 5 (temperature) to be 0.5, got %v", payload[5])
}
// Slot 6: top_p
if topP, ok := payload[6].(float64); !ok || topP != 0.8 {
t.Errorf("expected slot 6 (top_p) to be 0.8, got %v", payload[6])
}
// Slot 7: max_tokens
if maxTokens, ok := payload[7].(int); !ok || maxTokens != 2048 {
t.Errorf("expected slot 7 (max_tokens) to be 2048, got %v", payload[7])
}
}
func TestExtractThinkingAndDetailsHtmlFormat(t *testing.T) {
// 1. Closed details tag with thinking
raw1 := `<details open><summary>Thinking</summary>
I need to analyze this prompt step by step.
</details>
Here is the answer.`
rem1, think1 := ExtractThinking(raw1)
if think1 != "I need to analyze this prompt step by step." {
t.Errorf("unexpected reasoning: %q", think1)
}
if rem1 != "Here is the answer." {
t.Errorf("unexpected clean content: %q", rem1)
}
// 2. Preceding web search results details tag preserved
raw2 := `<details><summary>Web results</summary>
1. https://example.com
Snippet
</details>
<details open><summary>Thinking</summary>
Evaluating results...
</details>
Answer based on web results.`
rem2, think2 := ExtractThinking(raw2)
if think2 != "Evaluating results..." {
t.Errorf("unexpected reasoning: %q", think2)
}
if !strings.Contains(rem2, "<details><summary>Web results</summary>") {
t.Errorf("expected web results to remain in rem, got: %q", rem2)
}
if !strings.Contains(rem2, "Answer based on web results.") {
t.Errorf("expected final answer to remain in rem, got: %q", rem2)
}
if strings.Contains(rem2, "Thinking") {
t.Errorf("thinking tag should have been stripped from rem, got: %q", rem2)
}
// 3. Open details tag during streaming
raw3 := `<details open><summary>Thinking</summary>
Streaming thought step 1...`
rem3, think3 := ExtractThinking(raw3)
if think3 != "Streaming thought step 1..." {
t.Errorf("unexpected reasoning: %q", think3)
}
if rem3 != "" {
t.Errorf("expected empty content while thinking is open, got %q", rem3)
}
// 4. ParseGradioStreamOutput separation test
streamJSON := `["<details open><summary>Thinking</summary>\n\nReasoning...\n</details>\n\nClean answer."]`
frame := ParseGradioStreamOutput(streamJSON)
if !frame.OK {
t.Fatalf("expected frame.OK to be true")
}
if frame.Reasoning != "Reasoning..." {
t.Errorf("expected frame.Reasoning='Reasoning...', got %q", frame.Reasoning)
}
if frame.Content != "Clean answer." {
t.Errorf("expected frame.Content='Clean answer.', got %q", frame.Content)
}
}
func TestPausedSpaceDetectionAndImmediateExit(t *testing.T) {
callCount := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("The space is paused"))
}))
defer ts.Close()
makeReq := func() (*http.Request, error) {
return http.NewRequest("POST", ts.URL, bytes.NewBufferString("{}"))
}
_, err := DoWithFibonacciRetry(http.DefaultClient, makeReq, 5)
if err == nil {
t.Fatalf("expected error from paused space, got nil")
}
if !strings.Contains(strings.ToLower(err.Error()), "space is paused") {
t.Errorf("expected error to mention 'space is paused', got %v", err)
}
if callCount != 1 {
t.Errorf("expected exactly 1 attempt on paused space (no retries), got %d attempts", callCount)
}
}
func TestOmniForgeSpaceInspectionAndPayload(t *testing.T) {
cfgJSON := `{
"version": "6.26.0",
"mode": "blocks",
"title": "OmniForge AI",
"api_prefix": "/gradio_api",
"components": [
{
"id": 7,
"type": "textbox",
"props": {"label": "Ask OmniForge", "value": ""}
},
{
"id": 6,
"type": "chatbot",
"props": {"label": "OmniForge Conversation", "type": "messages"}
},
{
"id": 13,
"type": "dropdown",
"props": {
"label": "Specialty mode",
"value": "General Engineering",
"choices": ["General Engineering", "Diagnostics and Repair"]
}
},
{
"id": 14,
"type": "dropdown",
"props": {
"label": "Hugging Face model",
"value": "deepseek-ai/DeepSeek-V4.1-Flash",
"choices": [
"deepseek-ai/DeepSeek-V4.1-Flash",
"openai/gpt-oss-120b",
"Qwen/Qwen2.5-7B-Instruct",
"meta-llama/Llama-3.1-8B-Instruct"
]
}
},
{
"id": 1,
"type": "state",
"props": {"name": "state"},
"skip_api": true
},
{
"id": 2,
"type": "browserstate",
"props": {"default_value": [], "name": "browserstate"},
"skip_api": false
}
],
"dependencies": [
{
"id": 0,
"targets": [[10, "click"]],
"inputs": [7, 6, 13, 14, 1, 2],
"outputs": [6, 7],
"api_name": "chat",
"connection": "sse",
"protocol": "call_v2"
}
]
}`
infoJSON := `{
"named_endpoints": {
"/chat": {
"parameters": [
{"label": "message", "parameter_name": "message", "component": "Textbox"},
{"label": "history", "parameter_name": "history", "component": "Chatbot"},
{"label": "specialty_value", "parameter_name": "specialty_value", "component": "Dropdown"},
{"label": "model_value", "parameter_name": "model_value", "component": "Dropdown"},
{"label": "memory_value", "parameter_name": "memory_value", "component": "BrowserState", "parameter_default": []}
]
}
}
}`
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/config") {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(cfgJSON))
return
}
if strings.HasSuffix(r.URL.Path, "/info") {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(infoJSON))
return
}
http.NotFound(w, r)
}))
defer ts.Close()
disc, err := InspectSpace(http.DefaultClient, ts.URL, "gr2gw-test")
if err != nil {
t.Fatalf("InspectSpace failed: %v", err)
}
if !disc.IsOmniForge {
t.Errorf("expected disc.IsOmniForge=true, got false")
}
if disc.TotalInputs != 6 {
t.Errorf("expected TotalInputs=6, got %d", disc.TotalInputs)
}
if disc.ModelIndex != 3 {
t.Errorf("expected ModelIndex=3, got %d", disc.ModelIndex)
}
if disc.HistoryIndex != 1 {
t.Errorf("expected HistoryIndex=1, got %d", disc.HistoryIndex)
}
if disc.MessageIndex != 0 {
t.Errorf("expected MessageIndex=0, got %d", disc.MessageIndex)
}
if disc.PrimaryModel != "deepseek-ai/DeepSeek-V4.1-Flash" {
t.Errorf("expected PrimaryModel='deepseek-ai/DeepSeek-V4.1-Flash', got %q", disc.PrimaryModel)
}
if !disc.MatchesModel("gpt-oss-120b") {
t.Errorf("expected MatchesModel('gpt-oss-120b') to be true")
}
if !disc.MatchesModel("openai/gpt-oss-120b") {
t.Errorf("expected MatchesModel('openai/gpt-oss-120b') to be true")
}
gw := &GradioGateway{}
req := ChatCompletionRequest{
Model: "gpt-oss-120b",
Messages: []ChatMessage{
{Role: "user", Content: "Hello"},
{Role: "assistant", Content: "Hi there!"},
{Role: "user", Content: "Tell me a joke"},
},
}
payload, err := gw.BuildGradioPayload(disc, req)
if err != nil {
t.Fatalf("BuildGradioPayload failed: %v", err)
}
if len(payload) != 6 {
t.Fatalf("expected payload len 6, got %d", len(payload))
}
// Model selection mapped to slot 3
if payload[3] != "openai/gpt-oss-120b" {
t.Errorf("expected payload[3]='openai/gpt-oss-120b', got %v", payload[3])
}
// History slot mapped to slot 1: should be empty slice for OmniForge
if hist, ok := payload[1].([]map[string]interface{}); !ok || len(hist) != 0 {
t.Errorf("expected payload[1] to be empty slice, got %v", payload[1])
}
// Multi-turn history composed into promptMessageText in slot 0
promptStr, ok := payload[0].(string)
if !ok {
t.Fatalf("expected payload[0] to be string, got %T", payload[0])
}
if !strings.Contains(promptStr, "# Conversation History") ||
!strings.Contains(promptStr, "User: Hello") ||
!strings.Contains(promptStr, "Assistant: Hi there!") ||
!strings.Contains(promptStr, "# Current Request\nUser: Tell me a joke") {
t.Errorf("promptStr missing multi-turn history: %s", promptStr)
}
// Slot 5 is browserstate (memory_value)
if mem, ok := payload[5].([]interface{}); !ok && payload[5] == nil {
t.Errorf("expected payload[5] to be non-nil slice, got %v", payload[5])
} else if ok && len(mem) != 0 {
t.Errorf("expected payload[5] to be empty slice, got %v", mem)
}
}
func TestOmniForgeMockServerCompletion(t *testing.T) {
var receivedPayload map[string]interface{}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/gradio_api/call/v2/chat" && r.Method == "POST" {
json.NewDecoder(r.Body).Decode(&receivedPayload)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"event_id": "test-event-omni"}`))
return
}
if (r.URL.Path == "/gradio_api/call/chat/test-event-omni" || r.URL.Path == "/gradio_api/call/v2/chat/test-event-omni") && r.Method == "GET" {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
flusher, _ := w.(http.Flusher)
w.Write([]byte("event: generating\ndata: [[{\"role\": \"user\", \"metadata\": null, \"content\": [{\"text\": \"hello\", \"type\": \"text\"}], \"options\": null}, {\"role\": \"assistant\", \"metadata\": null, \"content\": [{\"text\": \"<think>Thinking about joke</think>Why did the chicken cross the road?\", \"type\": \"text\"}], \"options\": null}], \"\"]\n\n"))
if flusher != nil {
flusher.Flush()
}
w.Write([]byte("event: complete\ndata: null\n\n"))
if flusher != nil {
flusher.Flush()
}
return
}
http.NotFound(w, r)
}))
defer ts.Close()
disc := &SpaceDiscovery{
SpaceURL: ts.URL,
APIPrefix: "/gradio_api",
CleanEndpoint: "chat",
Protocol: "call_v2",
IsOmniForge: true,
TotalInputs: 6,
MessageIndex: 0,
HistoryIndex: 1,
ModelIndex: 3,
PrimaryModel: "openai/gpt-oss-120b",
LastDiscovered: time.Now(),
ParamMappings: []SpaceParamMapping{
{InputIndex: 0, ParamName: "message", ComponentType: "textbox"},
{InputIndex: 1, ParamName: "history", ComponentType: "chatbot"},
{InputIndex: 2, ParamName: "specialty_value", ComponentType: "dropdown", DefaultValue: "General Engineering"},
{InputIndex: 3, ParamName: "model_value", ComponentType: "dropdown", Choices: []string{"openai/gpt-oss-120b"}, DefaultValue: "openai/gpt-oss-120b"},
{InputIndex: 4, ParamName: "state", ParamType: "state", ComponentType: "state"},
{InputIndex: 5, ParamName: "memory_value", ComponentType: "browserstate", DefaultValue: []interface{}{}},
},
}
gw := NewGradioGateway(ts.URL, "", 10*time.Second)
gw.discoveries[ts.URL] = disc
// 1. Non-streaming test
req := ChatCompletionRequest{
Model: "openai/gpt-oss-120b",
Messages: []ChatMessage{
{Role: "user", Content: "Tell me a joke"},
},
}
rec := httptest.NewRecorder()
httpReq := httptest.NewRequest("POST", "/v1/chat/completions", nil)
err := gw.ExecuteChatCompletion(rec, httpReq, req)
if err != nil {
t.Fatalf("ExecuteChatCompletion failed: %v", err)
}
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d: %s", rec.Code, rec.Body.String())
}
// Check received payload by mock server
if receivedPayload["model_value"] != "openai/gpt-oss-120b" {
t.Errorf("expected model_value='openai/gpt-oss-120b', got %v", receivedPayload["model_value"])
}
if receivedPayload["memory_value"] == nil {
t.Errorf("expected memory_value to be non-nil in v2 payload")
}
if _, hasState := receivedPayload["state"]; hasState {
t.Errorf("expected unexposed state component to NOT be in v2 payload")
}
var resp ChatCompletionResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if len(resp.Choices) == 0 {
t.Fatalf("expected choices in response")
}
choice := resp.Choices[0]
if choice.Message.ReasoningContent != "Thinking about joke" {
t.Errorf("expected reasoning='Thinking about joke', got %q", choice.Message.ReasoningContent)
}
if choice.Message.Content != "Why did the chicken cross the road?" {
t.Errorf("expected content='Why did the chicken cross the road?', got %q", choice.Message.Content)
}
// 2. Streaming test
reqStream := ChatCompletionRequest{
Model: "openai/gpt-oss-120b",
Stream: true,
Messages: []ChatMessage{
{Role: "user", Content: "Tell me a joke"},
},
}
recStream := httptest.NewRecorder()
errStream := gw.ExecuteChatCompletion(recStream, httpReq, reqStream)
if errStream != nil {
t.Fatalf("streaming completion failed: %v", errStream)
}
bodyStr := recStream.Body.String()
if !strings.Contains(bodyStr, "Thinking about joke") {
t.Errorf("streaming response missing reasoning token: %s", bodyStr)
}
if !strings.Contains(bodyStr, "Why did the chicken cross the road?") {
t.Errorf("streaming response missing content token: %s", bodyStr)
}
}