mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 00:48:00 +00:00
feat: ModelRouter — unified OpenAI-compatible multi-source LLM gateway
- Lua adapters per upstream (transform_request/response/stream_chunk, build_headers signing hooks) - AUTO priority routing with per-model kind (chat/image), explicit source/model routing - Per-source concurrency caps with queueing, exponential backoff, AUTO failover - OpenAI-compatible API: chat completions, SSE streaming, image generations, models - Gateway key auth, web UI for adapter/source management, runtime persistence - e2e test running the real binary against mocked upstreams
This commit is contained in:
194
internal/core/core.go
Normal file
194
internal/core/core.go
Normal file
@ -0,0 +1,194 @@
|
||||
// Package core wires together the Lua VM, provider registry, scheduler and
|
||||
// runtime store and exposes management operations (hot reload, adapters,
|
||||
// sources) for the web UI and gateway.
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"llmsproxy/internal/config"
|
||||
"llmsproxy/internal/lua"
|
||||
"llmsproxy/internal/provider"
|
||||
"llmsproxy/internal/scheduler"
|
||||
)
|
||||
|
||||
// Core owns the running configuration and adapters.
|
||||
type Core struct {
|
||||
cfg *config.Config
|
||||
vm *lua.VM
|
||||
store *config.Store
|
||||
scheduler *scheduler.Scheduler
|
||||
registry *provider.Registry
|
||||
}
|
||||
|
||||
// New builds the core from a config file plus runtime overlay.
|
||||
func New(cfgPath string) (*Core, error) {
|
||||
cfg, err := config.Load(cfgPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewFromConfig(cfg)
|
||||
}
|
||||
|
||||
// NewFromConfig builds the core from an already-loaded config.
|
||||
func NewFromConfig(cfg *config.Config) (*Core, error) {
|
||||
c := &Core{cfg: cfg}
|
||||
c.vm = lua.NewVM(cfg.AdapterDir)
|
||||
if err := c.vm.Start(); err != nil {
|
||||
return nil, fmt.Errorf("lua vm: %w", err)
|
||||
}
|
||||
c.store = config.NewStore(cfg.RuntimeFile)
|
||||
if err := c.store.Load(); err != nil {
|
||||
return nil, fmt.Errorf("runtime store: %w", err)
|
||||
}
|
||||
c.scheduler = scheduler.New(buildRetries(cfg))
|
||||
if err := c.rebuildRegistry(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func buildRetries(cfg *config.Config) int {
|
||||
return len(cfg.Sources) // allow fallback across all sources
|
||||
}
|
||||
|
||||
// VM exposes the Lua adapter runtime.
|
||||
func (c *Core) VM() *lua.VM { return c.vm }
|
||||
|
||||
func (c *Core) Scheduler() *scheduler.Scheduler { return c.scheduler }
|
||||
|
||||
func (c *Core) Registry() *provider.Registry { return c.registry }
|
||||
|
||||
func (c *Core) DefaultModel() string { return c.cfg.DefaultModel }
|
||||
|
||||
func (c *Core) GatewayKeys() []string { return c.cfg.GatewayKeys }
|
||||
|
||||
func (c *Core) Listen() string { return c.cfg.Listen }
|
||||
|
||||
// Config exposes the underlying configuration (read-only usage).
|
||||
func (c *Core) Config() *config.Config { return c.cfg }
|
||||
|
||||
// mergedSources = base YAML sources + runtime sources (runtime wins by name).
|
||||
func (c *Core) mergedSources() []config.Source {
|
||||
byName := map[string]config.Source{}
|
||||
order := []string{}
|
||||
for _, s := range c.cfg.Sources {
|
||||
byName[s.Name] = s
|
||||
order = append(order, s.Name)
|
||||
}
|
||||
for _, s := range c.store.List() {
|
||||
if _, ok := byName[s.Name]; !ok {
|
||||
order = append(order, s.Name)
|
||||
}
|
||||
byName[s.Name] = s
|
||||
}
|
||||
out := make([]config.Source, 0, len(order))
|
||||
seen := map[string]bool{}
|
||||
for _, n := range order {
|
||||
if !seen[n] {
|
||||
seen[n] = true
|
||||
out = append(out, byName[n])
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (c *Core) rebuildRegistry() error {
|
||||
srcs := c.mergedSources()
|
||||
providers := make([]*provider.Provider, 0, len(srcs))
|
||||
for _, s := range srcs {
|
||||
providers = append(providers, provider.New(s, c.vm))
|
||||
}
|
||||
if c.registry == nil {
|
||||
c.registry = provider.NewRegistry(providers, c.cfg.DefaultModel)
|
||||
} else {
|
||||
c.registry.Replace(providers)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reload re-reads the runtime store and rebuilds sources (adapter reload is not
|
||||
// strictly needed since adapters are loaded into the VM at startup; uploaded
|
||||
// adapters are placed in the adapter dir and loaded by the web UI).
|
||||
func (c *Core) Reload() error {
|
||||
if err := c.store.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.rebuildRegistry()
|
||||
}
|
||||
|
||||
// ---- adapter management (web UI) ----
|
||||
|
||||
func (c *Core) ListAdapters() []lua.APIAdapter { return c.vm.ListAdapters() }
|
||||
|
||||
// UploadAdapter saves a new Lua adapter script to the adapter dir and loads it.
|
||||
func (c *Core) UploadAdapter(name, code string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("adapter name required")
|
||||
}
|
||||
if err := os.MkdirAll(c.cfg.AdapterDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
path := filepath.Join(c.cfg.AdapterDir, name+".lua")
|
||||
if err := os.WriteFile(path, []byte(code), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.vm.LoadAdapter(path); err != nil {
|
||||
return fmt.Errorf("load adapter: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAdapter deletes an adapter script and evicts it from the VM.
|
||||
func (c *Core) RemoveAdapter(name string) error {
|
||||
path := filepath.Join(c.cfg.AdapterDir, name+".lua")
|
||||
_ = os.Remove(path)
|
||||
c.vm.RemoveAdapter(name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- source management (web UI) ----
|
||||
|
||||
func (c *Core) AddSource(src config.Source) error {
|
||||
if err := normalizeSource(&src); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.store.Upsert(src); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.rebuildRegistry()
|
||||
}
|
||||
|
||||
func (c *Core) RemoveSource(name string) error {
|
||||
if _, err := c.store.Remove(name); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.rebuildRegistry()
|
||||
}
|
||||
|
||||
func (c *Core) Sources() []config.Source { return c.mergedSources() }
|
||||
|
||||
func normalizeSource(s *config.Source) error {
|
||||
if s.Name == "" || s.BaseURL == "" {
|
||||
return fmt.Errorf("source requires name and base_url")
|
||||
}
|
||||
if len(s.Models) == 0 {
|
||||
return fmt.Errorf("source requires at least one model")
|
||||
}
|
||||
if s.Adapter == "" {
|
||||
s.Adapter = "openai"
|
||||
}
|
||||
if s.MaxConcurrent == 0 {
|
||||
s.MaxConcurrent = 8
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close releases resources.
|
||||
func (c *Core) Close() {
|
||||
if c.vm != nil {
|
||||
c.vm.Stop()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user