config.go

v0.7.0
Doc Versions Source
1
package config
2
3
import (
4
	"errors"
5
	"fmt"
6
	"os"
7
	"path/filepath"
8
9
	"gopkg.in/yaml.v3"
10
	"sourcecraft.dev/bigbes/claudio/provider"
11
)
12
13
// providerKeys lists all known provider identifiers for seeding default config.
14
var providerKeys = []string{"deepseek", "zai", "zai-coding", "minimax", "kimi", "kimi-api-cn", "kimi-api-intl", "copilot", "nvidia", "openrouter", "ollama", "ollama-cloud", "lmstudio", "vkproxy"}
15
16
const appName = "claudio"
17
18
// ProviderConfig holds per-provider user settings.
19
// For built-in providers only api_key is needed.
20
// For custom providers, set base_url + model + compat to define one entirely in YAML.
21
type ProviderConfig struct {
22
	APIKey        string            `yaml:"api_key"`
23
	Name          string            `yaml:"name,omitempty"`
24
	Description   string            `yaml:"description,omitempty"`
25
	BaseURL       string            `yaml:"base_url,omitempty"`
26
	Model         string            `yaml:"model,omitempty"`
27
	SmallModel    string            `yaml:"small_model,omitempty"`
28
	Compat        string            `yaml:"compat,omitempty"`         // "anthropic" or "openai"
29
	Proxy         string            `yaml:"proxy,omitempty"`          // e.g. "socks5://127.0.0.1:1080"
30
	ClaudeProxy   string            `yaml:"claude_proxy,omitempty"`   // HTTP(S) proxy for Claude Code (it doesn't support SOCKS5)
31
	Plan          string            `yaml:"plan,omitempty"`           // plan key for providers with multiple plans (e.g. "api", "coding")
32
	ContextWindow int               `yaml:"context_window,omitempty"` // model context window in tokens
33
	Env           map[string]string `yaml:"env,omitempty"`            // extra env vars to set at launch
34
	Hidden        bool              `yaml:"hidden,omitempty"`         // hide from the main menu
35
}
36
37
// Config is the top-level configuration.
38
type Config struct {
39
	Schema         string                    `yaml:"$schema,omitempty"`
40
	ActiveProvider string                    `yaml:"active_provider"`
41
	Proxy          string                    `yaml:"proxy,omitempty"`        // global proxy for upstream requests, e.g. "socks5://127.0.0.1:1080"
42
	ClaudeProxy    string                    `yaml:"claude_proxy,omitempty"` // HTTP(S) proxy for Claude Code (it doesn't support SOCKS5)
43
	Providers      map[string]ProviderConfig `yaml:"providers"`
44
}
45
46
// Path returns the config file path following XDG Base Directory Specification.
47
func Path() string {
48
	dir := os.Getenv("XDG_CONFIG_HOME")
49
	if dir == "" {
50
		home, _ := os.UserHomeDir()
51
		dir = filepath.Join(home, ".config")
52
	}
53
	return filepath.Join(dir, appName, "config.yaml")
54
}
55
56
// Load reads the config from disk.
57
// If the file doesn't exist, it creates a default config with all known providers
58
// (empty API keys) and writes it to disk as a starting template.
59
func Load() (*Config, error) {
60
	data, err := os.ReadFile(Path())
61
	if err != nil {
62
		if errors.Is(err, os.ErrNotExist) {
63
			return createDefault()
64
		}
65
		return nil, err
66
	}
67
68
	cfg := &Config{
69
		Providers: make(map[string]ProviderConfig),
70
	}
71
	if err := yaml.Unmarshal(data, cfg); err != nil {
72
		return nil, err
73
	}
74
	if cfg.Providers == nil {
75
		cfg.Providers = make(map[string]ProviderConfig)
76
	}
77
78
	// Migrate legacy "zai-coding" → "zai" with plan=coding.
79
	if zc, ok := cfg.Providers["zai-coding"]; ok {
80
		zai := cfg.Providers["zai"]
81
		if zai.APIKey == "" && zc.APIKey != "" {
82
			zai.APIKey = zc.APIKey
83
		}
84
		if zai.Plan == "" {
85
			zai.Plan = "coding"
86
		}
87
		if zc.Model != "" && zai.Model == "" {
88
			zai.Model = zc.Model
89
		}
90
		cfg.Providers["zai"] = zai
91
		delete(cfg.Providers, "zai-coding")
92
		if cfg.ActiveProvider == "zai-coding" {
93
			cfg.ActiveProvider = "zai"
94
		}
95
	}
96
97
	return cfg, nil
98
}
99
100
// createDefault seeds and persists a config with all known providers.
101
func createDefault() (*Config, error) {
102
	cfg := &Config{
103
		Schema:    "https://bigbes.sourcecraft.site/claudio/config.schema.json",
104
		Providers: make(map[string]ProviderConfig),
105
	}
106
	for _, key := range providerKeys {
107
		cfg.Providers[key] = ProviderConfig{}
108
	}
109
	if err := Save(cfg); err != nil {
110
		return nil, fmt.Errorf("creating default config: %w", err)
111
	}
112
	return cfg, nil
113
}
114
115
// ResolveProvider builds a provider.Provider for the given key by merging
116
// the built-in registry entry (if any) with config overrides.
117
// Custom providers defined entirely in config are also supported.
118
func ResolveProvider(key string, pc ProviderConfig) *provider.Provider {
119
	p, exists := provider.Registry[key]
120
	if !exists {
121
		// Fully custom provider from config.
122
		compat := pc.Compat
123
		if compat == "" {
124
			compat = "openai"
125
		}
126
		name := pc.Name
127
		if name == "" {
128
			name = key
129
		}
130
		model := pc.Model
131
		if model == "" {
132
			model = key
133
		}
134
		small := pc.SmallModel
135
		if small == "" {
136
			small = model
137
		}
138
		return &provider.Provider{
139
			Name:        name,
140
			Description: pc.Description,
141
			BaseURL:     pc.BaseURL,
142
			Model:       model,
143
			SmallModel:  small,
144
			SonnetModel: model,
145
			OpusModel:   model,
146
			HaikuModel:  small,
147
			TimeoutMS:   "600000",
148
			AuthEnvVar:  "ANTHROPIC_AUTH_TOKEN",
149
			Compat:      compat,
150
		}
151
	}
152
153
	// Apply plan-specific overrides if set.
154
	if pc.Plan != "" {
155
		for _, plan := range p.Plans {
156
			if plan.Key == pc.Plan {
157
				p.BaseURL = plan.BaseURL
158
				if plan.Compat != "" {
159
					p.Compat = plan.Compat
160
				}
161
				if plan.AuthEnvVar != "" {
162
					p.AuthEnvVar = plan.AuthEnvVar
163
				}
164
				break
165
			}
166
		}
167
	}
168
169
	// Merge config overrides onto registry defaults.
170
	if pc.BaseURL != "" {
171
		p.BaseURL = pc.BaseURL
172
	}
173
	if pc.Model != "" {
174
		p.Model = pc.Model
175
		p.SonnetModel = pc.Model
176
		p.OpusModel = pc.Model
177
	}
178
	if pc.SmallModel != "" {
179
		p.SmallModel = pc.SmallModel
180
		p.HaikuModel = pc.SmallModel
181
	}
182
	if pc.Compat != "" {
183
		p.Compat = pc.Compat
184
	}
185
	if pc.ContextWindow > 0 {
186
		p.ContextWindow = pc.ContextWindow
187
	}
188
	return &p
189
}
190
191
// Save writes the config to disk.
192
func Save(cfg *Config) error {
193
	p := Path()
194
	if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
195
		return err
196
	}
197
	data, err := yaml.Marshal(cfg)
198
	if err != nil {
199
		return err
200
	}
201
	return os.WriteFile(p, data, 0o600)
202
}
203

Source Files