feat: provider presets (openai, openrouter, ollama, dahl, gonkarouter, custom), /api/info, provider display in Android settings
This commit is contained in:
parent
d2ec0848c8
commit
7b92e259ad
14 changed files with 233 additions and 18 deletions
|
|
@ -13,6 +13,8 @@ import (
|
|||
)
|
||||
|
||||
type LLM struct {
|
||||
// Provider selects a preset: openai, openrouter, ollama, dahl, gonkarouter or custom.
|
||||
Provider string `yaml:"provider" json:"provider"`
|
||||
BaseURL string `yaml:"base_url" json:"base_url"`
|
||||
APIKey string `yaml:"api_key" json:"api_key"`
|
||||
Model string `yaml:"model" json:"model"`
|
||||
|
|
@ -61,8 +63,7 @@ func Default() Config {
|
|||
Listen: "127.0.0.1:8787",
|
||||
DataDir: "./data",
|
||||
LLM: LLM{
|
||||
BaseURL: "https://api.openai.com/v1",
|
||||
Model: "gpt-4o-mini",
|
||||
Provider: "openai",
|
||||
TimeoutSec: 120,
|
||||
Temperature: 0.7,
|
||||
},
|
||||
|
|
@ -98,6 +99,9 @@ func Load(path string) (Config, error) {
|
|||
}
|
||||
}
|
||||
applyEnv(&cfg, os.Getenv)
|
||||
if err := applyPreset(&cfg.LLM, os.Getenv); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
if cfg.Workspace == "" {
|
||||
cfg.Workspace = filepath.Join(cfg.DataDir, "workspace")
|
||||
}
|
||||
|
|
@ -114,6 +118,7 @@ func applyEnv(c *Config, get func(string) string) {
|
|||
set("BYTECLAW_AUTH_TOKEN", &c.AuthToken)
|
||||
set("BYTECLAW_DATA_DIR", &c.DataDir)
|
||||
set("BYTECLAW_WORKSPACE", &c.Workspace)
|
||||
set("BYTECLAW_LLM_PROVIDER", &c.LLM.Provider)
|
||||
set("BYTECLAW_LLM_BASE_URL", &c.LLM.BaseURL)
|
||||
set("BYTECLAW_LLM_API_KEY", &c.LLM.APIKey)
|
||||
set("BYTECLAW_LLM_MODEL", &c.LLM.Model)
|
||||
|
|
|
|||
66
internal/config/presets.go
Normal file
66
internal/config/presets.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Preset describes a known OpenAI-compatible provider. All presets use
|
||||
// "Authorization: Bearer <api_key>" against <base_url>/chat/completions.
|
||||
type Preset struct {
|
||||
Name string
|
||||
BaseURL string
|
||||
DefaultModel string
|
||||
// KeyEnv is an additional env var read for the API key if none is configured.
|
||||
KeyEnv string
|
||||
Source string
|
||||
}
|
||||
|
||||
// Presets: values verified against provider docs (see Source) on 2026-10-25.
|
||||
var Presets = map[string]Preset{
|
||||
"openai": {Name: "openai", BaseURL: "https://api.openai.com/v1", DefaultModel: "gpt-4o-mini", KeyEnv: "OPENAI_API_KEY", Source: "https://platform.openai.com/docs/api-reference"},
|
||||
"openrouter": {Name: "openrouter", BaseURL: "https://openrouter.ai/api/v1", DefaultModel: "openai/gpt-4o-mini", KeyEnv: "OPENROUTER_API_KEY", Source: "https://openrouter.ai/docs"},
|
||||
"ollama": {Name: "ollama", BaseURL: "http://localhost:11434/v1", DefaultModel: "llama3.1", Source: "https://github.com/ollama/ollama/blob/main/docs/openai.md"},
|
||||
// Dahl: OpenAI-compatible inference on the Gonka network.
|
||||
"dahl": {Name: "dahl", BaseURL: "https://inference.dahl.global/v1", DefaultModel: "MiniMaxAI/MiniMax-M2.7", KeyEnv: "DAHL_API_KEY", Source: "https://docs.dahl.global/"},
|
||||
// GonkaRouter: OpenAI-/Anthropic-compatible router on the Gonka network.
|
||||
"gonkarouter": {Name: "gonkarouter", BaseURL: "https://api.gonkarouter.io/v1", DefaultModel: "MiniMaxAI/MiniMax-M2.7", KeyEnv: "GONKAROUTER_API_KEY", Source: "https://gonkarouter.io/docs"},
|
||||
}
|
||||
|
||||
// PresetNames returns known provider names plus "custom".
|
||||
func PresetNames() []string {
|
||||
out := []string{"custom"}
|
||||
for k := range Presets {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// applyPreset fills empty base_url/model/api_key from the selected provider preset.
|
||||
// Explicit config values always win. "custom" requires base_url and model.
|
||||
func applyPreset(l *LLM, get func(string) string) error {
|
||||
name := strings.ToLower(strings.TrimSpace(l.Provider))
|
||||
if name == "" {
|
||||
name = "openai"
|
||||
}
|
||||
l.Provider = name
|
||||
if name == "custom" {
|
||||
return nil
|
||||
}
|
||||
p, ok := Presets[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown llm.provider %q (known: %s)", name, strings.Join(PresetNames(), ", "))
|
||||
}
|
||||
if l.BaseURL == "" {
|
||||
l.BaseURL = p.BaseURL
|
||||
}
|
||||
if l.Model == "" {
|
||||
l.Model = p.DefaultModel
|
||||
}
|
||||
if l.APIKey == "" && p.KeyEnv != "" {
|
||||
l.APIKey = get(p.KeyEnv)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
76
internal/config/presets_test.go
Normal file
76
internal/config/presets_test.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func load(t *testing.T, yaml string) (Config, error) {
|
||||
t.Helper()
|
||||
p := filepath.Join(t.TempDir(), "c.yaml")
|
||||
os.WriteFile(p, []byte("auth_token: aaaaaaaaaaaaaaaaaaaa\n"+yaml), 0o600)
|
||||
return Load(p)
|
||||
}
|
||||
|
||||
func TestPresetDahl(t *testing.T) {
|
||||
t.Setenv("DAHL_API_KEY", "dahl_test")
|
||||
c, err := load(t, "llm:\n provider: dahl\n")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.LLM.BaseURL != "https://inference.dahl.global/v1" || c.LLM.Model != "MiniMaxAI/MiniMax-M2.7" || c.LLM.APIKey != "dahl_test" {
|
||||
t.Fatalf("%+v", c.LLM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPresetGonkaRouterViaEnv(t *testing.T) {
|
||||
t.Setenv("BYTECLAW_LLM_PROVIDER", "GonkaRouter")
|
||||
t.Setenv("GONKAROUTER_API_KEY", "sk-g")
|
||||
c, err := load(t, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.LLM.Provider != "gonkarouter" || c.LLM.BaseURL != "https://api.gonkarouter.io/v1" || c.LLM.Model != "MiniMaxAI/MiniMax-M2.7" || c.LLM.APIKey != "sk-g" {
|
||||
t.Fatalf("%+v", c.LLM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPresetExplicitValuesWin(t *testing.T) {
|
||||
t.Setenv("GONKAROUTER_API_KEY", "from-env")
|
||||
c, err := load(t, "llm:\n provider: gonkarouter\n model: zai-org/GLM-5.3-Flash\n api_key: explicit\n")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.LLM.Model != "zai-org/GLM-5.3-Flash" || c.LLM.APIKey != "explicit" || c.LLM.BaseURL != "https://api.gonkarouter.io/v1" {
|
||||
t.Fatalf("%+v", c.LLM)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPresetDefaultIsOpenAI(t *testing.T) {
|
||||
c, err := load(t, "")
|
||||
if err != nil || c.LLM.Provider != "openai" || c.LLM.BaseURL != "https://api.openai.com/v1" {
|
||||
t.Fatalf("%+v %v", c.LLM, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPresetUnknownAndCustom(t *testing.T) {
|
||||
if _, err := load(t, "llm:\n provider: nope\n"); err == nil {
|
||||
t.Fatal("unknown provider must fail")
|
||||
}
|
||||
if _, err := load(t, "llm:\n provider: custom\n"); err == nil {
|
||||
t.Fatal("custom without base_url/model must fail validation")
|
||||
}
|
||||
c, err := load(t, "llm:\n provider: custom\n base_url: http://x/v1\n model: m\n")
|
||||
if err != nil || c.LLM.BaseURL != "http://x/v1" {
|
||||
t.Fatalf("%+v %v", c.LLM, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllPresetsComplete(t *testing.T) {
|
||||
for n, p := range Presets {
|
||||
if p.Name != n || p.BaseURL == "" || p.DefaultModel == "" || p.Source == "" {
|
||||
t.Errorf("incomplete preset %q: %+v", n, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue