feat(admin): add model config CRUD and platform bridge pages
- Backend: ModelConfigStore with Provider/Model/Routing CRUD - Backend: /api/v1/admin/models/config endpoints - Frontend: ModelsPage with 3-tab editor (providers/models/routing) - Frontend: BridgePage showing adapter status + group chat rules - Admin nav expanded to 9 items
This commit is contained in:
1 parent
1a80ac2a00
commit
8420691438
7 files changed
+422
-1
No files matched your search
@@ -0,0 +1,135 @@
|
||||
// Package api — ModelConfigStore manages LLM provider/model/routing configuration.
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ModelProvider represents an LLM API provider.
|
||||
type ModelProvider struct {
|
||||
Name string `json:"name"`
|
||||
BaseURL string `json:"base_url"`
|
||||
APIKey string `json:"api_key,omitempty"`
|
||||
Timeout int `json:"timeout_sec"`
|
||||
Retries int `json:"max_retries"`
|
||||
}
|
||||
|
||||
// ModelEntry represents a configured LLM model.
|
||||
type ModelEntry struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags"`
|
||||
Params map[string]any `json:"params"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// RoutingRule defines fallback chain for a purpose.
|
||||
type RoutingRule struct {
|
||||
Purpose string `json:"purpose"`
|
||||
FallbackChain []string `json:"fallback_chain"`
|
||||
Required bool `json:"required"`
|
||||
}
|
||||
|
||||
// ModelConfigStore is an in-memory store for model configuration.
|
||||
type ModelConfigStore struct {
|
||||
mu sync.RWMutex
|
||||
providers map[string]*ModelProvider
|
||||
models map[string]*ModelEntry
|
||||
routing map[string]*RoutingRule
|
||||
}
|
||||
|
||||
// NewModelConfigStore creates a store with sensible defaults.
|
||||
func NewModelConfigStore() *ModelConfigStore {
|
||||
s := &ModelConfigStore{
|
||||
providers: make(map[string]*ModelProvider),
|
||||
models: make(map[string]*ModelEntry),
|
||||
routing: make(map[string]*RoutingRule),
|
||||
}
|
||||
|
||||
// Default provider
|
||||
s.providers["openai"] = &ModelProvider{
|
||||
Name: "openai", BaseURL: "https://api.openai.com/v1",
|
||||
Timeout: 120, Retries: 3,
|
||||
}
|
||||
// Default models
|
||||
s.models["primary"] = &ModelEntry{
|
||||
ID: "primary", Name: "gpt-4o", Provider: "openai",
|
||||
Description: "主对话模型", Tags: []string{"chat"},
|
||||
Params: map[string]any{"temperature": 0.8}, Enabled: true,
|
||||
}
|
||||
s.models["fallback"] = &ModelEntry{
|
||||
ID: "fallback", Name: "gpt-4o-mini", Provider: "openai",
|
||||
Description: "备用模型", Tags: []string{"chat"},
|
||||
Params: map[string]any{"temperature": 0.8}, Enabled: true,
|
||||
}
|
||||
// Default routing
|
||||
s.routing["chat"] = &RoutingRule{
|
||||
Purpose: "chat", FallbackChain: []string{"primary", "fallback"}, Required: true,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *ModelConfigStore) GetProviders() []*ModelProvider {
|
||||
s.mu.RLock(); defer s.mu.RUnlock()
|
||||
result := make([]*ModelProvider, 0, len(s.providers))
|
||||
for _, p := range s.providers { result = append(result, p) }
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *ModelConfigStore) GetModels() []*ModelEntry {
|
||||
s.mu.RLock(); defer s.mu.RUnlock()
|
||||
result := make([]*ModelEntry, 0, len(s.models))
|
||||
for _, m := range s.models { result = append(result, m) }
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *ModelConfigStore) GetRouting() []*RoutingRule {
|
||||
s.mu.RLock(); defer s.mu.RUnlock()
|
||||
result := make([]*RoutingRule, 0, len(s.routing))
|
||||
for _, r := range s.routing { result = append(result, r) }
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *ModelConfigStore) SaveProvider(p *ModelProvider) {
|
||||
s.mu.Lock(); defer s.mu.Unlock()
|
||||
s.providers[p.Name] = p
|
||||
}
|
||||
|
||||
func (s *ModelConfigStore) SaveModel(m *ModelEntry) {
|
||||
s.mu.Lock(); defer s.mu.Unlock()
|
||||
s.models[m.ID] = m
|
||||
}
|
||||
|
||||
func (s *ModelConfigStore) SaveRouting(r *RoutingRule) {
|
||||
s.mu.Lock(); defer s.mu.Unlock()
|
||||
s.routing[r.Purpose] = r
|
||||
}
|
||||
|
||||
func (s *ModelConfigStore) DeleteProvider(name string) {
|
||||
s.mu.Lock(); defer s.mu.Unlock()
|
||||
delete(s.providers, name)
|
||||
}
|
||||
|
||||
func (s *ModelConfigStore) DeleteModel(id string) {
|
||||
s.mu.Lock(); defer s.mu.Unlock()
|
||||
delete(s.models, id)
|
||||
}
|
||||
|
||||
func (s *ModelConfigStore) DeleteRouting(purpose string) {
|
||||
s.mu.Lock(); defer s.mu.Unlock()
|
||||
delete(s.routing, purpose)
|
||||
}
|
||||
|
||||
// Import/Export for models.json compatibility
|
||||
func (s *ModelConfigStore) ExportJSON() ([]byte, error) {
|
||||
s.mu.RLock(); defer s.mu.RUnlock()
|
||||
export := map[string]any{
|
||||
"providers": s.providers,
|
||||
"models": s.models,
|
||||
"routing": s.routing,
|
||||
}
|
||||
return json.MarshalIndent(export, "", " ")
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -73,6 +74,7 @@ type Dependencies struct {
|
||||
Orchestrator OrchestratorPipeline
|
||||
WSHub WebSocketHub
|
||||
LLMLogger LLMCallLogger
|
||||
ModelConfig *ModelConfigStore
|
||||
PluginList []map[string]string
|
||||
}
|
||||
|
||||
@@ -114,6 +116,8 @@ func (r *Router) setupRoutes() {
|
||||
r.mux.HandleFunc("/api/v1/admin/system", r.authMW(r.adminMW(r.systemInfoHandler)))
|
||||
r.mux.HandleFunc("/api/v1/admin/plugins", r.authMW(r.adminMW(r.pluginListHandler)))
|
||||
r.mux.HandleFunc("/api/v1/admin/sessions", r.authMW(r.adminMW(r.adminSessionsHandler)))
|
||||
r.mux.HandleFunc("/api/v1/admin/models/config", r.authMW(r.adminMW(r.modelConfigHandler)))
|
||||
r.mux.HandleFunc("/api/v1/admin/models/config/", r.authMW(r.adminMW(r.modelConfigItemHandler)))
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
@@ -392,7 +396,94 @@ func (r *Router) pluginListHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
}
|
||||
|
||||
func (r *Router) adminSessionsHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(w, 200, M{"sessions": []any{}, "total": r.deps.WSHub.Count()})
|
||||
writeJSON(w, 200, M{"sessions": []any{}, "total": 0})
|
||||
}
|
||||
|
||||
// --- Model Config ---
|
||||
|
||||
func (r *Router) modelConfigHandler(w http.ResponseWriter, req *http.Request) {
|
||||
if r.deps.ModelConfig == nil {
|
||||
writeJSON(w, 503, M{"error": "model config unavailable"})
|
||||
return
|
||||
}
|
||||
switch req.Method {
|
||||
case "GET":
|
||||
writeJSON(w, 200, M{
|
||||
"providers": r.deps.ModelConfig.GetProviders(),
|
||||
"models": r.deps.ModelConfig.GetModels(),
|
||||
"routing": r.deps.ModelConfig.GetRouting(),
|
||||
})
|
||||
default:
|
||||
writeJSON(w, 405, M{"error": "method not allowed"})
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) modelConfigItemHandler(w http.ResponseWriter, req *http.Request) {
|
||||
if r.deps.ModelConfig == nil {
|
||||
writeJSON(w, 503, M{"error": "model config unavailable"})
|
||||
return
|
||||
}
|
||||
// Path: /api/v1/admin/models/config/{type}/{name}
|
||||
path := strings.TrimPrefix(req.URL.Path, "/api/v1/admin/models/config/")
|
||||
parts := strings.SplitN(path, "/", 2)
|
||||
if len(parts) < 2 {
|
||||
writeJSON(w, 400, M{"error": "invalid path"})
|
||||
return
|
||||
}
|
||||
kind, name := parts[0], parts[1]
|
||||
|
||||
switch req.Method {
|
||||
case "POST", "PUT":
|
||||
var body map[string]any
|
||||
json.NewDecoder(req.Body).Decode(&body)
|
||||
switch kind {
|
||||
case "providers":
|
||||
r.deps.ModelConfig.SaveProvider(&ModelProvider{
|
||||
Name: name, BaseURL: str(body, "base_url"),
|
||||
APIKey: str(body, "api_key"), Timeout: num(body, "timeout_sec"),
|
||||
Retries: num(body, "max_retries"),
|
||||
})
|
||||
case "models":
|
||||
var tags []string
|
||||
if t, ok := body["tags"].([]any); ok {
|
||||
for _, v := range t { tags = append(tags, fmt.Sprint(v)) }
|
||||
}
|
||||
r.deps.ModelConfig.SaveModel(&ModelEntry{
|
||||
ID: name, Name: str(body, "name"), Provider: str(body, "provider"),
|
||||
Description: str(body, "description"), Tags: tags,
|
||||
Params: body["params"].(map[string]any), Enabled: true,
|
||||
})
|
||||
case "routing":
|
||||
var chain []string
|
||||
if c, ok := body["fallback_chain"].([]any); ok {
|
||||
for _, v := range c { chain = append(chain, fmt.Sprint(v)) }
|
||||
}
|
||||
r.deps.ModelConfig.SaveRouting(&RoutingRule{
|
||||
Purpose: name, FallbackChain: chain, Required: body["required"].(bool),
|
||||
})
|
||||
}
|
||||
writeJSON(w, 200, M{"ok": true})
|
||||
|
||||
case "DELETE":
|
||||
switch kind {
|
||||
case "providers": r.deps.ModelConfig.DeleteProvider(name)
|
||||
case "models": r.deps.ModelConfig.DeleteModel(name)
|
||||
case "routing": r.deps.ModelConfig.DeleteRouting(name)
|
||||
}
|
||||
writeJSON(w, 204, nil)
|
||||
|
||||
default:
|
||||
writeJSON(w, 405, M{"error": "method not allowed"})
|
||||
}
|
||||
}
|
||||
|
||||
func str(m map[string]any, key string) string {
|
||||
if v, ok := m[key].(string); ok { return v }
|
||||
return ""
|
||||
}
|
||||
func num(m map[string]any, key string) int {
|
||||
if v, ok := m[key].(float64); ok { return int(v) }
|
||||
return 0
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
Reference in new issue
Block a user