| 💾 Saved. d722711 k33g 7h ago | 1 | package engine |
| 2 | |
| 3 | import ( |
| 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. |
| 18 | var 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. |
| 24 | func (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. |
| 71 | func 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. |
| 81 | func 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 | } |