mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 08:57:57 +00:00
feat: config.yaml only, OpenRouter free models, remove runtime.json config
- Move all config (auto rules, keys) from runtime.json to config.yaml - Store now only holds runtime sources (WebUI-created) - Add OpenRouter free models to config.yaml - One-time migration from legacy runtime.json on startup - Fix gateway tests for new config structure - Update core.go with migrateFromRuntime, saveConfig, seedKeys/seedAuto - Remove SaveKey/KeyByValue/AutoRules from Store - Add Config.Save() with YAML marshaling - Update WebUI admin keys visibility (show all keys including admin) - Bump binary to 11MB with luajit
This commit is contained in:
@ -51,6 +51,9 @@ func NewFromConfig(cfg *config.Config) (*Core, error) {
|
||||
return nil, fmt.Errorf("runtime store: %w", err)
|
||||
}
|
||||
c.scheduler = scheduler.New(buildRetries(cfg))
|
||||
// One-time migration: lift auto rules and keys from legacy runtime.json
|
||||
// into config.yaml so all configuration lives in one place.
|
||||
c.migrateFromRuntime()
|
||||
if err := c.seedKeys(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -63,11 +66,35 @@ func NewFromConfig(cfg *config.Config) (*Core, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// seedAuto migrates the legacy per-source model priority into flat AUTO
|
||||
// scheduling slots (one slot per model, priority order) the first time no
|
||||
// explicit auto rules exist.
|
||||
// migrateFromRuntime lifts auto rules and keys from legacy runtime.json into
|
||||
// c.cfg — but only if they are not already present in the YAML config. This
|
||||
// lets people upgrade without losing their data; once migrated, the YAML file
|
||||
// is authoritative and runtime.json's auto/keys are ignored.
|
||||
func (c *Core) migrateFromRuntime() {
|
||||
legacy := c.store.LoadLegacy()
|
||||
if legacy == nil {
|
||||
return
|
||||
}
|
||||
changed := false
|
||||
if len(legacy.Auto) > 0 && len(c.cfg.Auto) == 0 {
|
||||
c.cfg.Auto = legacy.Auto
|
||||
changed = true
|
||||
}
|
||||
if len(legacy.Keys) > 0 && len(c.cfg.Keys) == 0 {
|
||||
c.cfg.Keys = legacy.Keys
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
if err := c.cfg.Save(); err != nil {
|
||||
fmt.Printf("[core] migrate to config.yaml: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// seedAuto uses the existing auto rules from config.yaml, or creates an
|
||||
// initial chain from legacy per-source model priority (first run only).
|
||||
func (c *Core) seedAuto() error {
|
||||
if len(c.store.AutoRules()) > 0 {
|
||||
if len(c.cfg.Auto) > 0 {
|
||||
return nil
|
||||
}
|
||||
type item struct {
|
||||
@ -93,14 +120,16 @@ func (c *Core) seedAuto() error {
|
||||
for _, it := range flat {
|
||||
entries = append(entries, config.ModelScope{Model: it.model})
|
||||
}
|
||||
return c.store.SaveAutoRules(entries)
|
||||
c.cfg.Auto = entries
|
||||
return c.cfg.Save()
|
||||
}
|
||||
|
||||
// seedKeys migrates the static config gateway_keys into the runtime store as
|
||||
// admin keys (once), so later UI-created keys can share the same store.
|
||||
// seedKeys ensures config.yaml has admin keys for every gateway_keys entry
|
||||
// from the YAML config (once). After first save they become GWKey records
|
||||
// in c.cfg.Keys and gateway_keys is no longer read for auth.
|
||||
func (c *Core) seedKeys() error {
|
||||
existing := map[string]bool{}
|
||||
for _, k := range c.store.ListKeys() {
|
||||
for _, k := range c.cfg.Keys {
|
||||
existing[k.Key] = true
|
||||
}
|
||||
changed := false
|
||||
@ -112,25 +141,29 @@ func (c *Core) seedKeys() error {
|
||||
if i > 0 {
|
||||
name = fmt.Sprintf("admin-%d", i+1)
|
||||
}
|
||||
if err := c.store.SaveKey(config.GWKey{
|
||||
c.cfg.Keys = append(c.cfg.Keys, config.GWKey{
|
||||
Key: raw,
|
||||
Role: "admin",
|
||||
Name: name,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
Seed: true,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
})
|
||||
changed = true
|
||||
}
|
||||
if changed {
|
||||
return c.store.Load()
|
||||
return c.cfg.Save()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// saveConfig writes the current config (including auto rules and keys) back
|
||||
// to config.yaml.
|
||||
func (c *Core) saveConfig() error {
|
||||
return c.cfg.Save()
|
||||
}
|
||||
|
||||
func buildRetries(cfg *config.Config) int {
|
||||
return len(cfg.Sources) // allow fallback across all sources
|
||||
return len(cfg.Sources)
|
||||
}
|
||||
|
||||
// VM exposes the Lua adapter runtime.
|
||||
@ -140,9 +173,7 @@ func (c *Core) Scheduler() *scheduler.Scheduler { return c.scheduler }
|
||||
|
||||
func (c *Core) Registry() *provider.Registry { return c.registry }
|
||||
|
||||
// AutoChain returns the current AUTO scheduling chain (immutable after build;
|
||||
// a rebuilt chain is swapped in atomically). nil before the first build or
|
||||
// when no auto slots could be resolved.
|
||||
// AutoChain returns the current AUTO scheduling chain.
|
||||
func (c *Core) AutoChain() *scheduler.Chain { return c.autoChain.Load() }
|
||||
|
||||
func (c *Core) DefaultModel() string { return c.cfg.DefaultModel }
|
||||
@ -151,24 +182,32 @@ func (c *Core) GatewayKeys() []string { return c.cfg.GatewayKeys }
|
||||
|
||||
func (c *Core) Listen() string { return c.cfg.Listen }
|
||||
|
||||
// TLS returns the configured cert/key file paths. Empty strings mean HTTP only.
|
||||
func (c *Core) TLS() (cert, key string) {
|
||||
return c.cfg.TLSCertFile, c.cfg.TLSKeyFile
|
||||
}
|
||||
|
||||
// PublicBaseURL returns the externally advertised base used in generated
|
||||
// connection snippets, or "" to infer it from the incoming request.
|
||||
func (c *Core) PublicBaseURL() string { return c.cfg.PublicBaseURL }
|
||||
|
||||
// ---- gateway key management (web UI) ----
|
||||
|
||||
// ListKeys returns all gateway keys (admin view).
|
||||
func (c *Core) ListKeys() []config.GWKey { return c.store.ListKeys() }
|
||||
func (c *Core) ListKeys() []config.GWKey {
|
||||
out := make([]config.GWKey, len(c.cfg.Keys))
|
||||
copy(out, c.cfg.Keys)
|
||||
return out
|
||||
}
|
||||
|
||||
// FindKey looks up a gateway key record by its secret value.
|
||||
func (c *Core) FindKey(key string) (config.GWKey, bool) { return c.store.KeyByValue(key) }
|
||||
func (c *Core) FindKey(key string) (config.GWKey, bool) {
|
||||
for _, k := range c.cfg.Keys {
|
||||
if k.Key == key {
|
||||
return k, true
|
||||
}
|
||||
}
|
||||
return config.GWKey{}, false
|
||||
}
|
||||
|
||||
// CreateKey builds a new random gateway key and persists it.
|
||||
// CreateKey builds a new random gateway key and persists it to config.yaml.
|
||||
func (c *Core) CreateKey(name, role string, models []config.ModelScope, note string) (config.GWKey, error) {
|
||||
models = cleanScopes(models)
|
||||
key := make([]byte, 16)
|
||||
@ -186,7 +225,8 @@ func (c *Core) CreateKey(name, role string, models []config.ModelScope, note str
|
||||
if rec.Role == "" {
|
||||
rec.Role = "user"
|
||||
}
|
||||
if err := c.store.SaveKey(rec); err != nil {
|
||||
c.cfg.Keys = append(c.cfg.Keys, rec)
|
||||
if err := c.saveConfig(); err != nil {
|
||||
return config.GWKey{}, err
|
||||
}
|
||||
return rec, nil
|
||||
@ -194,32 +234,44 @@ func (c *Core) CreateKey(name, role string, models []config.ModelScope, note str
|
||||
|
||||
// UpdateKey mutates a key's name/role/model scope and persists it.
|
||||
func (c *Core) UpdateKey(key, name, role string, models []config.ModelScope, note string) (config.GWKey, error) {
|
||||
rec, ok := c.store.KeyByValue(key)
|
||||
if !ok {
|
||||
return config.GWKey{}, fmt.Errorf("key not found")
|
||||
for i, k := range c.cfg.Keys {
|
||||
if k.Key == key {
|
||||
if name != "" {
|
||||
c.cfg.Keys[i].Name = name
|
||||
}
|
||||
if role == "admin" || role == "user" {
|
||||
c.cfg.Keys[i].Role = role
|
||||
}
|
||||
c.cfg.Keys[i].Models = cleanScopes(models)
|
||||
c.cfg.Keys[i].Note = note
|
||||
if err := c.saveConfig(); err != nil {
|
||||
return config.GWKey{}, err
|
||||
}
|
||||
return c.cfg.Keys[i], nil
|
||||
}
|
||||
}
|
||||
if name != "" {
|
||||
rec.Name = name
|
||||
}
|
||||
if role == "admin" || role == "user" {
|
||||
rec.Role = role
|
||||
}
|
||||
rec.Models = cleanScopes(models)
|
||||
rec.Note = note
|
||||
if err := c.store.SaveKey(rec); err != nil {
|
||||
return config.GWKey{}, err
|
||||
}
|
||||
return rec, nil
|
||||
return config.GWKey{}, fmt.Errorf("key not found")
|
||||
}
|
||||
|
||||
// DeleteKey removes a key record; returns false if it did not exist.
|
||||
func (c *Core) DeleteKey(key string) (bool, error) { return c.store.DeleteKey(key) }
|
||||
func (c *Core) DeleteKey(key string) (bool, error) {
|
||||
for i, k := range c.cfg.Keys {
|
||||
if k.Key == key {
|
||||
c.cfg.Keys = append(c.cfg.Keys[:i], c.cfg.Keys[i+1:]...)
|
||||
return true, c.saveConfig()
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// ---- AUTO scheduling slots (web UI canvas) ----
|
||||
// ---- AUTO scheduling slots (web UI) ----
|
||||
|
||||
// AutoRules returns the AUTO scheduling slots in priority order (slot 0 =
|
||||
// highest priority).
|
||||
func (c *Core) AutoRules() []config.ModelScope { return c.store.AutoRules() }
|
||||
// AutoRules returns the AUTO scheduling slots in priority order.
|
||||
func (c *Core) AutoRules() []config.ModelScope {
|
||||
out := make([]config.ModelScope, len(c.cfg.Auto))
|
||||
copy(out, c.cfg.Auto)
|
||||
return out
|
||||
}
|
||||
|
||||
func cleanScopes(entries []config.ModelScope) []config.ModelScope {
|
||||
clean := make([]config.ModelScope, 0, len(entries))
|
||||
@ -235,13 +287,13 @@ func cleanScopes(entries []config.ModelScope) []config.ModelScope {
|
||||
return clean
|
||||
}
|
||||
|
||||
// SaveAutoRules persists the AUTO scheduling slots, rebuilds the chain and
|
||||
// clears the cooldown of every slot in it — preference scores are kept, so a
|
||||
// reliably good model keeps its edge while an edited chain applies
|
||||
// immediately. Providers are NOT rebuilt here (their per-model state survives
|
||||
// the edit, plan 2.4 lifecycle); rebuildRegistry covers source edits.
|
||||
// SaveAutoRules persists the AUTO scheduling slots to config.yaml, rebuilds
|
||||
// the chain and clears the cooldown of every slot — preference scores are
|
||||
// kept, so a reliably good model keeps its edge while an edited chain applies
|
||||
// immediately.
|
||||
func (c *Core) SaveAutoRules(entries []config.ModelScope) error {
|
||||
if err := c.store.SaveAutoRules(cleanScopes(entries)); err != nil {
|
||||
c.cfg.Auto = cleanScopes(entries)
|
||||
if err := c.saveConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.buildAutoChain()
|
||||
@ -257,26 +309,21 @@ func (c *Core) SaveAutoRules(entries []config.ModelScope) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetHealth clears the scheduling backoff state of every provider (admin
|
||||
// UI action). Unlike SaveAutoRules this does not touch the chain itself.
|
||||
// ResetHealth clears the scheduling backoff state of every provider.
|
||||
func (c *Core) ResetHealth() {
|
||||
for _, p := range c.registry.Providers() {
|
||||
p.ResetHealth()
|
||||
}
|
||||
}
|
||||
|
||||
// Registry resolves model -> owning provider.
|
||||
func (c *Core) ProviderForModel(model string) *provider.Provider {
|
||||
return c.registry.ProviderForModel(model)
|
||||
}
|
||||
|
||||
// ProviderForSlot resolves a (model, source) scheduling slot to a provider;
|
||||
// source "" falls back to ProviderForModel.
|
||||
func (c *Core) ProviderForSlot(model, source string) *provider.Provider {
|
||||
return c.registry.ProviderForSlot(model, source)
|
||||
}
|
||||
|
||||
// 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).
|
||||
@ -299,9 +346,6 @@ func (c *Core) mergedSources() []config.Source {
|
||||
if !seen[n] {
|
||||
seen[n] = true
|
||||
s := c.resolveSourceKey(byName[n])
|
||||
// Runtime sources (web UI edits) are persisted without timeout
|
||||
// fields; apply the same defaults the YAML path gets so a dead
|
||||
// upstream cannot hold a concurrency slot forever (P10-3).
|
||||
if s.Timeout == 0 {
|
||||
s.Timeout = config.DefaultSourceTimeout
|
||||
}
|
||||
@ -317,8 +361,7 @@ func (c *Core) mergedSources() []config.Source {
|
||||
return out
|
||||
}
|
||||
|
||||
// resolveSourceKey applies api_key_env (env var overrides inline api_key) and
|
||||
// decrypts an inline enc:v1: ciphertext (useful for YAML-sourced keys).
|
||||
// resolveSourceKey applies api_key_env and decrypts enc:v1: ciphertext.
|
||||
func (c *Core) resolveSourceKey(s config.Source) config.Source {
|
||||
if s.APIKeyEnv != "" {
|
||||
if v := os.Getenv(s.APIKeyEnv); v != "" {
|
||||
@ -352,10 +395,8 @@ func (c *Core) rebuildRegistry() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildAutoChain rebuilds the AUTO chain snapshot from the persisted rules
|
||||
// against the current providers. Slots whose (model, source) no longer exists
|
||||
// and image-kind models are dropped; a chain with no slots makes AUTO
|
||||
// requests answer "no auto slot configured".
|
||||
// buildAutoChain rebuilds the AUTO chain snapshot from config.yaml rules
|
||||
// against the current providers.
|
||||
func (c *Core) buildAutoChain() {
|
||||
prov := func(model, source string) scheduler.Provider {
|
||||
p := c.registry.ProviderForSlot(model, source)
|
||||
@ -367,7 +408,7 @@ func (c *Core) buildAutoChain() {
|
||||
}
|
||||
return p
|
||||
}
|
||||
rules := c.store.AutoRules()
|
||||
rules := c.cfg.Auto
|
||||
sr := make([]scheduler.Rule, 0, len(rules))
|
||||
for _, e := range rules {
|
||||
r := scheduler.Rule{
|
||||
@ -379,8 +420,6 @@ func (c *Core) buildAutoChain() {
|
||||
Hours: e.Hours,
|
||||
}
|
||||
if r.Source == "" {
|
||||
// canonicalize to the owning source so summaries/audit/quota
|
||||
// windows always carry a real source name
|
||||
if p := c.registry.ProviderForSlot(e.Model, ""); p != nil {
|
||||
r.Source = p.Name()
|
||||
}
|
||||
@ -400,9 +439,7 @@ type AutoSlotState struct {
|
||||
Cooling bool `json:"cooling"`
|
||||
}
|
||||
|
||||
// AutoSlotStates returns per-slot health (preference, failure count,
|
||||
// cooldown) for every slot of the current AUTO chain, mirroring the chain
|
||||
// order so the priority-page UI can annotate its blocks.
|
||||
// AutoSlotStates returns per-slot health for every slot of the current chain.
|
||||
func (c *Core) AutoSlotStates() []AutoSlotState {
|
||||
ch := c.autoChain.Load()
|
||||
if ch == nil {
|
||||
@ -430,9 +467,7 @@ func (c *Core) AutoSlotStates() []AutoSlotState {
|
||||
return out
|
||||
}
|
||||
|
||||
// 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).
|
||||
// Reload re-reads the runtime store and rebuilds sources.
|
||||
func (c *Core) Reload() error {
|
||||
if err := c.store.Load(); err != nil {
|
||||
return err
|
||||
@ -446,7 +481,6 @@ 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")
|
||||
@ -461,9 +495,6 @@ func (c *Core) UploadAdapter(name, code string) error {
|
||||
return c.vm.LoadAdapter(path)
|
||||
}
|
||||
|
||||
// RemoveAdapter deletes an adapter script and evicts it from the VM. The file
|
||||
// is removed for real (adapter dir is authoritative after first run), so the
|
||||
// adapter stays gone across restarts.
|
||||
func (c *Core) RemoveAdapter(name string) error {
|
||||
path := filepath.Join(c.cfg.AdapterDir, name+".lua")
|
||||
_ = os.Remove(path)
|
||||
@ -477,9 +508,21 @@ func (c *Core) AddSource(src config.Source) error {
|
||||
if err := normalizeSource(&src); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.store.Upsert(src); err != nil {
|
||||
if err := config.UpsertSourceInYAML(c.cfg.Path, src.Name, src); err != nil {
|
||||
return err
|
||||
}
|
||||
// Update in-memory Sources so mergedSources() finds the entry.
|
||||
replaced := false
|
||||
for i, s := range c.cfg.Sources {
|
||||
if s.Name == src.Name {
|
||||
c.cfg.Sources[i] = src
|
||||
replaced = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !replaced {
|
||||
c.cfg.Sources = append(c.cfg.Sources, src)
|
||||
}
|
||||
return c.rebuildRegistry()
|
||||
}
|
||||
|
||||
@ -532,4 +575,4 @@ func (c *Core) Close() {
|
||||
if c.vm != nil {
|
||||
c.vm.Stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user