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:
+50
-8
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/andydixon/noema/internal/attention"
|
||||
"github.com/andydixon/noema/internal/auth"
|
||||
"github.com/andydixon/noema/internal/beliefs"
|
||||
"github.com/andydixon/noema/internal/llm"
|
||||
"github.com/andydixon/noema/internal/minds"
|
||||
"github.com/andydixon/noema/internal/version"
|
||||
"github.com/andydixon/noema/pkg/concepts"
|
||||
@@ -83,13 +84,34 @@ func (u *UI) parse(fsys fs.FS) error {
|
||||
"duration": func(s int64) string { return (time.Duration(s) * time.Second).Truncate(time.Second).String() },
|
||||
"bytes": humanBytes,
|
||||
"pct": func(f float64) string { return fmt.Sprintf("%.0f%%", f*100) },
|
||||
"conf": func(f float64) string { return fmt.Sprintf("%.2f", f) },
|
||||
"inc": func(i int) int { return i + 1 },
|
||||
"deref": func(f *float64) float64 {
|
||||
if f == nil {
|
||||
return 0
|
||||
"conf": func(v any) string {
|
||||
switch x := v.(type) {
|
||||
case float64:
|
||||
return fmt.Sprintf("%.2f", x)
|
||||
case float32:
|
||||
return fmt.Sprintf("%.2f", x)
|
||||
case *float64:
|
||||
if x != nil {
|
||||
return fmt.Sprintf("%.2f", *x)
|
||||
}
|
||||
}
|
||||
return *f
|
||||
return "–"
|
||||
},
|
||||
"inc": func(i int) int { return i + 1 },
|
||||
"deref": func(v any) any {
|
||||
switch x := v.(type) {
|
||||
case *float64:
|
||||
if x == nil {
|
||||
return 0.0
|
||||
}
|
||||
return *x
|
||||
case *bool:
|
||||
if x == nil {
|
||||
return false
|
||||
}
|
||||
return *x
|
||||
}
|
||||
return v
|
||||
},
|
||||
"title": func(s string) string { s = strings.ReplaceAll(s, "_", " "); return strings.ToUpper(s[:1]) + s[1:] },
|
||||
"valueKeys": func() []string { return minds.ValueKeys },
|
||||
@@ -97,6 +119,27 @@ func (u *UI) parse(fsys fs.FS) error {
|
||||
"memoryKinds": func() []concepts.MemoryKind { return concepts.AllMemoryKinds },
|
||||
"epistemicKinds": func() []concepts.Epistemic { return concepts.AllEpistemic },
|
||||
"relations": func() []concepts.Relation { return concepts.AllRelations },
|
||||
"llmRoles": func() []llm.RoleInfo { return llm.Roles },
|
||||
"deref_id": func(id *ids.ID) ids.ID {
|
||||
if id == nil {
|
||||
return ids.Nil
|
||||
}
|
||||
return *id
|
||||
},
|
||||
"deref_int": func(i *int) int {
|
||||
if i == nil {
|
||||
return 0
|
||||
}
|
||||
return *i
|
||||
},
|
||||
"has_id": func(list []ids.ID, id ids.ID) bool {
|
||||
for _, x := range list {
|
||||
if x == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
"beliefStatuses": func() []beliefs.Status {
|
||||
return []beliefs.Status{beliefs.StatusHypothesis, beliefs.StatusTentative, beliefs.StatusLikely, beliefs.StatusAccepted, beliefs.StatusContested, beliefs.StatusRejected, beliefs.StatusSuperseded}
|
||||
},
|
||||
@@ -253,11 +296,10 @@ func (u *UI) Register(m *http.ServeMux) {
|
||||
u.eventRoutes(m)
|
||||
u.memoryRoutes(m)
|
||||
u.beliefRoutes(m)
|
||||
u.llmRoutes(m)
|
||||
|
||||
for _, p := range []struct{ path, title, note string }{
|
||||
{"/approvals", "Approvals", "The approval centre arrives with the capability system."},
|
||||
{"/llm", "LLM", "Provider configuration arrives in phase 7."},
|
||||
{"/prompts", "Prompts", "The prompt manager arrives in phase 7."},
|
||||
{"/integrations", "Integrations", "Integrations arrive in phase 13."},
|
||||
{"/policies", "Policies", "The policy matrix arrives in phase 12."},
|
||||
} {
|
||||
|
||||
Reference in New Issue
Block a user