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
90
91
92
93
94
95
96
97
|
package engine
import (
"context"
"fmt"
"sort"
"strings"
"mm/internal/config"
"github.com/firebase/genkit/go/genkit"
)
// Provider is what changes from one LLM server to the next. There is ONE
// implementation per wire protocol, not one per vendor: Docker Model Runner and
// llama-server both speak the OpenAI chat-completions format, and differ only
// in a base URL, a key policy and the words of their error messages. Adding a
// server that speaks the same protocol is a registry entry (see openai_compat.go);
// adding a protocol (Anthropic's Messages API, Ollama's /api/chat) is a new type.
type Provider interface {
// Name is the registry key AND the Genkit plugin name, hence the prefix of
// the model reference Generate uses ("dmr/<id>").
Name() string
// Resolve turns the loaded config into a concrete Backend: the URL after
// environment overrides and the fallback probe, the key read from the
// environment, the model in the provider's own naming.
Resolve(cfg config.Config) (Backend, error)
// Open initialises Genkit with this provider's plugin, declares the model
// (stating that it knows how to call tools) and returns the full model
// reference. One Genkit per Engine for now; several backends in one Genkit
// is the seam for a `/model` command later.
Open(ctx context.Context, b Backend) (*genkit.Genkit, string, error)
// Probe learns what it can about the server before the first question.
// Best effort, never fatal: a server started AFTER the agent is a normal
// demo situation, not an error.
Probe(ctx context.Context, b Backend) Info
// Explain rewrites a transport error as ONE actionable line, in the words of
// this server: "start llama-server with --jinja" means nothing to a DMR
// user, and "docker model pull" nothing to a llama.cpp user.
Explain(err error, b Backend) string
}
// Backend is a resolved target: everything Open and Explain need, and nothing
// that still depends on the environment.
type Backend struct {
Provider string
BaseURL string
Model string // in the provider's naming, no Genkit prefix
APIKey string // the resolved value; a placeholder when the server ignores it
// APIKeyEnv is the variable the key was read from — the yaml's `apiKeyEnv`,
// else the provider's default — or "" when neither names one. Explain cites
// it on a 401: the first version named a variable nothing read, and a
// message that sends the user to `export` the wrong name is worse than none.
APIKeyEnv string
// ContextWindow is the hint from the config, 0 when absent. The probe may
// fill it from the server (llama-server tells its n_ctx); the config wins,
// because the operator knows what they started the server with.
ContextWindow int
}
// Info is what Probe found out. Warnings are printed one per line at start-up,
// dimmed like the config line; they are advice, not failures.
type Info struct {
Reachable bool
ContextWindow int // 0 = unknown
ContextSource string // "config", "/props", … — a number without an origin never gets corrected
Warnings []string
}
// registry: the providers this build knows. Keys are the values `provider:`
// accepts in agent.yaml.
var registry = map[string]Provider{}
func register(p Provider) { registry[p.Name()] = p }
// Lookup finds a provider by its config name. The error lists the known names:
// the user typed one, the fix is to pick another.
func Lookup(name string) (Provider, error) {
if p, ok := registry[name]; ok {
return p, nil
}
return nil, fmt.Errorf("unknown provider %q (known: %s)", name, strings.Join(Names(), ", "))
}
// Names lists the registered providers, sorted for stable messages.
func Names() []string {
names := make([]string, 0, len(registry))
for n := range registry {
names = append(names, n)
}
sort.Strings(names)
return names
}
|