okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petfeat(login): configure Vertex and Bedrock from /login3b308f0

main
4.1 KiB131 linesraw
1// Package config defines CLI settings and validates values before they are
2// submitted to the daemon. The daemon owns persistence and credentials.
3package config
4
5import (
6	"errors"
7	"net/url"
8	"os"
9	"path/filepath"
10	"regexp"
11	"strings"
12	"unicode"
13)
14
15var providerNameRegex = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$`)
16
17// HomeDir returns the albedo configuration directory.
18func HomeDir() string {
19	if env := os.Getenv("ALBEDO_HOME"); env != "" {
20		abs, err := filepath.Abs(env)
21		if err == nil {
22			return abs
23		}
24		return env
25	}
26	home, err := os.UserHomeDir()
27	if err != nil {
28		return ".albedo"
29	}
30	return filepath.Join(home, ".albedo")
31}
32
33// Settings represents a provider configuration. Its api key lives in the
34// daemon's credential store. APIKey carries a newly entered draft value;
35// HasKey is presence metadata returned by the daemon.
36type Settings struct {
37	ProfileName string  `json:"-"`
38	Effort      *string `json:"effort,omitempty"`
39	ImageEdge   *int    `json:"image_edge,omitempty"`
40	AccountID   *string `json:"account_id,omitempty"`
41	Project     *string `json:"project,omitempty"`
42	Location    *string `json:"location,omitempty"`
43	Extension   string  `json:"extension,omitempty"`
44	BaseURL     string  `json:"baseUrl,omitempty"`
45	APIKey      string  `json:"apiKey,omitempty"`
46	Model       string  `json:"model"`
47	Protocol    string  `json:"protocol"`
48	HasKey      bool    `json:"hasKey,omitempty"`
49}
50
51func validateModel(value string) (string, error) {
52	trimmed := strings.TrimSpace(value)
53	if trimmed == "" || len(trimmed) > 512 || strings.ContainsFunc(trimmed, func(r rune) bool { return r < 0x20 || r == 0x7f }) {
54		return "", errors.New("model ID must be 1–512 characters and contain no control characters")
55	}
56	return trimmed, nil
57}
58
59// Validate checks whether the Settings instance is valid.
60func (s Settings) Validate() (Settings, error) {
61	if s.Protocol != "responses" && s.Protocol != "chat_completions" {
62		return s, errors.New("protocol must be responses or chat_completions")
63	}
64
65	model, err := validateModel(s.Model)
66	if err != nil {
67		return s, err
68	}
69
70	ext := s.Extension
71	if ext == "" {
72		ext = "openai"
73	}
74
75	var endpoint string
76	if s.BaseURL != "" {
77		endpoint, err = ValidateEndpoint(s.BaseURL)
78		if err != nil {
79			return s, err
80		}
81	}
82
83	if ext == "openai" && endpoint == "" {
84		return s, errors.New("OpenAI provider needs an API base URL")
85	}
86
87	if strings.ContainsFunc(s.APIKey, func(r rune) bool { return unicode.IsSpace(r) || r < 0x20 || r == 0x7f }) {
88		return s, errors.New("API key must not contain spaces or control characters")
89	}
90
91	return Settings{
92		ProfileName: s.ProfileName, Effort: s.Effort, ImageEdge: s.ImageEdge, AccountID: s.AccountID, Project: s.Project, Location: s.Location,
93		Extension: ext,
94		BaseURL:   endpoint,
95		APIKey:    s.APIKey,
96		HasKey:    s.HasKey,
97		Model:     model,
98		Protocol:  s.Protocol,
99	}, nil
100}
101
102// ValidateProviderName validates a profile name.
103func ValidateProviderName(value string) (string, error) {
104	name := strings.TrimSpace(value)
105	if !providerNameRegex.MatchString(name) {
106		return "", errors.New("provider name must start with a letter or number and use only letters, numbers, dots, underscores, or hyphens (1–64 characters)")
107	}
108	return name, nil
109}
110
111// ValidateEndpoint parses and validates an HTTP/HTTPS API endpoint without query, credentials, or fragment.
112func ValidateEndpoint(value string) (string, error) {
113	u, err := url.Parse(strings.TrimSpace(value))
114	if err != nil || u.Scheme == "" || u.Host == "" {
115		return "", errors.New("enter an HTTP or HTTPS API base URL")
116	}
117	if u.Scheme != "http" && u.Scheme != "https" {
118		return "", errors.New("enter an HTTP or HTTPS API base URL")
119	}
120	if u.User != nil || u.RawQuery != "" || u.Fragment != "" {
121		return "", errors.New("use an HTTP or HTTPS API base URL without credentials, a query, or a fragment")
122	}
123	return strings.TrimRight(u.String(), "/"), nil
124}
125
126// Profiles represents the stored provider configuration.
127type Profiles struct {
128	ETag      string              `json:"-"`
129	Providers map[string]Settings `json:"providers"`
130	Active    string              `json:"active,omitempty"`
131}