Skip to content

feat(plugin): 领域中立的插件内核 + 模型/联网搜索接入 - #61

Closed
lyingbug wants to merge 9 commits into
mainfrom
cursor/pluginize-model-management-3c85
Closed

feat(plugin): 领域中立的插件内核 + 模型/联网搜索接入#61
lyingbug wants to merge 9 commits into
mainfrom
cursor/pluginize-model-management-3c85

Conversation

@lyingbug

@lyingbug lyingbug commented Aug 14, 2026

Copy link
Copy Markdown
Owner

起因

仓库里已经长出了四套互不相通的注册表——联网搜索、文档解析引擎、数据源连接器、模型厂商。它们各自回答同一组问题,答案还不一致:

问题 之前
这是什么,能干什么 有的只有 id,有的有 Name/Description
需要什么配置 有的用 Go struct,有的用 map[string]string 魔法键
配置是否合法 各自在构造函数里手写 if
现在能不能用,为什么不能 只有文档解析回答了,其余靠猜
前端表单怎么渲染 单独维护一份列表

四份答案已经漂移:模型层的厂商规则被手工复制到 frontend/src/utils/thinkingControl.ts,注释写着"必须与后端保持一致",然后它们就不一致了。

本 PR 做了什么

1. 插件内核 internal/plugin(领域中立)

  • Manifest 身份与能力标签,可序列化——进程内插件和 RPC 后面的插件用同一种描述。
  • Schema 类型化配置,一份声明同时做校验和表单渲染;密钥永不回传。
  • Health 可用性是带原因的上报事实,不是承诺。
  • Registry[T] 按领域泛型,编译期类型安全;注册可逆,注册时全量校验。
  • Catalog 跨领域的非泛型视图,一个端点就能回答"这个部署能干什么";RPC 发现的外部插件也进同一个目录。

内核的测试用一个虚构领域(Greeter)而不是真实领域——如果测试需要知道什么是模型,它就不是中立的。

2. 联网搜索迁移(通用性的证明)

迁移前 迁移后
DI 容器里一张 11 行的接线表 provider 在自己文件里 init() 自注册,接线表和旧注册表删除
每个构造函数手写 "API key is required" 内核在构造前校验,错误形态统一
三个 provider 从 ExtraConfig 里手工解析选项 选项成为声明字段,智谱写错 search engine 本地就被拒而不是发给上游
无健康检查 Probe 说明为什么配置不可用,且能区分"没填"和"填错了"
无表单结构 API key 自动渲染为掩码输入,且不会通过目录漏出去

provider 实现一行未改,只改了注册方式。

3. 模型域(前几个提交)

三个标准协议驱动(OpenAI Chat / OpenAI Responses / Anthropic Messages)+ 声明式厂商插件 + 能力清单 API + 前端去重复。详见提交记录。

4. 文档

  • docs/插件开发指南.md —— 怎么给已有领域加插件、怎么把新子系统改造成可插拔领域、进程外插件怎么接。
  • docs/模型插件化设计.md —— 模型域第一版为什么不合格,以及重建的目标形态与五步迁移顺序。

关于模型域:我承认第一版不合格

第一版做了干净的 seam,然后把它螺栓到了旧结构上——DetectProvider 的 URL 嗅探、ChatConfig 上帝结构、extra_config 魔法键、RemoteAPIChat 的 SDK/raw 双路径、providerAdapter,一个都没消失。而且它把明显通用的东西(配置、值系统、表单渲染)放在了 LLM 包里,别人用不上。

本 PR 已经纠正了位置错误:那些概念现在在内核里,联网搜索正在用。

模型域的重建按设计文档分五步走,尚未开始,因为第 3 步会改动模型调用链上的每一个消费方。设计已写清楚要删掉什么(providerAdapter、双路径、ChatConfig、请求路径上的 URL 嗅探、四个 giant switch),想先确认方向再动手。

测试

  • 内核:13 组用例,覆盖校验前置、类型宽容/值域严格、密钥不回传、表单裁剪、注册可逆、跨领域目录、外部插件。
  • 联网搜索:迁移收益逐条断言(凭据前置校验、选项词表、Probe 区分失败原因、密钥不外泄、11 个 provider 一个不少)。
  • 模型域:20 条黄金报文用例 + 三协议流式解码 + 能力清单 + 端到端 httptest。
  • go test ./internal/... 全绿;前端 396 个测试通过,vue-tscvite build 通过。
Open in Web Open in Cursor 

lyingbug and others added 5 commits August 13, 2026 23:43
Introduce internal/models/llm as the plugin seam for model management: a
vendor declares parameters, their domains, their wire encoding, and their
form presentation in one descriptor, instead of spreading those facts across
a provider adapter, a thinking-strategy enum, and a frontend copy of the
same heuristics.

- spi: Value/Param/Draft/Encoder/Constraint/Plan/Registry. Registration is
  reversible and validated at startup; resolution prefers model-specific
  descriptors over a vendor catch-all and can pin a protocol.
- encoding: reusable encoders for every documented wire shape (thinking
  object, enable_thinking boolean, chat_template_kwargs, effort ladders,
  token budgets) plus the cross-parameter constraints vendors require.
- vendors: built-in plugins for OpenAI, Azure, Anthropic, DeepSeek, Aliyun,
  Volcengine, Zhipu, Moonshot, LKEAP and self-hosted deployments, each
  linking the documentation it encodes.

Thinking is not special-cased: it is three ordinary parameters, so the same
machinery that pins Moonshot's temperature expresses Anthropic's budget.

Covered by 20 golden wire-format cases asserting the exact outbound body.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Add OpenAI Chat Completions, OpenAI Responses, and Anthropic Messages as
protocol drivers behind one seam. A protocol owns the canonical body, the
endpoint, response decoding, and stream decoding; everything vendor-specific
stays in the descriptor that selects it.

The three learn about reasoning in three different ways - a delta field, a
typed event, and a content block - and the shared emit helpers make the
decoded stream identical downstream, including for models that inline their
reasoning in <think> tags across chunk boundaries.

- spi/message.go: neutral request vocabulary shared by every protocol.
- sse: event-aware reader; the typed protocols need the event name, and
  reasoning payloads exceed the default scanner limit.
- protocol/{openaichat,responses,anthropic}: the drivers.
- protocol/all: side-effect registration.

Covered by stream fixtures following each vendor's documented event shape,
asserting the full decoded transcript rather than individual fields.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Replace the provider-adapter branches and the four-value thinking_control
enum with the declarative plugins. The chat layer now resolves a descriptor
and applies its plan; which field carries a toggle, which sampling knobs a
model forbids, and which value is pinned are all declared per vendor.

- chat.Message and friends become aliases of the seam vocabulary, so there
  is one definition of a message and protocol drivers can render it.
- provider.go keeps only what a declaration cannot express: signing, a
  non-derived endpoint, a message rewrite, and tool-call metadata.
- The OpenAI-compatible transport applies the plan to the request's own JSON
  and compares the bytes to decide whether the SDK path still fits, instead
  of maintaining a second list of providers that need raw HTTP.
- Anthropic routes through the Messages driver, replacing a text-only
  implementation that had no tools, no thinking blocks, and no images. The
  Responses protocol becomes selectable per model.
- A stored thinking_control still forces the wire format it names, so
  existing configurations keep their behavior.

Two real bugs surfaced and are fixed: an unset thinking mode was treated as
off, which discarded a depth setting the request would have honored, and
max_completion_tokens lost to max_tokens when both were set.

Legacy AnthropicChat is deleted; its coverage is replaced by end-to-end
tests against the new driver that also cover tools, thinking replay, and
the base-URL spellings users paste.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Add GET /models/capabilities, rendering a plugin descriptor as a form schema:
groups, fields, widgets, the vendor's own vocabularies, and the wire field
each control writes. It is the fourth surface the one declaration drives,
after the request body, validation, and the debug report.

The frontend stops predicting vendor behavior. utils/thinkingControl.ts
carried its own copy of the Go provider rules under a comment asking readers
to keep them aligned; it is deleted. What replaces it reads the manifest:

- utils/modelCapabilities.ts holds the types and pure readers, with no I/O so
  it stays testable on its own.
- api/model owns fetching and caches per query, since the manifest is a pure
  function of the query on the server.
- The model editor no longer computes a default wire format. Its control
  becomes an advanced override defaulting to 'follow the plugin', which
  reports the field the backend actually resolved.
- The debug drawer asks the backend whether a model has a thinking toggle
  instead of inferring it from the provider name.

A stored thinking_control still wins everywhere, so no configuration has to
change.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
A boolean cannot reach the mode several vendors document, where the model
decides whether to reason per request: Volcengine's `auto` and Anthropic's
adaptive thinking were unreachable from the application even though both
plugins encode them.

Options.ThinkingMode takes 'on', 'off', or 'auto' and wins over the boolean,
which stays for the callers that only need two states.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
@lyingbug
lyingbug marked this pull request as ready for review August 14, 2026 00:17
lyingbug and others added 4 commits August 14, 2026 00:25
Four registries had grown independently — web search providers, document
parser engines, datasource connectors, and model vendors — each with its own
answer to the same five questions: what is this, what configuration does it
need, is that configuration valid, is it usable right now, and how does a UI
render a form for it.

Four answers diverged in practice. Some registries validated configuration
and some did not; only the parser registry reported availability with a
reason; the model registry's rules were duplicated in the frontend by hand.

The kernel supplies one answer to each, and knows nothing about any domain:

- Manifest: identity and capability tags, serializable so a plugin behind an
  RPC is described the same way as one compiled in.
- Schema: typed configuration that validates a value and renders its form
  from one declaration, replacing bespoke structs and magic string maps.
  Secrets never travel back out.
- Health: usability as a reported fact with a reason, not a promise.
- Registry[T]: generic per domain for compile-time safety, with reversible
  registration and total validation at registration time.
- Catalog: the non-generic view across domains, so one endpoint can answer
  what a deployment can do, and external plugins discovered over RPC join it.

Exercised through an invented domain rather than a real one: if the tests
needed to know what a model is, the kernel would not be neutral.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
The first domain migrated, and the proof the kernel is not model-specific.

What the domain had: a registry mapping an id to a factory, credential checks
hand-written inside each constructor, provider options as undocumented keys in
an ExtraConfig map, and a wiring list in the dependency container that had to
be edited to add a provider.

What it has now:

- Providers self-register from init, so the container's wiring list and the
  old registry are deleted rather than replaced.
- Each declares its inputs, so the kernel validates before a constructor runs
  and every provider reports a missing credential the same way.
- The options three providers parsed out of ExtraConfig by hand are declared
  fields with their documented vocabularies, so a wrong Zhipu search engine is
  refused locally instead of sent upstream. ExtraConfig is now a transport
  detail between the adapter and an untouched constructor.
- Probe reports why a configuration will not work, reusing the provider checks
  a schema cannot express, and the answers distinguish the failures.
- API keys render masked and never travel back through the catalog.

Provider implementations are untouched; only registration changed.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Covers contributing a plugin to an existing domain, turning a subsystem into
a pluggable domain, and the out-of-tree paths (embedded registration and
remote plugins published into the catalog).

States the three constraints a plugin author has to follow — declare facts
rather than branch on names, treat the schema as the only configuration
contract, and report availability honestly — and explains why the backend
sends structure and i18n keys but never display text.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
… failed

The first attempt built a clean seam and then bolted it onto the legacy model
layer, so none of the old mess disappeared: URL sniffing still chose the
vendor, ChatConfig still carried vendor-specific credential fields,
extra_config still held magic keys, and the SDK/raw dual transport stayed.

It also put the reusable parts in the wrong place. Configuration, values, and
form rendering ended up inside the LLM package where no other subsystem could
use them; they belong in the kernel, and now do.

The document states the target shape, the explicit ModelSpec that replaces
ChatConfig, the one place URL sniffing survives as a migration fallback, the
list of what must be deleted rather than left coexisting, and a five-step
order where each step is independently revertible.

Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
@cursor cursor Bot changed the title refactor(models): 模型管理插件化 —— 三大标准协议 + 声明式厂商插件 feat(plugin): 领域中立的插件内核 + 模型/联网搜索接入 Aug 14, 2026
@lyingbug lyingbug closed this Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant