fix(tools): recover tool calls from tool_use_failed errors and update default space to digital-twin

This commit is contained in:
Luxferre
2026-09-07 10:10:51 +03:00
parent 9eaa1dd25c
commit cfb26602b5
3 changed files with 422 additions and 26 deletions
+225 -4
View File
@@ -25,7 +25,7 @@ import (
)
var (
DefaultSpaceURL = "https://ghost2513-openai-gpt-oss-120b.hf.space"
DefaultSpaceURL = "https://lucasmarchettidelima-digital-twin.hf.space"
DefaultUserAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:153.0) Gecko/20100101 Firefox/153.0"
ConfiguredUserAgent string
)
@@ -388,6 +388,9 @@ func DoWithFibonacciRetry(client *http.Client, makeReq func() (*http.Request, er
respBody, _ := io.ReadAll(resp.Body)
resp.Body.Close()
lastErr = fmt.Errorf("HTTP status %d: %s", resp.StatusCode, string(respBody))
if _, ok := extractFailedGeneration(string(respBody)); ok {
break
}
} else {
lastErr = err
}
@@ -1002,6 +1005,141 @@ func WriteCompletionResponse(w http.ResponseWriter, completionID string, created
json.NewEncoder(w).Encode(resp)
}
func extractFailedGeneration(raw string) (string, bool) {
var obj map[string]interface{}
if err := json.Unmarshal([]byte(raw), &obj); err == nil {
if fg, ok := obj["failed_generation"].(string); ok && fg != "" {
return strings.TrimSpace(fg), true
}
if errVal, ok := obj["error"]; ok {
if errMap, ok := errVal.(map[string]interface{}); ok {
if fg, ok := errMap["failed_generation"].(string); ok && fg != "" {
return strings.TrimSpace(fg), true
}
} else if errStr, ok := errVal.(string); ok {
if fg, ok := extractFailedGeneration(errStr); ok {
return fg, true
}
}
}
}
keyIdx := strings.Index(raw, `"failed_generation"`)
if keyIdx == -1 {
keyIdx = strings.Index(raw, `'failed_generation'`)
}
if keyIdx == -1 {
keyIdx = strings.Index(raw, `failed_generation`)
}
if keyIdx == -1 {
return "", false
}
colonIdx := strings.Index(raw[keyIdx:], ":")
if colonIdx == -1 {
return "", false
}
valStart := keyIdx + colonIdx + 1
for valStart < len(raw) && (raw[valStart] == ' ' || raw[valStart] == '\t' || raw[valStart] == '\r' || raw[valStart] == '\n') {
valStart++
}
if valStart >= len(raw) {
return "", false
}
firstChar := raw[valStart]
var candidate string
if firstChar == '\'' || firstChar == '"' {
quoteChar := firstChar
var b strings.Builder
escaped := false
for i := valStart + 1; i < len(raw); i++ {
ch := raw[i]
if escaped {
switch ch {
case 'n':
b.WriteByte('\n')
case 'r':
b.WriteByte('\r')
case 't':
b.WriteByte('\t')
case '\\':
b.WriteByte('\\')
case '\'':
b.WriteByte('\'')
case '"':
b.WriteByte('"')
default:
b.WriteByte('\\')
b.WriteByte(ch)
}
escaped = false
} else if ch == '\\' {
escaped = true
} else if ch == quoteChar {
break
} else {
b.WriteByte(ch)
}
}
candidate = strings.TrimSpace(b.String())
} else if firstChar == '{' || firstChar == '[' {
openChar := firstChar
closeChar := byte('}')
if openChar == '[' {
closeChar = ']'
}
depth := 0
inStr := false
var strQuote byte
escaped := false
endIdx := -1
for i := valStart; i < len(raw); i++ {
ch := raw[i]
if inStr {
if escaped {
escaped = false
} else if ch == '\\' {
escaped = true
} else if ch == strQuote {
inStr = false
}
} else {
if ch == '"' || ch == '\'' {
inStr = true
strQuote = ch
} else if ch == openChar {
depth++
} else if ch == closeChar {
depth--
if depth == 0 {
endIdx = i + 1
break
}
}
}
}
if endIdx != -1 {
candidate = strings.TrimSpace(raw[valStart:endIdx])
}
}
if candidate != "" {
if strings.Contains(candidate, `\"`) {
candidate = strings.ReplaceAll(candidate, `\"`, `"`)
}
if strings.Contains(candidate, `\n`) {
candidate = strings.ReplaceAll(candidate, `\n`, "\n")
}
return candidate, true
}
return "", false
}
func extractGradioErrorMessage(dataStr string) string {
var errObj map[string]interface{}
if err := json.Unmarshal([]byte(dataStr), &errObj); err == nil {
@@ -2339,6 +2477,9 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
return fmt.Errorf("failed to encode request: %w", err)
}
completionID := "chatcmpl-" + GenerateUUID()
createdTime := time.Now().Unix()
// 1. Submit to /call/{endpoint}
callURL := fmt.Sprintf("%s%s/call/%s", disc.SpaceURL, disc.APIPrefix, disc.CleanEndpoint)
makeCallReq := func() (*http.Request, error) {
@@ -2353,6 +2494,27 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
resp, err := DoWithFibonacciRetry(g.client, makeCallReq, 5)
if err != nil {
if fg, ok := extractFailedGeneration(err.Error()); ok {
if tcs, _, has := DetectToolCalls(fg); has && len(tcs) > 0 {
if !req.Stream {
WriteCompletionResponse(w, completionID, createdTime, modelName, FinalOutput{
ToolCalls: tcs,
FinishReason: "tool_calls",
})
return nil
}
flusher, _ := w.(http.Flusher)
streamer := NewStreamer(w, flusher, completionID, createdTime, modelName)
for i, tc := range tcs {
iCopy := i
tc.Index = &iCopy
streamer.ToolCallDelta(tc)
}
streamer.Finish("tool_calls")
streamer.Done()
return nil
}
}
// If call failed, try without APIPrefix or try /call/v2
altCallURL := fmt.Sprintf("%s/call/%s", disc.SpaceURL, disc.CleanEndpoint)
makeAltReq := func() (*http.Request, error) {
@@ -2366,6 +2528,27 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
}
resp, err = DoWithFibonacciRetry(g.client, makeAltReq, 3)
if err != nil {
if fg, ok := extractFailedGeneration(err.Error()); ok {
if tcs, _, has := DetectToolCalls(fg); has && len(tcs) > 0 {
if !req.Stream {
WriteCompletionResponse(w, completionID, createdTime, modelName, FinalOutput{
ToolCalls: tcs,
FinishReason: "tool_calls",
})
return nil
}
flusher, _ := w.(http.Flusher)
streamer := NewStreamer(w, flusher, completionID, createdTime, modelName)
for i, tc := range tcs {
iCopy := i
tc.Index = &iCopy
streamer.ToolCallDelta(tc)
}
streamer.Finish("tool_calls")
streamer.Done()
return nil
}
}
return fmt.Errorf("upstream Gradio call error: %w", err)
}
}
@@ -2390,13 +2573,31 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
streamResp, err := DoWithFibonacciRetry(g.client, makeStreamReq, 5)
if err != nil {
if fg, ok := extractFailedGeneration(err.Error()); ok {
if tcs, _, has := DetectToolCalls(fg); has && len(tcs) > 0 {
if !req.Stream {
WriteCompletionResponse(w, completionID, createdTime, modelName, FinalOutput{
ToolCalls: tcs,
FinishReason: "tool_calls",
})
return nil
}
flusher, _ := w.(http.Flusher)
streamer := NewStreamer(w, flusher, completionID, createdTime, modelName)
for i, tc := range tcs {
iCopy := i
tc.Index = &iCopy
streamer.ToolCallDelta(tc)
}
streamer.Finish("tool_calls")
streamer.Done()
return nil
}
}
return fmt.Errorf("upstream Gradio stream error: %w", err)
}
defer streamResp.Body.Close()
completionID := "chatcmpl-" + GenerateUUID()
createdTime := time.Now().Unix()
// 3. Handle Non-Streaming vs Streaming
if !req.Stream {
reader := bufio.NewReader(streamResp.Body)
@@ -2418,6 +2619,15 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
if strings.HasPrefix(line, "data: ") {
dataStr := strings.TrimPrefix(line, "data: ")
if currentEvent == "error" {
if fg, ok := extractFailedGeneration(dataStr); ok {
if tcs, _, has := DetectToolCalls(fg); has && len(tcs) > 0 {
latestFrame = GradioOutputFrame{
ToolCalls: tcs,
OK: true,
}
break
}
}
errMsg := extractGradioErrorMessage(dataStr)
log.Printf("Upstream Gradio error: %s", errMsg)
return fmt.Errorf("upstream Gradio error: %s", errMsg)
@@ -2496,6 +2706,17 @@ func (g *GradioGateway) ExecuteChatCompletion(w http.ResponseWriter, r *http.Req
if strings.HasPrefix(line, "data: ") {
dataStr := strings.TrimPrefix(line, "data: ")
if currentEvent == "error" {
if fg, ok := extractFailedGeneration(dataStr); ok {
if tcs, _, has := DetectToolCalls(fg); has && len(tcs) > 0 {
for i, tc := range tcs {
iCopy := i
tc.Index = &iCopy
streamer.ToolCallDelta(tc)
}
nativeToolCallsSeen = true
break
}
}
errMsg := extractGradioErrorMessage(dataStr)
log.Printf("Upstream Gradio error: %s", errMsg)
if !streamer.started {