feat: add provider-neutral LLM layer with secrets, budgets, recording and prompts
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.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andydixon/noema/internal/llm"
|
||||
)
|
||||
|
||||
// Anthropic speaks the Messages API.
|
||||
type Anthropic struct{ cfg Config }
|
||||
|
||||
// NewAnthropic creates a client.
|
||||
func NewAnthropic(cfg Config) *Anthropic { return &Anthropic{cfg: cfg} }
|
||||
|
||||
// Complete implements llm.LanguageModel.
|
||||
func (p *Anthropic) Complete(ctx context.Context, req llm.Request) (llm.Response, error) {
|
||||
msgs := make([]map[string]string, 0, len(req.Messages))
|
||||
for _, m := range req.Messages {
|
||||
role := m.Role
|
||||
if role != "assistant" {
|
||||
role = "user"
|
||||
}
|
||||
msgs = append(msgs, map[string]string{"role": role, "content": m.Content})
|
||||
}
|
||||
system := req.System
|
||||
if len(req.JSONSchema) > 0 {
|
||||
system = strings.TrimSpace(system + "\n\nRespond with a single JSON object only, matching this JSON schema:\n" + string(req.JSONSchema))
|
||||
}
|
||||
maxTokens := req.MaxTokens
|
||||
if maxTokens <= 0 {
|
||||
maxTokens = 1024
|
||||
}
|
||||
body := map[string]any{"model": p.cfg.Model, "messages": msgs, "max_tokens": maxTokens}
|
||||
if system != "" {
|
||||
body["system"] = system
|
||||
}
|
||||
if req.Temperature != nil {
|
||||
body["temperature"] = *req.Temperature
|
||||
}
|
||||
if req.TopP != nil {
|
||||
body["top_p"] = *req.TopP
|
||||
}
|
||||
if len(req.Stop) > 0 {
|
||||
body["stop_sequences"] = req.Stop
|
||||
}
|
||||
headers := map[string]string{"anthropic-version": "2023-06-01"}
|
||||
if p.cfg.APIKey != "" {
|
||||
headers["x-api-key"] = p.cfg.APIKey
|
||||
}
|
||||
for k, v := range p.cfg.Headers {
|
||||
headers[k] = v
|
||||
}
|
||||
var out struct {
|
||||
Model string `json:"model"`
|
||||
Content []struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
StopReason string `json:"stop_reason"`
|
||||
Usage struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
} `json:"usage"`
|
||||
}
|
||||
start := time.Now()
|
||||
if err := post(ctx, p.cfg.client(), p.cfg.url("/v1/messages"), headers, body, &out); err != nil {
|
||||
return llm.Response{}, err
|
||||
}
|
||||
var text strings.Builder
|
||||
for _, c := range out.Content {
|
||||
if c.Type == "text" {
|
||||
text.WriteString(c.Text)
|
||||
}
|
||||
}
|
||||
resp := llm.Response{Text: text.String(), Provider: p.cfg.Name, Model: firstNonEmpty(out.Model, p.cfg.Model), Latency: time.Since(start), FinishReason: out.StopReason,
|
||||
Usage: llm.Usage{PromptTokens: out.Usage.InputTokens, CompletionTokens: out.Usage.OutputTokens, TotalTokens: out.Usage.InputTokens + out.Usage.OutputTokens}}
|
||||
return resp, finish(req, &resp)
|
||||
}
|
||||
Reference in New Issue
Block a user