diff --git a/pkg/config/config.go b/pkg/config/config.go index 9de0b90..99b35ce 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -198,6 +198,49 @@ type ProvidersConfig struct { Proxies map[string]ProviderConfig `json:"proxies"` } +type providerProxyItem struct { + Name string `json:"name"` + ProviderConfig +} + +func (p *ProvidersConfig) UnmarshalJSON(data []byte) error { + var tmp struct { + Proxy ProviderConfig `json:"proxy"` + Proxies json.RawMessage `json:"proxies"` + } + if err := json.Unmarshal(data, &tmp); err != nil { + return err + } + p.Proxy = tmp.Proxy + p.Proxies = map[string]ProviderConfig{} + if len(bytes.TrimSpace(tmp.Proxies)) == 0 || string(bytes.TrimSpace(tmp.Proxies)) == "null" { + return nil + } + // Preferred format: object map + var asMap map[string]ProviderConfig + if err := json.Unmarshal(tmp.Proxies, &asMap); err == nil { + for k, v := range asMap { + if k == "" { + continue + } + p.Proxies[k] = v + } + return nil + } + // Compatibility format: array [{name, ...provider fields...}] + var asArr []providerProxyItem + if err := json.Unmarshal(tmp.Proxies, &asArr); err == nil { + for _, it := range asArr { + if it.Name == "" { + continue + } + p.Proxies[it.Name] = it.ProviderConfig + } + return nil + } + return fmt.Errorf("providers.proxies must be object map or array of {name,...}") +} + type ProviderConfig struct { APIKey string `json:"api_key" env:"CLAWGO_PROVIDERS_{{.Name}}_API_KEY"` APIBase string `json:"api_base" env:"CLAWGO_PROVIDERS_{{.Name}}_API_BASE"` diff --git a/webui/src/pages/Config.tsx b/webui/src/pages/Config.tsx index 380d640..8159d65 100644 --- a/webui/src/pages/Config.tsx +++ b/webui/src/pages/Config.tsx @@ -24,6 +24,7 @@ const Config: React.FC = () => { const [basicMode, setBasicMode] = useState(true); const [hotOnly, setHotOnly] = useState(false); const [search, setSearch] = useState(''); + const [newProxyName, setNewProxyName] = useState(''); const hotPrefixes = useMemo(() => hotReloadFieldDetails.map((x) => String(x.path || '').replace(/\.\*$/, '')).filter(Boolean), [hotReloadFieldDetails]); @@ -84,6 +85,45 @@ const Config: React.FC = () => { } }, [cfg, baseline]); + function updateProxyField(name: string, field: string, value: any) { + setCfg((v) => setPath(v, `providers.proxies.${name}.${field}`, value)); + } + + function removeProxy(name: string) { + setCfg((v) => { + const next = JSON.parse(JSON.stringify(v || {})); + if (next?.providers?.proxies && typeof next.providers.proxies === 'object') { + delete next.providers.proxies[name]; + } + return next; + }); + } + + function addProxy() { + const name = newProxyName.trim(); + if (!name) return; + setCfg((v) => { + const next = JSON.parse(JSON.stringify(v || {})); + if (!next.providers || typeof next.providers !== 'object') next.providers = {}; + if (!next.providers.proxies || typeof next.providers.proxies !== 'object' || Array.isArray(next.providers.proxies)) { + next.providers.proxies = {}; + } + if (!next.providers.proxies[name]) { + next.providers.proxies[name] = { + api_key: '', + api_base: '', + protocol: 'responses', + models: [], + supports_responses_compact: false, + auth: 'bearer', + timeout_sec: 120, + }; + } + return next; + }); + setNewProxyName(''); + } + async function saveConfig() { try { const payload = showRaw ? JSON.parse(cfgRaw) : cfg; @@ -158,7 +198,33 @@ const Config: React.FC = () => { -
+
+ {activeTop === 'providers' && !showRaw && ( +
+
+
Proxies
+
+ setNewProxyName(e.target.value)} placeholder="new provider name" className="px-2 py-1 rounded bg-zinc-900 border border-zinc-700 text-xs" /> + +
+
+
+ {Object.entries(((cfg as any)?.providers?.proxies || {}) as Record).map(([name, p]) => ( +
+
{name}
+ updateProxyField(name, 'api_base', e.target.value)} placeholder="api_base" className="md:col-span-2 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" /> + updateProxyField(name, 'api_key', e.target.value)} placeholder="api_key" className="md:col-span-2 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" /> + updateProxyField(name, 'protocol', e.target.value)} placeholder="protocol" className="md:col-span-1 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" /> + updateProxyField(name, 'models', e.target.value.split(',').map(s=>s.trim()).filter(Boolean))} placeholder="models,a,b" className="md:col-span-1 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" /> + +
+ ))} + {Object.keys(((cfg as any)?.providers?.proxies || {}) as Record).length === 0 && ( +
No custom providers yet.
+ )} +
+
+ )} {activeTop ? (