|
5ac549c…
|
lmata
|
1 |
package api |
|
5ac549c…
|
lmata
|
2 |
|
|
5ac549c…
|
lmata
|
3 |
import ( |
|
5ac549c…
|
lmata
|
4 |
"encoding/json" |
|
5ac549c…
|
lmata
|
5 |
"net/http" |
|
5ac549c…
|
lmata
|
6 |
"sort" |
|
5ac549c…
|
lmata
|
7 |
|
|
5ac549c…
|
lmata
|
8 |
"github.com/conflicthq/scuttlebot/internal/config" |
|
5ac549c…
|
lmata
|
9 |
"github.com/conflicthq/scuttlebot/internal/llm" |
|
5ac549c…
|
lmata
|
10 |
) |
|
5ac549c…
|
lmata
|
11 |
|
|
5ac549c…
|
lmata
|
12 |
// backendView is the read-safe representation of a backend returned by the API. |
|
5ac549c…
|
lmata
|
13 |
// API keys are replaced with "***" if set, so they are never exposed. |
|
5ac549c…
|
lmata
|
14 |
type backendView struct { |
|
5ac549c…
|
lmata
|
15 |
Name string `json:"name"` |
|
5ac549c…
|
lmata
|
16 |
Backend string `json:"backend"` |
|
1066004…
|
lmata
|
17 |
APIKey string `json:"api_key,omitempty"` // "***" if set, "" if not |
|
5ac549c…
|
lmata
|
18 |
BaseURL string `json:"base_url,omitempty"` |
|
5ac549c…
|
lmata
|
19 |
Model string `json:"model,omitempty"` |
|
5ac549c…
|
lmata
|
20 |
Region string `json:"region,omitempty"` |
|
5ac549c…
|
lmata
|
21 |
AWSKeyID string `json:"aws_key_id,omitempty"` // "***" if set |
|
5ac549c…
|
lmata
|
22 |
Allow []string `json:"allow,omitempty"` |
|
5ac549c…
|
lmata
|
23 |
Block []string `json:"block,omitempty"` |
|
5ac549c…
|
lmata
|
24 |
Default bool `json:"default,omitempty"` |
|
5ac549c…
|
lmata
|
25 |
Source string `json:"source"` // "config" (yaml, read-only) or "policy" (ui-managed) |
|
5ac549c…
|
lmata
|
26 |
} |
|
5ac549c…
|
lmata
|
27 |
|
|
5ac549c…
|
lmata
|
28 |
func mask(s string) string { |
|
5ac549c…
|
lmata
|
29 |
if s == "" { |
|
5ac549c…
|
lmata
|
30 |
return "" |
|
5ac549c…
|
lmata
|
31 |
} |
|
5ac549c…
|
lmata
|
32 |
return "***" |
|
5ac549c…
|
lmata
|
33 |
} |
|
5ac549c…
|
lmata
|
34 |
|
|
5ac549c…
|
lmata
|
35 |
// handleLLMKnown returns the list of all known backend names. |
|
5ac549c…
|
lmata
|
36 |
func (s *Server) handleLLMKnown(w http.ResponseWriter, _ *http.Request) { |
|
5ac549c…
|
lmata
|
37 |
type knownBackend struct { |
|
5ac549c…
|
lmata
|
38 |
Name string `json:"name"` |
|
5ac549c…
|
lmata
|
39 |
BaseURL string `json:"base_url,omitempty"` |
|
5ac549c…
|
lmata
|
40 |
Native bool `json:"native,omitempty"` |
|
5ac549c…
|
lmata
|
41 |
} |
|
5ac549c…
|
lmata
|
42 |
|
|
5ac549c…
|
lmata
|
43 |
var backends []knownBackend |
|
5ac549c…
|
lmata
|
44 |
for name, url := range llm.KnownBackends { |
|
5ac549c…
|
lmata
|
45 |
backends = append(backends, knownBackend{Name: name, BaseURL: url}) |
|
5ac549c…
|
lmata
|
46 |
} |
|
5ac549c…
|
lmata
|
47 |
backends = append(backends, |
|
5ac549c…
|
lmata
|
48 |
knownBackend{Name: "anthropic", Native: true}, |
|
5ac549c…
|
lmata
|
49 |
knownBackend{Name: "gemini", Native: true}, |
|
5ac549c…
|
lmata
|
50 |
knownBackend{Name: "bedrock", Native: true}, |
|
5ac549c…
|
lmata
|
51 |
knownBackend{Name: "ollama", BaseURL: "http://localhost:11434", Native: true}, |
|
5ac549c…
|
lmata
|
52 |
) |
|
5ac549c…
|
lmata
|
53 |
sort.Slice(backends, func(i, j int) bool { |
|
5ac549c…
|
lmata
|
54 |
return backends[i].Name < backends[j].Name |
|
5ac549c…
|
lmata
|
55 |
}) |
|
5ac549c…
|
lmata
|
56 |
writeJSON(w, http.StatusOK, backends) |
|
5ac549c…
|
lmata
|
57 |
} |
|
5ac549c…
|
lmata
|
58 |
|
|
5ac549c…
|
lmata
|
59 |
// handleLLMBackends lists all configured backends (YAML config + policy store). |
|
5ac549c…
|
lmata
|
60 |
// API keys are masked. |
|
5ac549c…
|
lmata
|
61 |
func (s *Server) handleLLMBackends(w http.ResponseWriter, _ *http.Request) { |
|
5ac549c…
|
lmata
|
62 |
var out []backendView |
|
5ac549c…
|
lmata
|
63 |
|
|
5ac549c…
|
lmata
|
64 |
// YAML-configured backends (read-only). |
|
5ac549c…
|
lmata
|
65 |
if s.llmCfg != nil { |
|
5ac549c…
|
lmata
|
66 |
for _, b := range s.llmCfg.Backends { |
|
5ac549c…
|
lmata
|
67 |
out = append(out, backendView{ |
|
5ac549c…
|
lmata
|
68 |
Name: b.Name, |
|
5ac549c…
|
lmata
|
69 |
Backend: b.Backend, |
|
5ac549c…
|
lmata
|
70 |
APIKey: mask(b.APIKey), |
|
5ac549c…
|
lmata
|
71 |
BaseURL: b.BaseURL, |
|
5ac549c…
|
lmata
|
72 |
Model: b.Model, |
|
5ac549c…
|
lmata
|
73 |
Region: b.Region, |
|
5ac549c…
|
lmata
|
74 |
AWSKeyID: mask(b.AWSKeyID), |
|
5ac549c…
|
lmata
|
75 |
Allow: b.Allow, |
|
5ac549c…
|
lmata
|
76 |
Block: b.Block, |
|
5ac549c…
|
lmata
|
77 |
Default: b.Default, |
|
5ac549c…
|
lmata
|
78 |
Source: "config", |
|
5ac549c…
|
lmata
|
79 |
}) |
|
5ac549c…
|
lmata
|
80 |
} |
|
5ac549c…
|
lmata
|
81 |
} |
|
5ac549c…
|
lmata
|
82 |
|
|
5ac549c…
|
lmata
|
83 |
// Policy-store backends (UI-managed, editable). |
|
5ac549c…
|
lmata
|
84 |
if s.policies != nil { |
|
5ac549c…
|
lmata
|
85 |
for _, b := range s.policies.Get().LLMBackends { |
|
5ac549c…
|
lmata
|
86 |
out = append(out, backendView{ |
|
5ac549c…
|
lmata
|
87 |
Name: b.Name, |
|
5ac549c…
|
lmata
|
88 |
Backend: b.Backend, |
|
5ac549c…
|
lmata
|
89 |
APIKey: mask(b.APIKey), |
|
5ac549c…
|
lmata
|
90 |
BaseURL: b.BaseURL, |
|
5ac549c…
|
lmata
|
91 |
Model: b.Model, |
|
5ac549c…
|
lmata
|
92 |
Region: b.Region, |
|
5ac549c…
|
lmata
|
93 |
AWSKeyID: mask(b.AWSKeyID), |
|
5ac549c…
|
lmata
|
94 |
Allow: b.Allow, |
|
5ac549c…
|
lmata
|
95 |
Block: b.Block, |
|
5ac549c…
|
lmata
|
96 |
Default: b.Default, |
|
5ac549c…
|
lmata
|
97 |
Source: "policy", |
|
5ac549c…
|
lmata
|
98 |
}) |
|
5ac549c…
|
lmata
|
99 |
} |
|
5ac549c…
|
lmata
|
100 |
} |
|
5ac549c…
|
lmata
|
101 |
|
|
5ac549c…
|
lmata
|
102 |
if out == nil { |
|
5ac549c…
|
lmata
|
103 |
out = []backendView{} |
|
5ac549c…
|
lmata
|
104 |
} |
|
5ac549c…
|
lmata
|
105 |
writeJSON(w, http.StatusOK, out) |
|
5ac549c…
|
lmata
|
106 |
} |
|
5ac549c…
|
lmata
|
107 |
|
|
5ac549c…
|
lmata
|
108 |
// handleLLMBackendCreate adds a new backend to the policy store. |
|
5ac549c…
|
lmata
|
109 |
func (s *Server) handleLLMBackendCreate(w http.ResponseWriter, r *http.Request) { |
|
5ac549c…
|
lmata
|
110 |
if s.policies == nil { |
|
5ac549c…
|
lmata
|
111 |
http.Error(w, "policy store not available", http.StatusServiceUnavailable) |
|
5ac549c…
|
lmata
|
112 |
return |
|
5ac549c…
|
lmata
|
113 |
} |
|
5ac549c…
|
lmata
|
114 |
var b PolicyLLMBackend |
|
5ac549c…
|
lmata
|
115 |
if err := json.NewDecoder(r.Body).Decode(&b); err != nil { |
|
5ac549c…
|
lmata
|
116 |
http.Error(w, "invalid request body", http.StatusBadRequest) |
|
5ac549c…
|
lmata
|
117 |
return |
|
5ac549c…
|
lmata
|
118 |
} |
|
5ac549c…
|
lmata
|
119 |
if b.Name == "" || b.Backend == "" { |
|
5ac549c…
|
lmata
|
120 |
http.Error(w, "name and backend are required", http.StatusBadRequest) |
|
5ac549c…
|
lmata
|
121 |
return |
|
5ac549c…
|
lmata
|
122 |
} |
|
5ac549c…
|
lmata
|
123 |
|
|
5ac549c…
|
lmata
|
124 |
p := s.policies.Get() |
|
5ac549c…
|
lmata
|
125 |
for _, existing := range p.LLMBackends { |
|
5ac549c…
|
lmata
|
126 |
if existing.Name == b.Name { |
|
5ac549c…
|
lmata
|
127 |
http.Error(w, "backend name already exists", http.StatusConflict) |
|
5ac549c…
|
lmata
|
128 |
return |
|
5ac549c…
|
lmata
|
129 |
} |
|
5ac549c…
|
lmata
|
130 |
} |
|
5ac549c…
|
lmata
|
131 |
// Also check YAML backends. |
|
5ac549c…
|
lmata
|
132 |
if s.llmCfg != nil { |
|
5ac549c…
|
lmata
|
133 |
for _, existing := range s.llmCfg.Backends { |
|
5ac549c…
|
lmata
|
134 |
if existing.Name == b.Name { |
|
5ac549c…
|
lmata
|
135 |
http.Error(w, "backend name already exists in config", http.StatusConflict) |
|
5ac549c…
|
lmata
|
136 |
return |
|
5ac549c…
|
lmata
|
137 |
} |
|
5ac549c…
|
lmata
|
138 |
} |
|
5ac549c…
|
lmata
|
139 |
} |
|
5ac549c…
|
lmata
|
140 |
|
|
5ac549c…
|
lmata
|
141 |
p.LLMBackends = append(p.LLMBackends, b) |
|
5ac549c…
|
lmata
|
142 |
if err := s.policies.Set(p); err != nil { |
|
5ac549c…
|
lmata
|
143 |
http.Error(w, "failed to save", http.StatusInternalServerError) |
|
5ac549c…
|
lmata
|
144 |
return |
|
5ac549c…
|
lmata
|
145 |
} |
|
5ac549c…
|
lmata
|
146 |
writeJSON(w, http.StatusCreated, backendView{ |
|
5ac549c…
|
lmata
|
147 |
Name: b.Name, |
|
5ac549c…
|
lmata
|
148 |
Backend: b.Backend, |
|
5ac549c…
|
lmata
|
149 |
APIKey: mask(b.APIKey), |
|
5ac549c…
|
lmata
|
150 |
BaseURL: b.BaseURL, |
|
5ac549c…
|
lmata
|
151 |
Model: b.Model, |
|
5ac549c…
|
lmata
|
152 |
Region: b.Region, |
|
5ac549c…
|
lmata
|
153 |
Allow: b.Allow, |
|
5ac549c…
|
lmata
|
154 |
Block: b.Block, |
|
5ac549c…
|
lmata
|
155 |
Default: b.Default, |
|
5ac549c…
|
lmata
|
156 |
Source: "policy", |
|
5ac549c…
|
lmata
|
157 |
}) |
|
5ac549c…
|
lmata
|
158 |
} |
|
5ac549c…
|
lmata
|
159 |
|
|
5ac549c…
|
lmata
|
160 |
// handleLLMBackendUpdate updates a policy-store backend by name. |
|
5ac549c…
|
lmata
|
161 |
// Fields present in the request body override the stored value. |
|
5ac549c…
|
lmata
|
162 |
// Send api_key / aws_secret_key as "" to leave the stored value unchanged |
|
5ac549c…
|
lmata
|
163 |
// (the UI masks these and should omit them if unchanged). |
|
5ac549c…
|
lmata
|
164 |
func (s *Server) handleLLMBackendUpdate(w http.ResponseWriter, r *http.Request) { |
|
5ac549c…
|
lmata
|
165 |
if s.policies == nil { |
|
5ac549c…
|
lmata
|
166 |
http.Error(w, "policy store not available", http.StatusServiceUnavailable) |
|
5ac549c…
|
lmata
|
167 |
return |
|
5ac549c…
|
lmata
|
168 |
} |
|
5ac549c…
|
lmata
|
169 |
name := r.PathValue("name") |
|
5ac549c…
|
lmata
|
170 |
var req PolicyLLMBackend |
|
5ac549c…
|
lmata
|
171 |
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
|
5ac549c…
|
lmata
|
172 |
http.Error(w, "invalid request body", http.StatusBadRequest) |
|
5ac549c…
|
lmata
|
173 |
return |
|
5ac549c…
|
lmata
|
174 |
} |
|
5ac549c…
|
lmata
|
175 |
|
|
5ac549c…
|
lmata
|
176 |
p := s.policies.Get() |
|
5ac549c…
|
lmata
|
177 |
idx := -1 |
|
5ac549c…
|
lmata
|
178 |
for i, b := range p.LLMBackends { |
|
5ac549c…
|
lmata
|
179 |
if b.Name == name { |
|
5ac549c…
|
lmata
|
180 |
idx = i |
|
5ac549c…
|
lmata
|
181 |
break |
|
5ac549c…
|
lmata
|
182 |
} |
|
5ac549c…
|
lmata
|
183 |
} |
|
5ac549c…
|
lmata
|
184 |
if idx == -1 { |
|
5ac549c…
|
lmata
|
185 |
http.Error(w, "backend not found (only policy backends are editable)", http.StatusNotFound) |
|
5ac549c…
|
lmata
|
186 |
return |
|
5ac549c…
|
lmata
|
187 |
} |
|
5ac549c…
|
lmata
|
188 |
|
|
5ac549c…
|
lmata
|
189 |
existing := p.LLMBackends[idx] |
|
5ac549c…
|
lmata
|
190 |
// Preserve stored secrets when the UI sends "***" or empty. |
|
5ac549c…
|
lmata
|
191 |
if req.APIKey == "" || req.APIKey == "***" { |
|
5ac549c…
|
lmata
|
192 |
req.APIKey = existing.APIKey |
|
5ac549c…
|
lmata
|
193 |
} |
|
5ac549c…
|
lmata
|
194 |
if req.AWSSecretKey == "" || req.AWSSecretKey == "***" { |
|
5ac549c…
|
lmata
|
195 |
req.AWSSecretKey = existing.AWSSecretKey |
|
5ac549c…
|
lmata
|
196 |
} |
|
5ac549c…
|
lmata
|
197 |
if req.AWSKeyID == "" || req.AWSKeyID == "***" { |
|
5ac549c…
|
lmata
|
198 |
req.AWSKeyID = existing.AWSKeyID |
|
5ac549c…
|
lmata
|
199 |
} |
|
5ac549c…
|
lmata
|
200 |
req.Name = name // name is immutable |
|
5ac549c…
|
lmata
|
201 |
p.LLMBackends[idx] = req |
|
5ac549c…
|
lmata
|
202 |
|
|
5ac549c…
|
lmata
|
203 |
if err := s.policies.Set(p); err != nil { |
|
5ac549c…
|
lmata
|
204 |
http.Error(w, "failed to save", http.StatusInternalServerError) |
|
5ac549c…
|
lmata
|
205 |
return |
|
5ac549c…
|
lmata
|
206 |
} |
|
5ac549c…
|
lmata
|
207 |
writeJSON(w, http.StatusOK, backendView{ |
|
5ac549c…
|
lmata
|
208 |
Name: req.Name, |
|
5ac549c…
|
lmata
|
209 |
Backend: req.Backend, |
|
5ac549c…
|
lmata
|
210 |
APIKey: mask(req.APIKey), |
|
5ac549c…
|
lmata
|
211 |
BaseURL: req.BaseURL, |
|
5ac549c…
|
lmata
|
212 |
Model: req.Model, |
|
5ac549c…
|
lmata
|
213 |
Region: req.Region, |
|
5ac549c…
|
lmata
|
214 |
Allow: req.Allow, |
|
5ac549c…
|
lmata
|
215 |
Block: req.Block, |
|
5ac549c…
|
lmata
|
216 |
Default: req.Default, |
|
5ac549c…
|
lmata
|
217 |
Source: "policy", |
|
5ac549c…
|
lmata
|
218 |
}) |
|
5ac549c…
|
lmata
|
219 |
} |
|
5ac549c…
|
lmata
|
220 |
|
|
5ac549c…
|
lmata
|
221 |
// handleLLMBackendDelete removes a policy-store backend by name. |
|
5ac549c…
|
lmata
|
222 |
func (s *Server) handleLLMBackendDelete(w http.ResponseWriter, r *http.Request) { |
|
5ac549c…
|
lmata
|
223 |
if s.policies == nil { |
|
5ac549c…
|
lmata
|
224 |
http.Error(w, "policy store not available", http.StatusServiceUnavailable) |
|
5ac549c…
|
lmata
|
225 |
return |
|
5ac549c…
|
lmata
|
226 |
} |
|
5ac549c…
|
lmata
|
227 |
name := r.PathValue("name") |
|
5ac549c…
|
lmata
|
228 |
p := s.policies.Get() |
|
5ac549c…
|
lmata
|
229 |
idx := -1 |
|
5ac549c…
|
lmata
|
230 |
for i, b := range p.LLMBackends { |
|
5ac549c…
|
lmata
|
231 |
if b.Name == name { |
|
5ac549c…
|
lmata
|
232 |
idx = i |
|
5ac549c…
|
lmata
|
233 |
break |
|
5ac549c…
|
lmata
|
234 |
} |
|
5ac549c…
|
lmata
|
235 |
} |
|
5ac549c…
|
lmata
|
236 |
if idx == -1 { |
|
5ac549c…
|
lmata
|
237 |
http.Error(w, "backend not found (only policy backends are deletable)", http.StatusNotFound) |
|
5ac549c…
|
lmata
|
238 |
return |
|
5ac549c…
|
lmata
|
239 |
} |
|
5ac549c…
|
lmata
|
240 |
p.LLMBackends = append(p.LLMBackends[:idx], p.LLMBackends[idx+1:]...) |
|
5ac549c…
|
lmata
|
241 |
if err := s.policies.Set(p); err != nil { |
|
5ac549c…
|
lmata
|
242 |
http.Error(w, "failed to save", http.StatusInternalServerError) |
|
5ac549c…
|
lmata
|
243 |
return |
|
5ac549c…
|
lmata
|
244 |
} |
|
5ac549c…
|
lmata
|
245 |
w.WriteHeader(http.StatusNoContent) |
|
5ac549c…
|
lmata
|
246 |
} |
|
5ac549c…
|
lmata
|
247 |
|
|
5ac549c…
|
lmata
|
248 |
// handleLLMModels runs model discovery for the named backend. |
|
5ac549c…
|
lmata
|
249 |
// Looks in YAML config first, then policy store. |
|
5ac549c…
|
lmata
|
250 |
func (s *Server) handleLLMModels(w http.ResponseWriter, r *http.Request) { |
|
5ac549c…
|
lmata
|
251 |
name := r.PathValue("name") |
|
5ac549c…
|
lmata
|
252 |
cfg, ok := s.findBackendConfig(name) |
|
5ac549c…
|
lmata
|
253 |
if !ok { |
|
5ac549c…
|
lmata
|
254 |
http.Error(w, "backend not found", http.StatusNotFound) |
|
5ac549c…
|
lmata
|
255 |
return |
|
5ac549c…
|
lmata
|
256 |
} |
|
5ac549c…
|
lmata
|
257 |
models, err := llm.Discover(r.Context(), cfg) |
|
5ac549c…
|
lmata
|
258 |
if err != nil { |
|
5ac549c…
|
lmata
|
259 |
s.log.Error("llm model discovery", "backend", name, "err", err) |
|
5ac549c…
|
lmata
|
260 |
http.Error(w, "model discovery failed: "+err.Error(), http.StatusBadGateway) |
|
5ac549c…
|
lmata
|
261 |
return |
|
5ac549c…
|
lmata
|
262 |
} |
|
5ac549c…
|
lmata
|
263 |
writeJSON(w, http.StatusOK, models) |
|
5ac549c…
|
lmata
|
264 |
} |
|
5ac549c…
|
lmata
|
265 |
|
|
5ac549c…
|
lmata
|
266 |
// handleLLMDiscover runs ad-hoc model discovery from form credentials. |
|
5ac549c…
|
lmata
|
267 |
// Used by the UI "load live models" button before a backend is saved. |
|
5ac549c…
|
lmata
|
268 |
func (s *Server) handleLLMDiscover(w http.ResponseWriter, r *http.Request) { |
|
5ac549c…
|
lmata
|
269 |
var req struct { |
|
5ac549c…
|
lmata
|
270 |
Backend string `json:"backend"` |
|
5ac549c…
|
lmata
|
271 |
APIKey string `json:"api_key"` |
|
5ac549c…
|
lmata
|
272 |
BaseURL string `json:"base_url"` |
|
5ac549c…
|
lmata
|
273 |
Model string `json:"model"` |
|
5ac549c…
|
lmata
|
274 |
Region string `json:"region"` |
|
5ac549c…
|
lmata
|
275 |
AWSKeyID string `json:"aws_key_id"` |
|
5ac549c…
|
lmata
|
276 |
AWSSecretKey string `json:"aws_secret_key"` |
|
5ac549c…
|
lmata
|
277 |
Allow []string `json:"allow"` |
|
5ac549c…
|
lmata
|
278 |
Block []string `json:"block"` |
|
5ac549c…
|
lmata
|
279 |
} |
|
5ac549c…
|
lmata
|
280 |
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
|
5ac549c…
|
lmata
|
281 |
http.Error(w, "invalid request body", http.StatusBadRequest) |
|
5ac549c…
|
lmata
|
282 |
return |
|
5ac549c…
|
lmata
|
283 |
} |
|
5ac549c…
|
lmata
|
284 |
if req.Backend == "" { |
|
5ac549c…
|
lmata
|
285 |
http.Error(w, "backend is required", http.StatusBadRequest) |
|
5ac549c…
|
lmata
|
286 |
return |
|
5ac549c…
|
lmata
|
287 |
} |
|
5ac549c…
|
lmata
|
288 |
cfg := llm.BackendConfig{ |
|
5ac549c…
|
lmata
|
289 |
Backend: req.Backend, |
|
5ac549c…
|
lmata
|
290 |
APIKey: req.APIKey, |
|
5ac549c…
|
lmata
|
291 |
BaseURL: req.BaseURL, |
|
5ac549c…
|
lmata
|
292 |
Model: req.Model, |
|
5ac549c…
|
lmata
|
293 |
Region: req.Region, |
|
5ac549c…
|
lmata
|
294 |
AWSKeyID: req.AWSKeyID, |
|
5ac549c…
|
lmata
|
295 |
AWSSecretKey: req.AWSSecretKey, |
|
5ac549c…
|
lmata
|
296 |
Allow: req.Allow, |
|
5ac549c…
|
lmata
|
297 |
Block: req.Block, |
|
5ac549c…
|
lmata
|
298 |
} |
|
5ac549c…
|
lmata
|
299 |
models, err := llm.Discover(r.Context(), cfg) |
|
5ac549c…
|
lmata
|
300 |
if err != nil { |
|
5ac549c…
|
lmata
|
301 |
s.log.Error("llm ad-hoc discovery", "backend", req.Backend, "err", err) |
|
5ac549c…
|
lmata
|
302 |
http.Error(w, "model discovery failed: "+err.Error(), http.StatusBadGateway) |
|
5ac549c…
|
lmata
|
303 |
return |
|
5ac549c…
|
lmata
|
304 |
} |
|
5ac549c…
|
lmata
|
305 |
writeJSON(w, http.StatusOK, models) |
|
5ac549c…
|
lmata
|
306 |
} |
|
5ac549c…
|
lmata
|
307 |
|
|
5ac549c…
|
lmata
|
308 |
// findBackendConfig looks up a backend by name in YAML config then policy store. |
|
5ac549c…
|
lmata
|
309 |
func (s *Server) findBackendConfig(name string) (llm.BackendConfig, bool) { |
|
5ac549c…
|
lmata
|
310 |
if s.llmCfg != nil { |
|
5ac549c…
|
lmata
|
311 |
for _, b := range s.llmCfg.Backends { |
|
5ac549c…
|
lmata
|
312 |
if b.Name == name { |
|
5ac549c…
|
lmata
|
313 |
return yamlBackendToLLM(b), true |
|
5ac549c…
|
lmata
|
314 |
} |
|
5ac549c…
|
lmata
|
315 |
} |
|
5ac549c…
|
lmata
|
316 |
} |
|
5ac549c…
|
lmata
|
317 |
if s.policies != nil { |
|
5ac549c…
|
lmata
|
318 |
for _, b := range s.policies.Get().LLMBackends { |
|
5ac549c…
|
lmata
|
319 |
if b.Name == name { |
|
5ac549c…
|
lmata
|
320 |
return policyBackendToLLM(b), true |
|
5ac549c…
|
lmata
|
321 |
} |
|
5ac549c…
|
lmata
|
322 |
} |
|
5ac549c…
|
lmata
|
323 |
} |
|
5ac549c…
|
lmata
|
324 |
return llm.BackendConfig{}, false |
|
5ac549c…
|
lmata
|
325 |
} |
|
5ac549c…
|
lmata
|
326 |
|
|
5ac549c…
|
lmata
|
327 |
func yamlBackendToLLM(b config.LLMBackendConfig) llm.BackendConfig { |
|
5ac549c…
|
lmata
|
328 |
return llm.BackendConfig{ |
|
5ac549c…
|
lmata
|
329 |
Backend: b.Backend, |
|
5ac549c…
|
lmata
|
330 |
APIKey: b.APIKey, |
|
5ac549c…
|
lmata
|
331 |
BaseURL: b.BaseURL, |
|
5ac549c…
|
lmata
|
332 |
Model: b.Model, |
|
5ac549c…
|
lmata
|
333 |
Region: b.Region, |
|
5ac549c…
|
lmata
|
334 |
AWSKeyID: b.AWSKeyID, |
|
5ac549c…
|
lmata
|
335 |
AWSSecretKey: b.AWSSecretKey, |
|
5ac549c…
|
lmata
|
336 |
Allow: b.Allow, |
|
5ac549c…
|
lmata
|
337 |
Block: b.Block, |
|
5ac549c…
|
lmata
|
338 |
} |
|
5ac549c…
|
lmata
|
339 |
} |
|
5ac549c…
|
lmata
|
340 |
|
|
5ac549c…
|
lmata
|
341 |
// handleLLMComplete proxies a prompt to a named backend and returns the text. |
|
5ac549c…
|
lmata
|
342 |
// The API key stays server-side — callers only need a Bearer token. |
|
5ac549c…
|
lmata
|
343 |
// |
|
5ac549c…
|
lmata
|
344 |
// POST /v1/llm/complete |
|
5ac549c…
|
lmata
|
345 |
// |
|
5ac549c…
|
lmata
|
346 |
// {"backend": "anthro", "prompt": "hello"} |
|
5ac549c…
|
lmata
|
347 |
// → {"text": "..."} |
|
5ac549c…
|
lmata
|
348 |
func (s *Server) handleLLMComplete(w http.ResponseWriter, r *http.Request) { |
|
5ac549c…
|
lmata
|
349 |
var req struct { |
|
5ac549c…
|
lmata
|
350 |
Backend string `json:"backend"` |
|
5ac549c…
|
lmata
|
351 |
Prompt string `json:"prompt"` |
|
5ac549c…
|
lmata
|
352 |
} |
|
5ac549c…
|
lmata
|
353 |
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
|
5ac549c…
|
lmata
|
354 |
http.Error(w, "invalid request body", http.StatusBadRequest) |
|
5ac549c…
|
lmata
|
355 |
return |
|
5ac549c…
|
lmata
|
356 |
} |
|
5ac549c…
|
lmata
|
357 |
if req.Backend == "" || req.Prompt == "" { |
|
5ac549c…
|
lmata
|
358 |
http.Error(w, "backend and prompt are required", http.StatusBadRequest) |
|
5ac549c…
|
lmata
|
359 |
return |
|
5ac549c…
|
lmata
|
360 |
} |
|
5ac549c…
|
lmata
|
361 |
|
|
5ac549c…
|
lmata
|
362 |
cfg, ok := s.findBackendConfig(req.Backend) |
|
5ac549c…
|
lmata
|
363 |
if !ok { |
|
5ac549c…
|
lmata
|
364 |
http.Error(w, "backend not found", http.StatusNotFound) |
|
5ac549c…
|
lmata
|
365 |
return |
|
5ac549c…
|
lmata
|
366 |
} |
|
5ac549c…
|
lmata
|
367 |
|
|
5ac549c…
|
lmata
|
368 |
provider, err := llm.New(cfg) |
|
5ac549c…
|
lmata
|
369 |
if err != nil { |
|
5ac549c…
|
lmata
|
370 |
http.Error(w, "backend init failed: "+err.Error(), http.StatusInternalServerError) |
|
5ac549c…
|
lmata
|
371 |
return |
|
5ac549c…
|
lmata
|
372 |
} |
|
5ac549c…
|
lmata
|
373 |
|
|
5ac549c…
|
lmata
|
374 |
text, err := provider.Summarize(r.Context(), req.Prompt) |
|
5ac549c…
|
lmata
|
375 |
if err != nil { |
|
5ac549c…
|
lmata
|
376 |
s.log.Error("llm complete", "backend", req.Backend, "err", err) |
|
5ac549c…
|
lmata
|
377 |
http.Error(w, "llm error: "+err.Error(), http.StatusBadGateway) |
|
5ac549c…
|
lmata
|
378 |
return |
|
5ac549c…
|
lmata
|
379 |
} |
|
5ac549c…
|
lmata
|
380 |
|
|
5ac549c…
|
lmata
|
381 |
writeJSON(w, http.StatusOK, map[string]string{"text": text}) |
|
5ac549c…
|
lmata
|
382 |
} |
|
5ac549c…
|
lmata
|
383 |
|
|
5ac549c…
|
lmata
|
384 |
func policyBackendToLLM(b PolicyLLMBackend) llm.BackendConfig { |
|
5ac549c…
|
lmata
|
385 |
return llm.BackendConfig{ |
|
5ac549c…
|
lmata
|
386 |
Backend: b.Backend, |
|
5ac549c…
|
lmata
|
387 |
APIKey: b.APIKey, |
|
5ac549c…
|
lmata
|
388 |
BaseURL: b.BaseURL, |
|
5ac549c…
|
lmata
|
389 |
Model: b.Model, |
|
5ac549c…
|
lmata
|
390 |
Region: b.Region, |
|
5ac549c…
|
lmata
|
391 |
AWSKeyID: b.AWSKeyID, |
|
5ac549c…
|
lmata
|
392 |
AWSSecretKey: b.AWSSecretKey, |
|
5ac549c…
|
lmata
|
393 |
Allow: b.Allow, |
|
5ac549c…
|
lmata
|
394 |
Block: b.Block, |
|
5ac549c…
|
lmata
|
395 |
} |
|
5ac549c…
|
lmata
|
396 |
} |