bots-garden/mini-mepublic Fork 0
d72271127802973540c648bfb372176cdaaa8e4f
Commits
Clone
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

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