Phase 7 (ADR-0004). Cognitive code asks the router for a role; the router maps roles to configured models with ordered fallbacks, applies global and per-mind daily token budgets and a concurrency limit, records every call (request, response, usage, latency, cost, seed, request hash) and can serve recordings back for exact replay. Providers are raw HTTP clients for OpenAI-compatible, Anthropic and Ollama endpoints, with embedding support for the embed role wired into memory retrieval. Secrets are references only: env:, file:, or enc: values encrypted with AES-256-GCM under NOEMA_MASTER_KEY; keys are resolved at call time and never rendered, logged or exported. Outbound HTTP refuses private and loopback addresses at dial time unless a provider explicitly allows it, bounds redirects and caps bodies. Provider errors are scrubbed. Prompts are named, versioned rows with system and user templates and an optional JSON schema; untrusted text enters only through delimited data blocks. Six defaults are seeded and never overwritten. The UI adds the LLM page (providers, models, role matrix, usage, errors, budgets), a provider page with connectivity test, and a prompt manager with editor, test runner, history, restore, clone and diff. CLI: provider, prompt.
85 lines
2.7 KiB
Go
85 lines
2.7 KiB
Go
package providers
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/andydixon/noema/internal/llm"
|
|
)
|
|
|
|
// Ollama speaks the native Ollama API, which exposes seeds and JSON mode
|
|
// directly; llama.cpp servers should use the OpenAI kind instead.
|
|
type Ollama struct{ cfg Config }
|
|
|
|
// NewOllama creates a client.
|
|
func NewOllama(cfg Config) *Ollama { return &Ollama{cfg: cfg} }
|
|
|
|
// Complete implements llm.LanguageModel.
|
|
func (p *Ollama) Complete(ctx context.Context, req llm.Request) (llm.Response, error) {
|
|
opts := map[string]any{}
|
|
if req.Temperature != nil {
|
|
opts["temperature"] = *req.Temperature
|
|
}
|
|
if req.TopP != nil {
|
|
opts["top_p"] = *req.TopP
|
|
}
|
|
if req.MaxTokens > 0 {
|
|
opts["num_predict"] = req.MaxTokens
|
|
}
|
|
if req.Seed != nil {
|
|
opts["seed"] = *req.Seed
|
|
}
|
|
if len(req.Stop) > 0 {
|
|
opts["stop"] = req.Stop
|
|
}
|
|
body := map[string]any{"model": p.cfg.Model, "messages": messages(req), "stream": false, "options": opts}
|
|
if len(req.JSONSchema) > 0 {
|
|
body["format"] = "json"
|
|
}
|
|
var out struct {
|
|
Model string `json:"model"`
|
|
Message struct{ Content string } `json:"message"`
|
|
DoneReason string `json:"done_reason"`
|
|
PromptEvalCount int `json:"prompt_eval_count"`
|
|
EvalCount int `json:"eval_count"`
|
|
}
|
|
start := time.Now()
|
|
if err := post(ctx, p.cfg.client(), p.cfg.url("/api/chat"), p.cfg.Headers, body, &out); err != nil {
|
|
return llm.Response{}, err
|
|
}
|
|
resp := llm.Response{Text: out.Message.Content, Provider: p.cfg.Name, Model: firstNonEmpty(out.Model, p.cfg.Model), Latency: time.Since(start), FinishReason: out.DoneReason,
|
|
Usage: llm.Usage{PromptTokens: out.PromptEvalCount, CompletionTokens: out.EvalCount, TotalTokens: out.PromptEvalCount + out.EvalCount}}
|
|
return resp, finish(req, &resp)
|
|
}
|
|
|
|
// OllamaEmbedder calls /api/embed.
|
|
type OllamaEmbedder struct {
|
|
cfg Config
|
|
dims int
|
|
}
|
|
|
|
// NewOllamaEmbedder creates an embedder.
|
|
func NewOllamaEmbedder(cfg Config, dims int) *OllamaEmbedder {
|
|
return &OllamaEmbedder{cfg: cfg, dims: dims}
|
|
}
|
|
|
|
// Model implements embeddings.Embedder.
|
|
func (e *OllamaEmbedder) Model() string { return e.cfg.Name + "/" + e.cfg.Model }
|
|
|
|
// Dims implements embeddings.Embedder.
|
|
func (e *OllamaEmbedder) Dims() int { return e.dims }
|
|
|
|
// Embed implements embeddings.Embedder.
|
|
func (e *OllamaEmbedder) Embed(ctx context.Context, texts []string) ([][]float32, error) {
|
|
var out struct {
|
|
Embeddings [][]float32 `json:"embeddings"`
|
|
}
|
|
if err := post(ctx, e.cfg.client(), e.cfg.url("/api/embed"), e.cfg.Headers, map[string]any{"model": e.cfg.Model, "input": texts}, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(out.Embeddings) > 0 {
|
|
e.dims = len(out.Embeddings[0])
|
|
}
|
|
return out.Embeddings, nil
|
|
}
|