feat(plugin): 领域中立的插件内核 + 模型/联网搜索接入 - #61
Closed
lyingbug wants to merge 9 commits into
Closed
Conversation
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
marked this pull request as ready for review
August 14, 2026 00:17
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
起因
仓库里已经长出了四套互不相通的注册表——联网搜索、文档解析引擎、数据源连接器、模型厂商。它们各自回答同一组问题,答案还不一致:
map[string]string魔法键四份答案已经漂移:模型层的厂商规则被手工复制到
frontend/src/utils/thinkingControl.ts,注释写着"必须与后端保持一致",然后它们就不一致了。本 PR 做了什么
1. 插件内核
internal/plugin(领域中立)Manifest身份与能力标签,可序列化——进程内插件和 RPC 后面的插件用同一种描述。Schema类型化配置,一份声明同时做校验和表单渲染;密钥永不回传。Health可用性是带原因的上报事实,不是承诺。Registry[T]按领域泛型,编译期类型安全;注册可逆,注册时全量校验。Catalog跨领域的非泛型视图,一个端点就能回答"这个部署能干什么";RPC 发现的外部插件也进同一个目录。内核的测试用一个虚构领域(Greeter)而不是真实领域——如果测试需要知道什么是模型,它就不是中立的。
2. 联网搜索迁移(通用性的证明)
init()自注册,接线表和旧注册表删除ExtraConfig里手工解析选项Probe说明为什么配置不可用,且能区分"没填"和"填错了"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),想先确认方向再动手。测试
go test ./internal/...全绿;前端 396 个测试通过,vue-tsc与vite build通过。