mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-21 09:28:00 +00:00
feat: opencode zen adapter + first-run config generation, fix stats/stream bugs
- adapters/opencode.lua: opencode.ai zen free pool adapter — sends the opencode client User-Agent (zen fingerprints clients by UA; non-official clients hit FreeUsageLimitError); pairs with api_key: public - config: no config file ships in the repo; first run generates a default config at the -config path with a random admin key, loopback listen and a keyless zen source (config.EnsureDefault); remove config.example.yaml - lua: seed bundled adapters from the embedded FS instead of a hardcoded name list - ui: widen model kind select (chat was clipped to 'cha') - phase 5 bugfixes: stats ms/s bucket mixing, cleanScopes nil, ctx.Err guards, direct-path ModelAvailable, empty stream body failure, bestImageModel rewrite, transform failure recording, Core.mu, timer, effective model for tool-calls
This commit is contained in:
@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
@ -63,8 +64,13 @@ type Source struct {
|
||||
QueueTimeout time.Duration `yaml:"queue_timeout" json:"-"`
|
||||
}
|
||||
|
||||
// Load reads and validates a config file.
|
||||
// Load reads and validates a config file. When the file does not exist yet a
|
||||
// default config is generated at that path first (first-run bootstrap), so a
|
||||
// fresh binary just works: `llmsproxy -config /path/to/config.yaml`.
|
||||
func Load(path string) (*Config, error) {
|
||||
if _, err := EnsureDefault(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -80,6 +86,63 @@ func Load(path string) (*Config, error) {
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// EnsureDefault creates a default config file at path when it does not exist
|
||||
// yet and returns whether it was created. The repo ships no config file
|
||||
// (config files carry real keys); the binary generates one per install with a
|
||||
// fresh random admin key. An existing file is never touched.
|
||||
func EnsureDefault(path string) (bool, error) {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return false, nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return false, err
|
||||
}
|
||||
if err := writeDefaultConfig(path); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// writeDefaultConfig writes a minimal, safe-by-default config: loopback-only
|
||||
// listen, a fresh random admin key, and a no-key zen source that works out of
|
||||
// the box. adapter_dir / runtime_file live next to the config file so the
|
||||
// binary works regardless of the working directory it is started from.
|
||||
func writeDefaultConfig(path string) error {
|
||||
key, err := NewGatewayKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate gateway key: %w", err)
|
||||
}
|
||||
dir := filepath.Dir(path)
|
||||
abs, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
abs = dir
|
||||
}
|
||||
cfg := Config{
|
||||
Listen: "127.0.0.1:8080",
|
||||
GatewayKeys: []string{key},
|
||||
DefaultModel: "AUTO",
|
||||
AdapterDir: filepath.Join(abs, "adapters"),
|
||||
RuntimeFile: filepath.Join(abs, "runtime.json"),
|
||||
Sources: []Source{
|
||||
{
|
||||
Name: "zen",
|
||||
BaseURL: "https://opencode.ai/zen/v1",
|
||||
APIKey: "public", // zen 免费池:官方无 key 客户端实际发送 Bearer public
|
||||
Adapter: "opencode",
|
||||
Models: []Model{{ID: "deepseek-v4-flash-free", Priority: 100, Kind: "chat"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
out, err := yaml.Marshal(&cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal default config: %w", err)
|
||||
}
|
||||
// The config holds the plaintext admin key — restrict permissions.
|
||||
if err := os.MkdirAll(abs, 0755); err != nil {
|
||||
return fmt.Errorf("mkdir config dir: %w", err)
|
||||
}
|
||||
return os.WriteFile(path, out, 0600)
|
||||
}
|
||||
|
||||
// RemoveSourceFromYAML deletes the named source entry from the config file so
|
||||
// the delete is a real one (no tombstone needed). Uses yaml.Node to preserve
|
||||
// the rest of the file's comments and formatting.
|
||||
|
||||
Reference in New Issue
Block a user