feat(config): 运行时配置体系①——LLM 多厂商独立组落 models.json + 面板落盘 + 热更新
- 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:
1 parent
8cde0162e6
commit
2d7151f979
16 files changed
+1079
-108
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user