feat(config): 运行时配置体系①——LLM 多厂商独立组落 models.json + 面板落盘 + 热更新
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- runtimeconfig/models.go: models.json 承载 [{id,provider,baseUrl,apiKey,model,fallback?,multimodal?}] 多厂商独立组(首项=生效)
- LoadModels 优先文件/回退env(GroupFromEnv), SaveModels(0600+tmp原子写), Activate/Upsert/MaskedKey脱敏
- 面板 POST /admin/llm 落盘 models.json(取代temporary内存) + GET 列组(脱敏)
- runtime/model/runtime.go: RuntimeLLM(adapter+router) atomic快照可热替换, Reload 重建不中断进行中请求; per-role路由用接口
- wire 用 NewRuntimeLLM(adapter+router), 保留 a.modelAdapter(=runtime stack, 审查模型随热更新)
- .env 退兜底(.env.example注明); apiKey不打印/Config()脱敏/热更新不中断
- 全仓 go build/vet/test 绿(54包)+交叉编译+前端build绿(79模块); api-client v0.15已同步
- 遗留: vision ModelInfo 仍读静态env(后续纳入); 保存组=生效组(第一版语义)
This commit is contained in:
Aska committed 2026-09-07 13:07:58 +08:00
1 parent 8cde0162e6
commit 2d7151f979
16 files changed
+1079 -108

No files matched your search

+34 -16
View File
@@ -8,8 +8,8 @@ import { Button, Surface, Switch, Tag } from "../../components/primitives";
const sample: AdminLlmConfig = {
providers: [
{ id: "p1", name: "主模型", baseUrl: "https://api.example.com/v1", model: "example-ultra", multimodal: true, enabled: true },
{ id: "p2", name: "备选模型", baseUrl: "https://api.backup.example.com/v1", model: "example-fast", multimodal: false, enabled: true },
{ id: "p1", name: "主模型", provider: "openai", baseUrl: "https://api.example.com/v1", model: "example-ultra", multimodal: true, enabled: true },
{ id: "p2", name: "备选模型", provider: "openai", baseUrl: "https://api.backup.example.com/v1", model: "example-fast", multimodal: false, enabled: true },
],
};
@@ -20,8 +20,10 @@ export function LlmModule(): React.ReactNode {
const providers = backend && data ? data.providers : sample.providers;
const [name, setName] = useState("");
const [provider, setProvider] = useState("openai");
const [model, setModel] = useState("");
const [baseUrl, setBaseUrl] = useState("");
const [apiKey, setApiKey] = useState("");
const [multimodal, setMultimodal] = useState(false);
const [busy, setBusy] = useState(false);
const [fail, setFail] = useState<string | null>(null);
@@ -31,13 +33,14 @@ export function LlmModule(): React.ReactNode {
setBusy(true);
setFail(null);
setResult(null);
adminAddLlmProvider({ name, model, baseUrl, multimodal })
.then(({ provider }) => {
adminAddLlmProvider({ id: name || undefined, provider, baseUrl, apiKey, model, multimodal })
.then((r) => {
log("llm.addProvider", "ok");
setResult(`已记录 ${provider.name}(进程生命周期,未绑定模型路由)`);
setResult(`已保存到 models.json 并热更新:${r.provider.name} · ${r.provider.model}${r.path ? `(${r.path})` : ""}`);
setName("");
setModel("");
setBaseUrl("");
setApiKey("");
setMultimodal(false);
reload();
})
@@ -52,7 +55,7 @@ export function LlmModule(): React.ReactNode {
setBusy(true);
setFail(null);
setResult(null);
const providerId = p.id === "anthropic" ? "anthropic" : "openai";
const providerId = p.provider || "openai";
adminTestLlm({ provider: providerId, baseUrl: p.baseUrl, model: p.model })
.then((r) => {
log("llm.test", "ok");
@@ -71,8 +74,8 @@ export function LlmModule(): React.ReactNode {
title="LLM 模型"
subtitle="供应商 + 模型配置(base/key/多模态检测) + 测试(真实调用) + fallback + 按角色路由 + 用量"
actions={
<Button variant="primary" disabled={busy || !name || !model} onClick={addProvider}>
新增模型
<Button variant="primary" disabled={busy || !model || !baseUrl} onClick={addProvider}>
保存模型
</Button>
}
/>
@@ -102,12 +105,21 @@ export function LlmModule(): React.ReactNode {
</Surface>
<Surface className="space-y-3">
<h3 className="text-sm font-semibold">新增模型(进程生命周期,未绑定路由)</h3>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
<h3 className="text-sm font-semibold">新增模型(保存到 models.json,即刻热更新)</h3>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
<label className="block space-y-1">
<span className="text-xs text-[var(--mu-text-lo)]">名称</span>
<input value={name} onChange={(e) => setName(e.target.value)} className={inputCls} />
<span className="text-xs text-[var(--mu-text-lo)]">名称 / ID(留空自动生成)</span>
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="deepseek" className={inputCls} />
</label>
<label className="block space-y-1">
<span className="text-xs text-[var(--mu-text-lo)]">协议 Provider</span>
<select value={provider} onChange={(e) => setProvider(e.target.value)} className={inputCls}>
<option value="openai">openai(OpenAI 兼容)</option>
<option value="anthropic">anthropic(Anthropic Messages)</option>
</select>
</label>
</div>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
<label className="block space-y-1">
<span className="text-xs text-[var(--mu-text-lo)]">模型</span>
<input value={model} onChange={(e) => setModel(e.target.value)} className={inputCls} />
@@ -117,10 +129,16 @@ export function LlmModule(): React.ReactNode {
<input value={baseUrl} onChange={(e) => setBaseUrl(e.target.value)} placeholder="https://api.example.com/v1" className={inputCls} />
</label>
</div>
<div className="flex items-center gap-2">
<Switch checked={multimodal} onChange={setMultimodal} label="多模态" />
<span className="text-xs text-[var(--mu-text-lo)]">API key 不入库、不进 wire、不泄露;测试端点每次用请求内的 key 直接调用。</span>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
<label className="block space-y-1">
<span className="text-xs text-[var(--mu-text-lo)]">API key(写入 models.json,不回显)</span>
<input value={apiKey} onChange={(e) => setApiKey(e.target.value)} placeholder="sk-…" className={inputCls} />
</label>
<div className="flex items-end gap-2 pb-[2px]">
<Switch checked={multimodal} onChange={setMultimodal} label="多模态" />
</div>
</div>
<p className="text-xs text-[var(--mu-text-lo)]">保存后写入 models.json(本地、已 gitignore),并热更新运行中的 LLM provider,无需重启进程。API key 不在列表/响应中回显。</p>
</Surface>
<Surface className="space-y-3">
@@ -134,7 +152,7 @@ export function LlmModule(): React.ReactNode {
))}
</Surface>
<Notice>per-role 路由 + 降级逻辑由后端 loop 实现;新增模型记录为进程生命周期临时(不持久化/config 不变)。</Notice>
<Notice>per-role 路由 + 降级逻辑由后端 loop 实现;面板保存即写入 models.json 并热更新运行中的 LLM provider(无需重启,design/04 §3.1)。.env 为兜底。</Notice>
</div>
);
}