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,61 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Fake is a scripted LanguageModel for tests and demos. Responses are served
|
||||
// in order; when the script is exhausted the last response repeats. A nil
|
||||
// script yields a fixed "OK" answer.
|
||||
type Fake struct {
|
||||
mu sync.Mutex
|
||||
Script []string
|
||||
Err error
|
||||
Calls []Request
|
||||
ModelName string
|
||||
Delay time.Duration
|
||||
}
|
||||
|
||||
// Complete implements LanguageModel.
|
||||
func (f *Fake) Complete(ctx context.Context, req Request) (Response, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.Calls = append(f.Calls, req)
|
||||
if f.Delay > 0 {
|
||||
select {
|
||||
case <-time.After(f.Delay):
|
||||
case <-ctx.Done():
|
||||
return Response{}, ctx.Err()
|
||||
}
|
||||
}
|
||||
if f.Err != nil {
|
||||
return Response{}, f.Err
|
||||
}
|
||||
text := "OK"
|
||||
if n := len(f.Script); n > 0 {
|
||||
i := len(f.Calls) - 1
|
||||
if i >= n {
|
||||
i = n - 1
|
||||
}
|
||||
text = f.Script[i]
|
||||
}
|
||||
model := f.ModelName
|
||||
if model == "" {
|
||||
model = "fake-1"
|
||||
}
|
||||
resp := Response{Text: text, Provider: "fake", Model: model, Usage: Usage{PromptTokens: len(req.System)/4 + 10, CompletionTokens: len(text) / 4, TotalTokens: len(req.System)/4 + 10 + len(text)/4}, Latency: time.Millisecond}
|
||||
if len(req.JSONSchema) > 0 {
|
||||
if !json.Valid([]byte(text)) {
|
||||
return Response{}, ErrBadJSON
|
||||
}
|
||||
resp.JSON = json.RawMessage(text)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ErrFakeDown simulates an outage.
|
||||
var ErrFakeDown = errors.New("fake provider down")
|
||||
Reference in New Issue
Block a user