From 47e56ca13c8a29b46a5a8af60dbb8cce4cee9be3 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 4 Oct 2026 18:11:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E9=9D=A2=E6=9D=BF=E6=9A=B4?= =?UTF-8?q?=E9=9C=B2=20contextWindow=EF=BC=88=E5=8F=AF=E8=A7=81=E5=8F=AF?= =?UTF-8?q?=E6=94=B9=EF=BC=89+=20DocumentRef=20=E8=AF=AD=E4=B9=89=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 面板读写面与 maxTokens 逐处对齐:读 DTO 增 contextWindow(omitempty)、写请求 ContextWindow *int(缺失=保留、显式 0=未声明同义、负数 400)、回写 models.json;负数红线仍由 runtimeconfig.Group.Validate 唯一裁决,网关守卫只做「更早失败 + 面板可读字段名」。前端:类型 + 表单字段(提交前 /^\d+$/ 校验)+ 分组行角标 + Notice + 目录候选用早已解析却没人用的 contextLimit 预填。文案写明「重启后生效」(推导只在进程启动时算一次)。 - 顺手修既有红线违反:ProviderFields 87→76(抽 ApiKeyAndBudgetField)、LlmProviderForm 113→74(buildRequest 提为模块级纯函数)——本次碰过的文件里已无 >80 的函数,行为/文案逐字未变。 - DocumentRef 的 chars/truncated 注释从「抽取侧」改为「本回合注入侧」(全文长度只在 document-read 的 totalChars、extractTruncated 专指撞 8 MiB 安全上限);机械校验为空 ⇒ 代码零改动。 - 主会话独立验收:隔离 worktree Go 侧 build/vet + gateway/history 两包绿;主树 npx tsc --noEmit exit 0、npm run build 成功并 copy 到 internal/webui/dist;注释机械校验为空;变异由子代理复现(写路径不认该键 ⇒ 3 红;去掉网关负数守卫 ⇒ 400 变 500)。 --- frontend/web/src/admin/modules/Llm.tsx | 7 +- .../src/admin/modules/llm/LlmCatalogPanel.tsx | 7 +- .../src/admin/modules/llm/LlmProviderForm.tsx | 117 ++++++------ .../admin/modules/llm/LlmProviderRouting.tsx | 71 +++++++- frontend/web/src/lib/types/admin.ts | 4 + frontend/web/src/lib/types/adminWrite.ts | 3 + internal/runtime/history/document.go | 16 +- internal/services/gateway/admin.go | 5 + .../gateway/admin_llm_context_window_test.go | 172 ++++++++++++++++++ .../services/gateway/admin_llm_provider.go | 1 + .../gateway/admin_write_llm_channels.go | 20 +- 11 files changed, 355 insertions(+), 68 deletions(-) create mode 100644 internal/services/gateway/admin_llm_context_window_test.go diff --git a/frontend/web/src/admin/modules/Llm.tsx b/frontend/web/src/admin/modules/Llm.tsx index 1d494b1..2534743 100644 --- a/frontend/web/src/admin/modules/Llm.tsx +++ b/frontend/web/src/admin/modules/Llm.tsx @@ -10,7 +10,7 @@ import { CatalogPanel } from "./llm/LlmCatalogPanel"; import { LlmProviderForm } from "./llm/LlmProviderForm"; import type { CatalogHint } from "./llm/LlmProviderRouting"; -/** 一个供应商分组行:身份 + token 预算 + 推理形状 + 路由 + 测试/编辑动作。 */ +/** 一个供应商分组行:身份 + token 预算 + 上下文窗口 + 推理形状 + 路由 + 测试/编辑动作。 */ function ProviderRow({ p, busy, @@ -31,6 +31,9 @@ function ProviderRow({ {p.maxTokens ? `maxTokens ${p.maxTokens}` : "maxTokens 默认"} + + {p.contextWindow ? `窗口 ${p.contextWindow}` : "窗口未声明"} + {`推理 ${p.reasoning?.kind || "未声明"}`} @@ -259,7 +262,7 @@ export function LlmModule(): React.ReactNode { - 每组的 reasoning.kind(推理开关形状)按端点选:形状属于端点而不属于模型,选错会 400,因此面板只给出「未声明 / auto」与后端下发的几种形状。按角色路由(persona → 模型 / 协议)与降级链由后端 loop 真接线;面板保存即写入 models.json 并热更新运行中的 LLM provider(无需重启,design/04 §3.1),.env 为兜底。maxTokens 留空 = 保持原值 / 未设时回落 LLM_FALLBACK_MAX_TOKENS。multimodal 只表示主模型自己能否收图;图片/OCR 工具用的模型走 .env 的 LLM_VISION_MODEL / LLM_OCR_MODEL。目录候选来自 models.dev,由后端缓存成快照(离线仍可用),绝不进模型运行时热路径。 + 每组的 reasoning.kind(推理开关形状)按端点选:形状属于端点而不属于模型,选错会 400,因此面板只给出「未声明 / auto」与后端下发的几种形状。按角色路由(persona → 模型 / 协议)与降级链由后端 loop 真接线;面板保存即写入 models.json 并热更新运行中的 LLM provider(无需重启,design/04 §3.1),.env 为兜底。maxTokens 留空 = 保持原值 / 未设时回落 LLM_FALLBACK_MAX_TOKENS。contextWindow(模型上下文窗口,token)留空 = 未声明:后端按 128 Ki token 保守估算并记一条 WARN,填该模型的真实窗口才能吃到按窗口推导的上下文预算(该推导只在启动时算一次 ⇒ 重启后生效)。multimodal 只表示主模型自己能否收图;图片/OCR 工具用的模型走 .env 的 LLM_VISION_MODEL / LLM_OCR_MODEL。目录候选来自 models.dev,由后端缓存成快照(离线仍可用),绝不进模型运行时热路径。 ); diff --git a/frontend/web/src/admin/modules/llm/LlmCatalogPanel.tsx b/frontend/web/src/admin/modules/llm/LlmCatalogPanel.tsx index d7f975d..5985f45 100644 --- a/frontend/web/src/admin/modules/llm/LlmCatalogPanel.tsx +++ b/frontend/web/src/admin/modules/llm/LlmCatalogPanel.tsx @@ -158,8 +158,9 @@ function protocolFromCatalog(p: CatalogProvider): string { } /** 目录候选 → 表单草稿 + 能力建议。分组 id 取目录 id,重复填入即幂等更新同名 - * 分组;multimodal 只在目录**明确标注**接受图片输入时预填,maxTokens 建议值取 - * 模型的输出上限(目录真值,没有任何猜测成分)——两者都只是预填,用户可推翻。 */ + * 分组;multimodal 只在目录**明确标注**接受图片输入时预填,maxTokens 与 + * contextWindow 的建议值取目录的**输出上限 / 上下文窗口**(目录真值,没有任何 + * 猜测成分)——都只是预填,用户可推翻。 */ function draftFromCatalog(p: CatalogProvider, m: CatalogModel): { draft: AdminLlmProvider; hint: CatalogHint } { return { draft: { @@ -171,6 +172,7 @@ function draftFromCatalog(p: CatalogProvider, m: CatalogModel): { draft: AdminLl multimodal: m.attachment === true, enabled: false, maxTokens: m.outputLimit || undefined, + contextWindow: m.contextLimit || undefined, }, hint: { provider: p.id, @@ -199,6 +201,7 @@ function CatalogModelRow({ {model.reasoning === true ? 推理 : null} {model.toolCall ? 工具 : null} {model.outputLimit ? {`输出上限 ${model.outputLimit}`} : null} + {model.contextLimit ? {`上下文窗口 ${model.contextLimit}`} : null} {provider.api ? null : 目录未标 baseURL}