Support Gradio 6 call_v2 protocol with named parameters and fallback
This commit is contained in:
@@ -1179,6 +1179,10 @@ func extractFailedGeneration(raw string) (string, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func extractGradioErrorMessage(dataStr string) string {
|
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{}
|
var errObj map[string]interface{}
|
||||||
if err := json.Unmarshal([]byte(dataStr), &errObj); err == nil {
|
if err := json.Unmarshal([]byte(dataStr), &errObj); err == nil {
|
||||||
if e, ok := errObj["error"].(string); ok && e != "" {
|
if e, ok := errObj["error"].(string); ok && e != "" {
|
||||||
@@ -1545,6 +1549,7 @@ type GradioEndpointInfo struct {
|
|||||||
Returns []GradioParamInfo `json:"returns"`
|
Returns []GradioParamInfo `json:"returns"`
|
||||||
APIVisibility string `json:"api_visibility"`
|
APIVisibility string `json:"api_visibility"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
CodeSnippets map[string]interface{} `json:"code_snippets,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GradioAPIInfoResponse struct {
|
type GradioAPIInfoResponse struct {
|
||||||
@@ -1599,6 +1604,7 @@ type SpaceParamMapping struct {
|
|||||||
ComponentID int `json:"component_id"`
|
ComponentID int `json:"component_id"`
|
||||||
ComponentType string `json:"component_type,omitempty"`
|
ComponentType string `json:"component_type,omitempty"`
|
||||||
Label string `json:"label,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"
|
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"`
|
DefaultValue interface{} `json:"default_value,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -2230,11 +2236,29 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Select protocol
|
// 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.") {
|
if strings.HasPrefix(discovery.GradioVersion, "3.") {
|
||||||
discovery.Protocol = "predict"
|
discovery.Protocol = "predict"
|
||||||
discovery.APIPrefix = ""
|
discovery.APIPrefix = ""
|
||||||
discovery.Endpoint = "/run/predict"
|
discovery.Endpoint = "/run/predict"
|
||||||
discovery.CleanEndpoint = "run/predict"
|
discovery.CleanEndpoint = "run/predict"
|
||||||
|
} else if isCallV2 {
|
||||||
|
discovery.Protocol = "call_v2"
|
||||||
|
if bestEndpoint != "" {
|
||||||
|
discovery.Endpoint = bestEndpoint
|
||||||
|
discovery.CleanEndpoint = strings.TrimPrefix(bestEndpoint, "/")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
discovery.Protocol = "call"
|
discovery.Protocol = "call"
|
||||||
if bestEndpoint != "" {
|
if bestEndpoint != "" {
|
||||||
@@ -2268,6 +2292,7 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
|
|||||||
mapping.DefaultValue = p.ParameterDefault
|
mapping.DefaultValue = p.ParameterDefault
|
||||||
}
|
}
|
||||||
mapping.Label = p.ParameterName
|
mapping.Label = p.ParameterName
|
||||||
|
mapping.ParamName = p.ParameterName
|
||||||
if p.Component != "" {
|
if p.Component != "" {
|
||||||
mapping.ComponentType = p.Component
|
mapping.ComponentType = p.Component
|
||||||
}
|
}
|
||||||
@@ -2440,6 +2465,7 @@ func InspectSpace(client *http.Client, rawURL, userAgent string) (*SpaceDiscover
|
|||||||
InputIndex: idx,
|
InputIndex: idx,
|
||||||
ComponentType: p.Component,
|
ComponentType: p.Component,
|
||||||
Label: p.Label,
|
Label: p.Label,
|
||||||
|
ParamName: p.ParameterName,
|
||||||
ParamType: "other",
|
ParamType: "other",
|
||||||
DefaultValue: p.ParameterDefault,
|
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)
|
return g.executePredictCompletion(w, r, disc, gradioData, req, completionID, createdTime, modelName, effUA)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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{}{
|
payloadMap := map[string]interface{}{
|
||||||
"data": gradioData,
|
"data": gradioData,
|
||||||
"session_hash": GenerateUUID(),
|
"session_hash": GenerateUUID(),
|
||||||
}
|
}
|
||||||
jsonPayload, err := json.Marshal(payloadMap)
|
return json.Marshal(payloadMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
callPath := "call"
|
||||||
|
if isV2 {
|
||||||
|
callPath = "call/v2"
|
||||||
|
}
|
||||||
|
jsonPayload, err := buildPayload(isV2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to encode request: %w", err)
|
return fmt.Errorf("failed to encode request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Submit to /call/{endpoint}
|
// 1. Submit to /call/{endpoint} or /call/v2/{endpoint}
|
||||||
callURL := fmt.Sprintf("%s%s/call/%s", disc.SpaceURL, disc.APIPrefix, disc.CleanEndpoint)
|
callURL := fmt.Sprintf("%s%s/%s/%s", disc.SpaceURL, disc.APIPrefix, callPath, disc.CleanEndpoint)
|
||||||
makeCallReq := func() (*http.Request, error) {
|
makeCallReq := func() (*http.Request, error) {
|
||||||
r, err := http.NewRequest("POST", callURL, bytes.NewBuffer(jsonPayload))
|
r, err := http.NewRequest("POST", callURL, bytes.NewBuffer(jsonPayload))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -3485,6 +3538,14 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := DoWithFibonacciRetry(g.client, makeCallReq, 5)
|
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 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 {
|
||||||
@@ -3508,7 +3569,7 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If call failed, try without APIPrefix
|
// 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) {
|
makeAltReq := func() (*http.Request, error) {
|
||||||
r, err := http.NewRequest("POST", altCallURL, bytes.NewBuffer(jsonPayload))
|
r, err := http.NewRequest("POST", altCallURL, bytes.NewBuffer(jsonPayload))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+126
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user