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/"). 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 }