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.

💾 Saved. d722711 · on d72271127802973540c648bfb372176cdaaa8e4f · k33g · 6h ago
explain.go · 89 lines · 3.1 KBGo Blame HistoryRaw
 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
}