1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
|
package engine
import (
"errors"
"fmt"
"net"
"regexp"
"strconv"
"strings"
"github.com/openai/openai-go"
)
// statusInText recovers the HTTP status from the OpenAI client's error text
// (`POST "http://…": 401 Unauthorized {…}`). Genkit sometimes rebuilds errors
// with %v instead of %w, and the typed error is then out of reach of errors.As;
// the text still carries the number.
var statusInText = regexp.MustCompile(`": (\d{3}) `)
// Explain maps a failed generation to one line the user can act on. The REPL
// prints `[error: …]` and moves on; a stack of wrapped messages there is noise
// on stage, while "start llama-server with --jinja" is the whole diagnosis.
// Unknown errors come back unchanged: better a raw message than a wrong hint.
func (p *openaiCompat) Explain(err error, b Backend) string {
if err == nil {
return ""
}
msg := err.Error()
status := 0
var apiErr *openai.Error
if errors.As(err, &apiErr) && apiErr.Response != nil {
status = apiErr.Response.StatusCode
} else if m := statusInText.FindStringSubmatch(msg); m != nil {
status, _ = strconv.Atoi(m[1])
}
switch {
case connectionRefused(err, msg):
return fmt.Sprintf("%s: nothing answers at %s — %s", p.name, b.BaseURL, p.startHint)
// llama-server without --jinja refuses `tools` with a message naming the
// flag. It is the one llama.cpp error a first-time user hits, so it gets
// its own line rather than the generic "server error".
case strings.Contains(msg, "jinja"):
return fmt.Sprintf("%s: tool calls need llama-server started with --jinja", p.name)
// The variable named here is the one Resolve actually read (yaml `apiKeyEnv`,
// else the provider's default). When there is none — DMR ignores keys — a
// 401 means the URL is not the server we think it is, so that is the hint.
case status == 401 || status == 403:
if b.APIKeyEnv == "" {
return fmt.Sprintf("%s: authentication failed at %s — this server should not need a key; check baseUrl", p.name, b.BaseURL)
}
return fmt.Sprintf("%s: authentication failed at %s — is $%s set and valid?", p.name, b.BaseURL, b.APIKeyEnv)
case status == 404:
return fmt.Sprintf("%s: %q not found at %s — %s", p.name, b.Model, b.BaseURL, fmt.Sprintf(p.notFoundHint, b.Model))
case status == 429:
return fmt.Sprintf("%s: rate limited — wait, or lower the pace", p.name)
case status >= 500:
return fmt.Sprintf("%s: server error %d — %s", p.name, status, firstLine(msg))
}
return msg
}
// connectionRefused: a dial that failed, whatever the wrapping. The typed check
// covers the errors.As path; the text check covers Genkit's %v rebuilds.
func connectionRefused(err error, msg string) bool {
var opErr *net.OpError
if errors.As(err, &opErr) && opErr.Op == "dial" {
return true
}
return strings.Contains(msg, "connection refused") || strings.Contains(msg, "no such host")
}
// firstLine keeps a server's message to its first line, cut at 120 columns —
// the same one-line-per-item rule as the command recap.
func firstLine(s string) string {
if i := strings.IndexByte(s, '\n'); i >= 0 {
s = s[:i]
}
if r := []rune(s); len(r) > 120 {
return string(r[:119]) + "…"
}
return s
}
|