diff --git a/gr2gw.go b/gr2gw.go index 1b6a1d7..ecc240a 100644 --- a/gr2gw.go +++ b/gr2gw.go @@ -1179,6 +1179,10 @@ func extractFailedGeneration(raw string) (string, bool) { } func extractGradioErrorMessage(dataStr string) string { + var strVal string + if err := json.Unmarshal([]byte(dataStr), &strVal); err == nil && strVal != "" { + return strVal + } var errObj map[string]interface{} if err := json.Unmarshal([]byte(dataStr), &errObj); err == nil { if e, ok := errObj["error"].(string); ok && e != "" { @@ -1541,10 +1545,11 @@ type GradioParamInfo struct { } type GradioEndpointInfo struct { - Parameters []GradioParamInfo `json:"parameters"` - Returns []GradioParamInfo `json:"returns"` - APIVisibility string `json:"api_visibility"` - Description string `json:"description"` + Parameters []GradioParamInfo `json:"parameters"` + Returns []GradioParamInfo `json:"returns"` + APIVisibility string `json:"api_visibility"` + Description string `json:"description"` + CodeSnippets map[string]interface{} `json:"code_snippets,omitempty"` } type GradioAPIInfoResponse struct { @@ -1599,6 +1604,7 @@ type SpaceParamMapping struct { ComponentID int `json:"component_id"` ComponentType string `json:"component_type,omitempty"` Label string `json:"label,omitempty"` + ParamName string `json:"param_name,omitempty"` ParamType string `json:"param_type"` // "message", "history", "system_prompt", "temperature", "max_tokens", "top_p", "think_level", "tools", "stream", "state", "other" DefaultValue interface{} `json:"default_value,omitempty"` } @@ -2230,11 +2236,29 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover } // Select protocol + isCallV2 := false + if strings.HasPrefix(discovery.GradioVersion, "6.") { + isCallV2 = true + } + if bestEndpointInfo != nil && bestEndpointInfo.CodeSnippets != nil { + if bashSnippet, ok := bestEndpointInfo.CodeSnippets["bash"].(string); ok { + if strings.Contains(bashSnippet, "/call/v2/") { + isCallV2 = true + } + } + } + if strings.HasPrefix(discovery.GradioVersion, "3.") { discovery.Protocol = "predict" discovery.APIPrefix = "" discovery.Endpoint = "/run/predict" discovery.CleanEndpoint = "run/predict" + } else if isCallV2 { + discovery.Protocol = "call_v2" + if bestEndpoint != "" { + discovery.Endpoint = bestEndpoint + discovery.CleanEndpoint = strings.TrimPrefix(bestEndpoint, "/") + } } else { discovery.Protocol = "call" if bestEndpoint != "" { @@ -2268,6 +2292,7 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover mapping.DefaultValue = p.ParameterDefault } mapping.Label = p.ParameterName + mapping.ParamName = p.ParameterName if p.Component != "" { mapping.ComponentType = p.Component } @@ -2440,6 +2465,7 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover InputIndex: idx, ComponentType: p.Component, Label: p.Label, + ParamName: p.ParameterName, ParamType: "other", DefaultValue: p.ParameterDefault, } @@ -3463,17 +3489,44 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req return g.executePredictCompletion(w, r, disc, gradioData, req, completionID, createdTime, modelName, effUA) } - payloadMap := map[string]interface{}{ - "data": gradioData, - "session_hash": GenerateUUID(), + isV2 := disc.Protocol == "call_v2" + buildPayload := func(v2 bool) ([]byte, error) { + if v2 { + v2Payload := make(map[string]interface{}) + for idx, val := range gradioData { + pName := "" + if idx < len(disc.ParamMappings) { + if disc.ParamMappings[idx].ParamName != "" { + pName = disc.ParamMappings[idx].ParamName + } else if disc.ParamMappings[idx].Label != "" { + pName = disc.ParamMappings[idx].Label + } + } + if pName == "" { + pName = fmt.Sprintf("param_%d", idx) + } + v2Payload[pName] = val + } + return json.Marshal(v2Payload) + } + payloadMap := map[string]interface{}{ + "data": gradioData, + "session_hash": GenerateUUID(), + } + return json.Marshal(payloadMap) } - jsonPayload, err := json.Marshal(payloadMap) + + callPath := "call" + if isV2 { + callPath = "call/v2" + } + jsonPayload, err := buildPayload(isV2) if err != nil { return fmt.Errorf("failed to encode request: %w", err) } - // 1. Submit to /call/{endpoint} - callURL := fmt.Sprintf("%s%s/call/%s", disc.SpaceURL, disc.APIPrefix, disc.CleanEndpoint) + // 1. Submit to /call/{endpoint} or /call/v2/{endpoint} + callURL := fmt.Sprintf("%s%s/%s/%s", disc.SpaceURL, disc.APIPrefix, callPath, disc.CleanEndpoint) makeCallReq := func() (*http.Request, error) { r, err := http.NewRequest("POST", callURL, bytes.NewBuffer(jsonPayload)) if err != nil { @@ -3485,6 +3538,14 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req } resp, err := DoWithFibonacciRetry(g.client, makeCallReq, 5) + if err != nil && isV2 { + log.Printf("Gradio /call/v2 endpoint failed (%v), falling back to /call...", err) + isV2 = false + callPath = "call" + jsonPayload, _ = buildPayload(false) + callURL = fmt.Sprintf("%s%s/%s/%s", disc.SpaceURL, disc.APIPrefix, callPath, disc.CleanEndpoint) + resp, err = DoWithFibonacciRetry(g.client, makeCallReq, 3) + } if err != nil { if fg, ok := extractFailedGeneration(err.Error()); ok { if tcs, _, has := DetectToolCalls(fg); has && len(tcs) > 0 { @@ -3508,7 +3569,7 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req } } // If call failed, try without APIPrefix - altCallURL := fmt.Sprintf("%s/call/%s", disc.SpaceURL, disc.CleanEndpoint) + altCallURL := fmt.Sprintf("%s/%s/%s", disc.SpaceURL, callPath, disc.CleanEndpoint) makeAltReq := func() (*http.Request, error) { r, err := http.NewRequest("POST", altCallURL, bytes.NewBuffer(jsonPayload)) if err != nil { diff --git a/gr2gw_test.go b/gr2gw_test.go index 3b00804..c9dbd10 100644 --- a/gr2gw_test.go +++ b/gr2gw_test.go @@ -2196,6 +2196,132 @@ func TestExecuteCallCompletionSessionHashAndTrailingStateTrim(t *testing.T) { } } +// TestGradio6CallV2Protocol verifies that Gradio 6 spaces resolve protocol call_v2, +// submit named JSON parameters to /call/v2/{endpoint}, and stream from /call/{endpoint}/{event_id}. +func TestGradio6CallV2Protocol(t *testing.T) { + var receivedBody map[string]interface{} + var callEndpointReached string + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if 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{}{ + "/answer": map[string]interface{}{ + "parameters": []map[string]interface{}{ + { + "parameter_name": "prompt", + "component": "Textbox", + "label": "Prompt", + }, + }, + "code_snippets": map[string]interface{}{ + "bash": "curl -X POST http://localhost:7860/gradio_api/call/v2/answer -s -H \"Content-Type: application/json\" -d '{\"prompt\": \"Hello!!\"}' | awk -F'\"' '{ print $4}' | read EVENT_ID; curl -N http://localhost:7860/gradio_api/call/answer/$EVENT_ID", + }, + }, + }, + }) + return + } + + if r.URL.Path == "/config" { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "version": "6.13.0", + "components": []map[string]interface{}{ + {"id": 1, "type": "textbox", "props": map[string]interface{}{"label": "Prompt"}}, + }, + "dependencies": []map[string]interface{}{ + { + "id": 0, + "api_name": "answer", + "inputs": []int{1}, + "outputs": []int{1}, + "types": map[string]interface{}{"generator": true}, + }, + }, + }) + return + } + + if r.URL.Path == "/gradio_api/call/v2/answer" { + callEndpointReached = r.URL.Path + if err := json.NewDecoder(r.Body).Decode(&receivedBody); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "event_id": "evt-v2-777", + }) + return + } + + if r.URL.Path == "/gradio_api/call/answer/evt-v2-777" { + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.WriteHeader(http.StatusOK) + flusher, _ := w.(http.Flusher) + fmt.Fprintf(w, "event: complete\ndata: [\"Hello from Gradio 6 v2!\"]\n\n") + if flusher != nil { + flusher.Flush() + } + return + } + + http.NotFound(w, r) + })) + defer ts.Close() + + gw := NewGradioGateway(ts.URL, "", 10*time.Second) + + // 1. Verify inspection resolved call_v2 protocol + disc, err := InspectSpace(gw.client, ts.URL, DefaultUserAgent) + if err != nil { + t.Fatalf("InspectSpace failed: %v", err) + } + if disc.Protocol != "call_v2" { + t.Errorf("expected Protocol call_v2, got %s", disc.Protocol) + } + if disc.Endpoint != "/answer" { + t.Errorf("expected Endpoint /answer, got %s", disc.Endpoint) + } + + // 2. Execute chat completion + req := ChatCompletionRequest{ + Model: "default", + Messages: []ChatMessage{ + {Role: "user", Content: "Hello test"}, + }, + } + httpReq := httptest.NewRequest("POST", "/v1/chat/completions", nil) + rec := httptest.NewRecorder() + + 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()) + } + if callEndpointReached != "/gradio_api/call/v2/answer" { + t.Errorf("expected call to /gradio_api/call/v2/answer, got %s", callEndpointReached) + } + if promptVal, ok := receivedBody["prompt"].(string); !ok || promptVal != "Hello test" { + t.Errorf("expected named param 'prompt' with value 'Hello test', got %v", receivedBody) + } + + var resp ChatCompletionResponse + if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + if len(resp.Choices) == 0 || resp.Choices[0].Message.Content != "Hello from Gradio 6 v2!" { + t.Errorf("unexpected content: %v", resp.Choices[0].Message.Content) + } +} + +