Skip to content

feat(model): capability-driven model management and unified provider invocation layer - #3278

Closed
Greenplumwine wants to merge 89 commits into
Tencent:mainfrom
Greenplumwine:feat/model-mgmt-unified-invocation
Closed

Greenplumwine wants to merge 89 commits into
Tencent:mainfrom
Greenplumwine:feat/model-mgmt-unified-invocation

Conversation

@Greenplumwine

@Greenplumwine Greenplumwine commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Description

本 PR 重构模型接入体系:以「能力声明 + 统一调用层」取代原先分散在 chat / embedding / rerank / asr / vlm 五个包的厂商实现,让新厂商接入从"实现五个接口"降为"声明能力 + 覆盖线协议差异"。当前仅阿里云(DashScope 原生)完成适配与真机验证,其余厂商的适配完成度与待办见文末——欢迎官方在此基础上共同开发与审核。

统一调用层(internal/models/invoke)

  • 单一分发入口按 facet(chat / embedding / rerank / asr / remote-list)路由到厂商适配器;适配器只描述线协议,SSRF 门控、重试、并发限流、SSE 解复用收归 executor
  • 厂商能力声明(Capabilities,JSON 形态随 /models/providers 下发):思考档位、维度可覆盖、输入模态、凭证 spec、模型列表支持位等,前端据此动态渲染编辑器表单
  • 中立消息/工具调用词表(Role/ToolDef/ToolCall/StreamKind),厂商词表映射收敛在各适配器内(//exhaustive:enforce)
  • ProviderError 内部分类(auth/provider_upstream/rate_limit/…,驱动重试链)、LLM 调试黑匣子(请求/响应/用量落盘)、prompt-cache 断点标记、langfuse 入口内建埋点
  • 原有 internal/models/{chat,embedding,rerank,asr,vlm} 全部退役,所有调用方完成迁移(agent 引擎、压缩、记忆、QA 流水线、会话)

模型管理

  • 模型编辑器按能力声明动态渲染:凭证 spec 逐槽(编辑态走 /credentials 子资源)、厂商 extraFields(进 extra_config,测试连接与生产同源)、思考档位(模型记录分片 ∩ 厂商枚举)、目录预填即终态(保存后不再被目录覆盖)
  • 会话级思考覆盖:面板支持开/关 + 档位(初始展示智能体设置;QA 请求新增 thinking 布尔,nil=跟随智能体)
  • 模型卡片/下拉厂商 logo、usage 用量展示、下拉按模型类型过滤
  • 阿里云 DashScope 原生协议适配器 + 原生显式缓存(cache_control 断点,默认开启)

顺带修复(真机排查发现)

  • openai 流桥:finish_reason 与 tool_calls 同帧时工具调用被整帧丢弃
  • anthropic:extended thinking 的 thinking_delta 未透传(思考开了但 UI 不可见)
  • 阿里云强制思考谓词漏裸名 glm-5.3(标题生成 400);DashScope 带内错误帧静默丢弃;中间帧 finish_reason:"null" 字符串掐断流
  • 远程目录探测对 /api/v3、/api/plan/v3 类地址拼出双版本 URL 必 404
  • 多模态 embedding 索引字段双形制 + 空向量落库防护

本轮追加(2026-09-16,模型类型收敛 + 真机回归修复)

  • 模型类型收敛为四类:视觉(VLLM)不再是独立类型,并入对话类型、经输入模态声明区分(文本恒选不可关 / 图片 / 音频可勾;视频无产品入口,不进词表);类型轴 = 调用操作(与管理/调用层的 facet 一致),不引入输出模态概念。配套数据迁移 000097(postgres)/ 000018(sqlite):存量 VLLM 行改写为 KnowledgeQA 并恒补 text+image 模态声明(类型即视觉声明,单向门);初始化向导的槽位分拣改为显式 slot 直取;图像理解类绑定槽位(KB 图片描述、智能体看图)选择器改按能力过滤(对话 ∧ 图片输入)
  • 思考主开关成为硬门:模型管理页关闭思考后,会话/智能体层的思考请求一律不再发出(foldChatOptions 单点强制,流式/非流式全覆盖);对话页所选模型思考关闭时思考触发按钮与面板整块消失;智能体编辑器思考控件置灰说明;"思考不可用" UX 统一为整块隐藏
  • 自定义 OpenAI 兼容端点恢复思考配置:ProviderGeneric 补入厂商思考声明表——运行时本就走 openai 适配器(reasoning_effort),此前编辑器对自定义端点整块隐藏思考区,声明与运行时对齐
  • 图像入口级内联(design §6.6 单一归属):resource:// / local:// / storage:// 内部句柄在 invoke 入口统一解析为 data URI 后再分发适配器;修复火山 Ark 收到内部 resource:// 句柄直接 400("Only base64, http or https URLs are supported")
  • 移植上游 special_tokens 净化:跟进上游对特殊 token 字面量(<|、[INST]、<<SYS>> 等)的出站中和,在 invoke 入口单点应用于全部消息(文本 / ReasoningContent / 工具参数),全适配器继承
  • 附带:上游 dingtalk 迁移与本批迁移版本号相撞(双方同为 000096/000017),本批迁移重编号为 000097(postgres)/ 000018(sqlite)

Type of Change

  • ✨ New feature
  • 🐛 Bug fix
  • 🎨 Refactor
  • 🧪 Test
  • 📚 Documentation update
  • ⚡ Performance improvement

Related Issue

无对应 issue;后续协作项见文末,欢迎认领。

Testing

  • 全量 go test ./...(含 -ldflags="-X 'google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn'",qdrant/milvus proto 冲突所需):失败集与 origin/main 基线逐名一致,差异为零;基线中的环境依赖失败为 fake-IP DNS SSRF 家族(TestQueryTemplates*/TestUpdate*/TestDelete* 等,本机 DNS 代理所致)与 TestSkillPythonVerifier,与本次改动无关
  • golangci-lint run --new-from-rev=origin/main ./...:0 issues;git diff --check 通过
  • 前端:vue-tsc type-check 0 错误;npm run test 925/925(含上游合并新增用例);vite build 通过
  • 适配器对账:golden 重放/对账测试覆盖各协议族线协议形状
  • 真机验证:仅阿里云(DashScope 原生,glm/qwen 族、显式缓存)与部分 OpenAI 兼容/Anthropic 协议链路(火山 agent plan deepseek 对话与 embeddings、ollama)完成真机验证,其余厂商待逐个验证

未完成与后续协作项

整体状态:除阿里云(DashScope 原生)外,其余厂商适配均未完成——适配器骨架与存量行为已迁移进统一调用层,但能力声明细化、线协议核对、真机验证均待逐厂商补齐。以下按优先级分层,欢迎官方认领协作。

一、架构级待定(需与官方对齐设计后再动)

  1. models.json 目录规范未定:条目格式规定、本地存储位置、公共(社区共享)models.json 仓库的位置与同步机制——当前仓库内置文件仅作编辑器预填数据源(保存即终态,不回写已存模型)
  2. 统一错误码与错误信息规范未定:现有 ProviderError 分类(auth/provider_upstream/rate_limit/…)仅服务于重试链内部决策,面向用户与开发者的统一错误码体系待设计
  3. 数据库改造未启动:表结构沿用现有设计,后续是否需要随能力声明/目录/参数分片深化(如凭证分离、按 facet 建表)待评审

二、厂商适配(骨架已迁移,逐厂商补齐)

  1. Google/Gemini:未真实验证。当前按"官方提供 OpenAI 兼容层即用兼容层"的裁定走 compat 层;本意是使用 Google 原生接口(Generative Language API),是否回切待与官方对齐
  2. Anthropic:extended thinking 的 signature 回放未做(agent 多轮 tool-use 循环必需;本 PR 仅完成思考输出透传)
  3. 其余厂商具体缺陷清单(存量移植,均未真机验证):
    • Azure:ASR 三处行为错误;chat api-version 仍为 2023-05-15 旧版
    • GPUStack / 千帆 / 智谱 / 火山(常规 Ark):模型列表 URL 推导 404(本 PR 已修 generic 路径的通用版本段识别,厂商专属验证待续)
    • Novita:max_tokens 必填未发送;rerank 未注册
    • LKEAP embedding / OpenRouter rerank / 智谱 ASR:facet 未注册
    • OpenAI:rerank 支持应移除(官方无此接口)
    • 硅基流动:thinking 参数未接;GPUStack / ModelScope:缺 chat_template_kwargs
    • 七牛:DetectProvider 域名不匹配
    • ollama:未完成普通模型服务商迁移
    • Anthropic / Gemini 适配器尚无图像内容映射(消息携带图片时会被静默丢弃,不随请求发出)

四、文档(刻意后置)

  • swagger 重新生成(make docs);apifox 同步;v2 迁移升级说明;Langfuse 集成文档
  • 备注:website-docs/02-api-model-system.md 已随本 PR 更新(capabilities/extraFields/catalog 端点)

五、观察项 / 候选

  • 会话标题生成模型独立配置(当前跟随智能体默认模型)
  • 火山 Agent Plan 网关无 /models 路由(厂商确认 404),该网关下配置走手输模型 ID
  • 远程目录探测失败原因仅日志可见,未透出 UI;厂商目录与适配器 caps 的 model_listing 位存在双轨缺口

Checklist

  • git diff --check origin/main...HEAD passes
  • Changed source files are formatted
  • Targeted tests for the changed packages/components pass
  • Diff-scoped lint passes where applicable (for Go: golangci-lint run --new-from-rev=origin/main ./...)
  • Full-repository checks were run, or any unrelated/environment-dependent failures are documented above
  • Self-reviewed the code
  • Added/updated tests covering the change
  • Updated related documentation (README, docs/, Swagger annotations, etc.)
  • Breaking changes are clearly called out in the description above

Screenshots / Recordings

真机截图随后补充在评论区(模型编辑器能力声明动态表单 / 会话思考面板 / 厂商 logo 与用量展示)。

Greenplumwine and others added 30 commits August 13, 2026 14:35
Co-Authored-By: Claude <noreply@anthropic.com>
…ng levels, unified invocation core

Backend implementation of the model-management refactor + unified invocation
layer (task 09-08-model-mgmt-unified-invocation; ADRs 0001-0003, CONTEXT.md):

- Capabilities: ProviderInfo.EffectiveCapabilities() synthesizes provider-level
  capability declarations (thinking levels, protocol family, per-type shards);
  ModelProviderDTO serves capabilities + extra_fields (design §5.1)
- Parameter sharding: ModelParameters.Chat shard (context window, thinking
  level/selection, input modalities) with dual-read accessors over the
  deprecated top-level fields — zero DDL, legacy rows keep working (design §5.2)
- Thinking levels: ChatOptions/AgentConfig/CustomAgentConfig/QA-request
  ThinkingLevel fields + ResolveThinkingLevel chain (session > agent > model >
  provider default, SelectedLevels-governed fallback, design §5.4)
- Wire mapping: OpenAI/Azure reasoning_effort passthrough (gated to
  o-series/GPT-5); Anthropic thinking.budget_tokens with max_tokens ceiling
  enforcement (design §4.3, continuous-vendor收编)
- Unified error model: ProviderError taxonomy with mandatory classification at
  every response path + provider-level retry with backoff (retryable: 429/5xx/
  network/timeout; fast-fail: auth/context/policy, design §4.6)
- Model catalog: WeKnora-own models.json schema (15 providers / 857 models
  converted from the models.dev reference), LOCAL-embedded loader with
  GITHUB/CNB enum hooks, GET /models/catalog + POST /models/remote-catalog
  backend probe (SSRF-gated, rate-limited, key never logged, design §5.10)

Deferred: RPM/TPM rate limiting (no token-bucket infra exists), fallback
chain (D8), Qwen/Gemini numeric level mapping (pending vendor docs).
Env-dependent pre-existing failure: TestSkillPythonVerifier venv check.
…log prefill

Frontend half of the model-management refactor (task 09-08-model-mgmt-
unified-invocation):

- Vendor-knowledge mirror deleted: utils/thinkingControl.ts + test removed
  (zero residual references); the wire-format dropdown and its supporting
  state/styles removed from ModelEditorDialog (D3)
- Thinking controls rendered from the provider capability declaration:
  on/off switch (hidden when can_disable=false), level multi-select +
  default select fed by capabilities.chat.thinking.supported_levels;
  unsupported models show the degradation hint instead (design §5.4)
- Catalog prefill per the value chain (ADR 0001/D1): new or changed model id
  probes POST /models/remote-catalog then reads GET /models/catalog (exact →
  normalized id); saved values are never overwritten, manual edits win, and
  source badges mark catalog/remote origin
- Model id input becomes a searchable select backed by the remote-catalog
  probe (debounced, falls back to free input)
- 13-provider hardcoded fallback collapsed to the generic custom-vendor entry
- Saving writes the parameters.chat shard (thinking_level, selected_levels,
  input_modalities); legacy flat fields no longer written for chat — the
  backend dual-read accessors keep old rows working (design §8)
- i18n: new keys across zh-CN/en-US/ko-KR/ru-RU/ja-JP (audit requires all
  locales share keys); legacy thinkingControl keys removed
- ModelSettings tab counts and dropdown menu untouched (5.6.6)

Verified: vue-tsc type-check, check-i18n audit, and the full frontend test
suite pass.
…ultimodal retry, lint

Quality-check fixes over the model-management branch:

- POST /models/remote-catalog was registered inside the /models apiKeyGroup
  and inherited its manage_models/full-access API-key policy, violating the
  design constraint that the key-carrying probe serves JWT only. Moved out of
  the group (APIKeyGate fails closed → API keys 403), dropped the ApiKeyAuth
  swagger annotation, and added a policy regression assertion.
- The multimodal-degradation retry in chatOnce/chatStreamOnce re-ran
  shapedRequest with the raw opts, bypassing resolveThinkingLevelOpts — a
  second attempt could emit an unvalidated reasoning_effort. Resolution now
  happens once per attempt.
- 29 diff-scoped lint findings fixed (errcheck, lll, gofumpt, revive:
  catalog type-name stutters, const-block comments, builtin shadowing).

golangci-lint --new-from-rev=main: 0 issues. All targeted tests green.
…den wire baseline (P1a/P1b)

- invoke entry (Chat/ChatStream/Embed/Rerank/Transcribe/List) with langfuse
  middleware, entry-level llm_debug records (v1-format), image preprocessing,
  multimodal degradation retry, thinking-level entry fold
- single HTTP executor: model-level concurrency, SSRF gate without vendor
  exceptions, retry/backoff, SSE+NDJSON demux, RPM/TPM placeholder slot
- adapters dispatch by provider name (openai family incl. weknoracloud HMAC,
  anthropic native Messages, ollama native /api/chat + /api/tags list facet);
  model-conditional shaping via shapeFor/thinkingFor
- typed Role enum with //exhaustive:enforce switches + golangci exhaustive
  (explicit-exhaustive-switch); JSON on-disk representation unchanged
- 32 golden wire scenarios reconciled byte-for-byte against v1
- provider capability shards + Credentials spec DTO for /models/providers
…vlm path (P1c/P1d)

- ~40 callers migrated to invoke.Chat/ChatStream: agent engine
  (think/finalize/observe/compaction/token estimator), chat_pipeline,
  session QA, memory/*, message_suggestion, extract, graph,
  knowledge auto-tag/process, wiki ingest, evaluation, handlers,
  modelcontext, container assembly
- interfaces.ModelService drops GetChatModel/GetVLMModel; session title,
  agent image describer, temporary-document VLM go through the shared
  buildModelConfigByID + invoke.Chat seam
- single shared constructor BuildModelConfig: WeKnoraCloud tenant
  credential fallback, legacy local-record ollama mapping (with /v1
  strip), extra_config.remote_model_name wire-name override
- budget unified on ChatOptions.MaxCompletionTokens; legacy
  summary.max_tokens migrated at config load (backfillSummaryBudget,
  outside the prompt-templates gate); tenant_skill installer deduped
  onto the shared helper
- deleted internal/models/chat and internal/models/vlm (strangler
  complete for chat+vlm; embedding/rerank/asr follow in P2/P3)
…pec forms (P-FE)

- extract ThinkingControls.vue shared component (dual mode: single level
  selector / multi-level allow-set, badge slot for catalog prefill)
- agent editor: static thinking toggle replaced by ThinkingControls under
  the model selector; level cleared with toast when switching to a model
  that does not support it; unsupported models greyed out
- chat page: ThinkingControls next to the model picker (session-scoped,
  in-memory only with reset); thinking_level now sent on the QA stream
  (closes the v1-deferred send path)
- model editor: credential inputs rendered dynamically from the provider
  Credentials spec (edit shows Configured state + CredentialResource card,
  empty submit = no change)
- i18n: locale keys audited across zh-CN/en-US/ja-JP/ko-KR/ru-RU
P2-a/P2-b strangler (task 09-08): adapters produce invoke.Request for the
embedding facet; the executor stays the only network egress.

- invoke.Embed: record-derived knobs on EmbeddingOptions (truncate budget,
  dimension-override gate); entry-level per-input fan-out via the optional
  SingleInputEmbedder (volcengine Ark multimodal yields one vector per
  request); llm_debug + langfuse middleware ported from the v1 decorator
  stack (exact v1 record format, v1 input shapes).
- adapters/embedding.go: shared openai-shape build/parse (openai/zhipu/
  generic family) with per-vendor deltas — azure deployment URL + Api-Key,
  nvidia input_type=passage (v1 ctx variant was dead code), jina truncate
  boolean (newly registered, embedding-only), aliyun text/vision dual-path,
  volcengine multimodal, gemini native batchEmbedContents (ported ahead of
  P5; only generateContent chat remains doc-gated).
- ollama /api/embed facet (SDK-equivalent wire; auto-pull retired per
  design §11); weknoracloud HMAC-signed /api/v1/embeddings with index
  integrity validation.
- Every embedding Build sets the v1 60s timeout; registration lock #2 now
  covers 12 dual/single-facet providers (jina joins the registry).
- Entry applyCustomHeaders gains the always-protected auth-header set
  (v1 reservedHeaderKeys minus content-type): review finding 3.
- 12 golden wire baselines recorded from the v1 clients and replayed
  byte-for-byte through invoke.Embed; unit coverage for parse semantics,
  ollama SDK-equivalent wire, and auth-header protection.
…odels/embedding

P2-c/P2-e strangler (task 09-08): the v1 client package (23 files) is
deleted; every caller now rides the unified entry.

- Embedder/EmbedderPooler caller seam moves to types/interfaces (method
  set unchanged; test fakes are mechanical renames).
- service/embedding_client.go: invokeEmbedder over invoke.Embed — v1
  empty-result 3x retry on Embed, count contract on BatchEmbed (review
  finding 4), BATCH_EMBED_SIZE sub-batch pooler through the process ants
  pool; concurrency gating rides the executor limiter (background-only,
  same semantics as the v1 innermost wrapper).
- GetEmbeddingModel/GetEmbeddingModelForTenant rebuild through the shared
  constructor (BuildModelConfig): credential slots, WeKnoraCloud tenant
  fallback, legacy local-record mapping — v1 ConfigFromModel promise kept.
- TestEmbeddingModel goes through BuildModelConfig + invoke.Embed with the
  form's EmbeddingParameters carried over (v1 production/test parity);
  dimension now reported from the returned vector (review findings 1-2).
- aliyun multimodal base URL trims the trailing slash like the v1
  constructor (review finding 6).
P3-a/P3-b strangler (task 09-08): adapters produce invoke.Request for the
rerank and ASR facets; the executor stays the only network egress.

- adapters/rerank.go: openai-shape fallback (truncate_prompt_tokens only
  via the Endpoint fold of extra_config — never sent by default, issue
  Tencent#2143) + per-vendor deltas: jina (return_documents), zhipu (full-endpoint
  base posted directly), nvidia (query/passages objects, rankings envelope,
  raw logit sigmoid-normalized like v1 normalizeNvidiaLogit), aliyun
  (DashScope envelope output.results, top_n=len(documents)), weknoracloud
  (HMAC-signed /api/v1/rerank). Composites per shard set: quad
  (chat+embedding+rerank+ASR), rerank/ASR triples, jina embed+rerank.
- P3 ruling: lkeap (TC3) and volcengine (IAM) rerank stay on the v1 SDK
  clients until P5 — signature protocols are native-adapter work. Their
  adapters trim the rerank shard (P1a interim convention; the frontend DTO
  still reads the provider package) and the service layer branches them.
- adapters/asr.go: one openai-shape multipart transcriptions facet covers
  every vendor (field set mirrors the v1 go-openai SDK writer; default
  audio.mp3; multipart Content-Type protected); verbose_json parse with v1
  TrimSpace semantics; body rides replayable Request.Body.
- invoke.Rerank/Transcribe gain the v1 decorator equivalents (langfuse
  generation shapes + llm_debug record formats); invokeFacet gains the
  design §6.2 lock-#3 facet guard (ErrUnsupportedType, never a panic).
- 7 golden rerank baselines recorded from the v1 clients and replayed
  byte-for-byte (request side) with response-side assertions on the two
  envelope-sensitive vendors; ASR wire asserted semantically (random
  boundary).
P3-c/P3-d strangler (task 09-08): models/asr is deleted; models/rerank is
trimmed to the two P5-deferred SDK vendors.

- types/interfaces/asr.go hosts the caller seam (ASR/TranscriptionResult/
  Segment) — the deleted package cannot; GetASRModel returns it.
- service/rerank_client.go: invokeReranker (rerank.Reranker over
  invoke.Rerank; Document re-derived from input by index, equivalent to the
  vendor echo) + invokeASR over invoke.Transcribe.
- GetRerankModel branches by provider: lkeap/volcengine keep the v1 SDK
  clients (TC3/IAM signatures — P5), everything else rides BuildModelConfig
  + invoke.Rerank; the trimmed models/rerank factory rejects any other
  provider loudly. checkRerankModelConnection mirrors the branch.
- TestASRModel goes through BuildModelConfig + invoke.Transcribe (silent-WAV
  regression preserved).
- models/rerank keeps only lkeap/volcengine clients + factory + decorators
  + transport; deletion completes in P5 with the signature adapters.
…AC signer to invoke, ollama up one level

P4 清场批次 1/4。slices.go 泛型工具归 internal/utils(与同层通用工具同居);
WeKnoraCloud HMAC Sign 迁 invoke/signer.go(端点协议知识与 weknoracloud 适配器
同居调用核心包,docparser 转发路径同源复用);ollama 子包升一级供模型管理 UI
独立使用(不进调用链)。零行为变更;TestSSRFSafeURL 失败为存量 fake-IP DNS 环境族。
… is executor semantics

P4 清场批次 2/4。Redis 分布式信号量(Lua 原子准入 + 租约心跳 + fail-open)、
本地信号量、governor 单例与 RuntimeStats /system 暴露面全部原样迁入 invoke 包;
executor.go 直调 GateNamedN,container/system_setting/handler 三处调用方改
invoke.*。零行为变更;handler 失败集仍为已知存量 1 项。
… dispatch deleted

P4 清场批次 3/4。openai 家族(含 weknoracloud/volcengine 经 composite 提升)与
anthropic 补第五分面 BuildListRequest/ParseListResponse,URL 推导自 v1
catalog/remote.go 逐字迁移(parity 由 list_test 钉住);invoke.List 入口接管
探测编排——SSRF 门、8s 探测超时(env 可调)、500 上限,执行器统一出口。
azure 部署模式 Build 时显式拒绝(DRIFT:v1 发注定 404 的请求后降级,理由文案
不同);jina/weknoracloud 不实现 listing,与分片声明锁 #2 一致。
RemoteModel 补 v1 wire JSON 契约(id/display_name/owned_by;恒空的 meta 信封
删除——v1 从不填充,前端只读 id)。HTTP 契约不变。
…lds, usage chip

Ruling batch (2026-09-14): Tencent#15 / D1 / azure-extra-fields / Tencent#14 FE / E9 UI.

- Tencent#15: providerLogos registry gains the 'model' category — drop SVGs at
  assets/img/providers/{color,mono}/model/<provider>.svg and they light up
  in the provider select (color direct / mono masked, initial-badge
  fallback) and on the model-card badge (type icon stays the fallback).
- D1: the editor drawer no longer closes on emit — the parent calls the
  exposed resetAfterSave() only after create/update resolves, so a failed
  save keeps every input on screen.
- Azure extra fields: provider extraFields render as dynamic inputs bound
  to extra_config (defaults prefilled on provider switch, values merged
  into save payload and the connection-test request; lkeap region keeps
  its dedicated control).
- app_secret for signed rerank (lkeap/volcengine) is required in label
  star + save validation, matching the rerank constructor contract.
- Provider switch with no default URL for the type clears the base URL
  instead of keeping the previous vendor's host.
- Tencent#14 FE: the type tab filter is pushed to the server (?type=), tab
  switches reload; full loads only refresh the shared model cache.
- E9: the complete handler stores event usage on the assistant message
  and botmsg renders a tokens line (prompt → completion · total · cache
  hit) whenever usage exists.
…nd to end

Report: uploading a document with parent-child chunking failed the
embeddings batch insert with 'halfvec must have at least 1 dimension'.

Root cause chain, three layers:
- The two native DashScope embedding endpoints disagree on the response
  index field: text-embedding carries text_index (通用文本向量API文档),
  multimodal-embedding carries index (多模态向量API详情.md:219). The shared
  parser read only text_index, so for vision embedding models EVERY entry
  decoded to slot 0 — all vectors in a sub-batch overwrote one slot and
  the rest stayed nil (one real + N empty per batch of 5, matching the
  observed rows).
- The sub-batch pooler validated count only, so empty vectors passed.
- The postgres batch insert happily wrote 0-dimension rows, and halfvec
  rejects them, killing the whole statement.

Fixes: the parser decodes both fields (typed pointer picks the one the
wire sent) and hard-errors on unfilled slots (duplicate/missing index);
the pooler rejects empty vectors as a sub-batch failure; the postgres
batch save skips vector-less rows (the documented 'parents go into DB
but NOT into the vector index' contract) with a warn instead of
poisoning the statement. Tests pinned at all three layers.
…ame autofill

Real-machine feedback (2026-09-14):
- The remote model dropdown now filters by the edited model type
  client-side: server-side filtering only works for aliyun (capability
  codes), every other vendor's catalog carries no type metadata. Name
  heuristics: embedding/rerank/asr keep their families, chat/vllm
  exclude the non-chat families; creatable manual input stays as the
  escape hatch for anything misnamed.
- Dropdown options render model ID as the primary text with the vendor
  display name as small gray secondary text (label keeps 'id · name' so
  search and the selected trigger are unchanged).
- Picking a model auto-fills 显示名称 from the vendor's display name —
  manual edits win (prefill-source badge + manual-field tracking, same
  contract as the other prefilled fields).
The card row was align-items:flex-start (plus a 1px nudge on the badge),
so the 36px logo sat on the title line while the body is two lines tall.
Row now centers; the top-right action buttons pin themselves back to the
top via align-self.
The custom two-part option layout never took effect under TDesign's
options/slot interplay and the user prefers the plain form anyway —
options now carry label = model ID. Type filtering and the display-name
autofill (which reads the vendor display_name from data, not the
dropdown text) are unchanged.
The ID-only simplification dropped BOTH the options prop and the slot,
leaving the select with no items at all. Re-attach
:options=remoteModelOptions (label = model ID); type filtering lives in
the computed and is unaffected.
…lidates, gate was vapor

The 自定义输出维度 toggle and 向量维度 input were hard-locked for every
remote provider: defaultCapabilities synthesized EmbeddingCaps{} with
CanOverrideDimension=false and nothing ever set it true, making the
fully-wired dimensions machinery (every embedding adapter sends the
vendor dimension param) unreachable dead UI.

The toggle itself is the user's confirmation, and 测试连接 sends the
requested dimension and reports the observed one — a vendor that
ignores/rejects the param surfaces at probe time. Provider-level
false stays as an explicit escape hatch.
…ation

Hydration intentionally skips the probe watch (one guard against
per-field firing while the form fills), but nothing triggered the first
probe afterwards — an edit-open model dropdown stayed empty until the
user edited some field (e.g. cleared the model name) to wake the watch.
Call probeRemoteModels() once after hydratingForm clears; the probe
already carries edit-mode credential fallback (model_id) and the
sequence guard dedupes any racing user-triggered probe.
…or buttons

Ollama's think is a boolean wire param (adapter already passes
opts.Thinking straight through), but the capability layer never declared
it — the editor's thinking section was hidden for every ollama model.
Declare Supported/CanDisable with an empty level set; ThinkingControls
now renders toggle-only for level-less providers (levels mode shows the
hint under the toggle; single mode drops the level select) instead of
the old hint-with-no-toggle dead end. The session-level thinking panel
stays hidden for boolean providers: the session API only carries
thinking_level, which cannot express a boolean.

Also flex-center the icon+text content of the ollama 刷新列表 and
dimension-check buttons — a raw t-icon (svg, no text baseline) aligned
against a bare text node rides the baseline and floats.

The capability sanity test now accepts empty SupportedLevels as a
legitimate boolean-thinking declaration while keeping the
DefaultLevel ⊆ SupportedLevels invariant.
Merge origin/main (9e55f74, 56 upstream commits) into the unified
invocation branch. Conflict resolution and porting decisions:

- internal/models/chat/* (deleted here, upstream added anthropic
  tool-use + langfuse tool metadata): ported the anthropic tool-use
  feature into invoke/adapters/anthropic_tools.go (invoke.ToolDef /
  invoke.ToolCall shapes, SSE tool_stream assembly with unclosed-block
  drop), and the langfuse tool_names/mcp_catalog metadata into the
  entry-builtin startLangfuse (invoke/langfuse.go + tests).
- internal/agent: took upstream's streamlined prompt policy (image
  requirement injection removed, sanitize orphaned tool results as
  escaped untrusted_tool_result, finalize reuses the live transcript);
  converted new upstream files (steer.go, tool_images.go) to invoke
  types; mcpCatalogDescriptionLen re-typed for the merged engine log.
- internal/agent/mockchat_test.go: new test double porting main's
  mockChat onto the invoketest seam (chunk conversion keeps unset
  ResponseType as answer, emits non-Done tool calls).
- openai stream bridge: a frame carrying finish_reason AND tool_calls
  no longer drops the calls (mixed-delta fall-through) — real bug the
  upstream MCP exposure integration test flushed out.
- FE ModelEditorDialog: kept this branch's contract (credentials-spec
  rendering, emit-confirm + parent-driven close per D1, explicit
  validations); re-adopted upstream's connection-field invalidation
  watch; the inputModalities watch moved below the formData
  declaration (TDZ on mount, caught by upstream's new flow test).
- FE chat: union of steer messaging (upstream) + per-request thinking
  level (this branch).

Verification: go vet clean; diff-scoped golangci-lint 0 issues; full
go test failure set identical to origin/main baseline (known fake-IP
DNS SSRF + PythonVerifier env failures only); FE type-check clean,
894/894 tests, vite build ok.
Session-level thinking was a level-only override (the on/off toggle was
a dead control: the QA API carried no thinking boolean). Rulings from
real-machine feedback:

- QA requests carry an optional thinking bool (nil = follow the agent
  or global default); knowledge-QA and agent paths apply it after the
  agent overrides fold in, so the panel toggle is a first-class
  per-request override.
- The panel now shows the agent's own thinking setting as the initial
  state (enabled + configured level, falling back to the provider
  default_level when the agent has none or it is not offered by the
  selected model); untouched state sends nothing and the backend chain
  resolves as before.
- The reset button is gone (the untouched state IS the reset); the
  trigger button lights up (brand color) whenever thinking is
  effectively on.
- Boolean-thinking providers (ollama think, empty SupportedLevels) are
  un-hidden from the session panel — the bool now travels, so the panel
  renders a bare toggle there.
- ThinkingControls loses the hideToggle prop (its only consumer needed
  the toggle hidden because the bool could not travel).

Model/agent/session switches still clear the override (model switch
drops a level the new model no longer offers, with the existing toast).
… list URLs

listURLFor only whitelisted /v1, /v1beta and /compatible-mode/v1 as
versioned suffixes, so an OpenAI-compatible base like Volcengine Ark's
/api/plan/v3 fell through to the bare-base branch and the probe hit
{base}/v1/models — a double-versioned path that 404s no matter what the
gateway serves (the frozen 'volcengine listing URL 404' audit finding,
reproduced here through the generic provider). Any path ending in a
bare v<digits> segment now appends /models directly; bare bases keep
the /v1/models convention. Also skip the remote-catalog probe while the
base URL is still empty (it could only ever answer 'base URL is
required' — pure log noise on every editor keystroke).
…-supply name

The DashScope always-thinking predicate only matched the ZHIPU/-prefixed
form (zhipu/glm-5.3), so a model record named glm-5.3 — the aliyun
direct-supply spelling — fell through to the hybrid branch and the
title-generation path's explicit thinking=false went out as
enable_thinking=false, which the vendor rejects with 400
'enable_thinking ... restricted to True'. The field is now suppressed
for any glm-5.3 spelling (bare, -flash, ZHIPU/-prefixed) and kimi-k3.
The main conversation in the reported session was unaffected — the 400
came from the async title generation, which shares the session's title
model.
The adapter requests extended thinking (budget_tokens from the resolved
level) but the response side never delivered it: the stream bridge only
handled text_delta frames, so thinking_delta — the carrier of the
model's reasoning — was silently dropped, and the aggregate/non-stream
paths ignored thinking content blocks. Real-machine symptom: with
thinking explicitly enabled (session override on, level high) the UI
never showed a thinking panel.

thinking_delta frames now bridge to StreamKindThinking (entry maps them
to ResponseTypeThinking), the SSE aggregate and the non-stream parser
fold thinking blocks into ChatResponse.Thinking, and the llm_debug
options line prints the thinking boolean — 'was the param requested at
all' is the first question every no-thinking debug starts with, and
ThinkingLevel alone could not answer it.

Note: whether the Volcengine agent-plan Anthropic endpoint actually
emits thinking blocks for deepseek models is vendor-side; with this fix
whatever the vendor sends now reaches the UI.
Drop-in color SVGs for 26 providers under
assets/img/providers/color/model/<provider>.svg — consumed by the
providerLogos registry (import.meta.glob; mono/ falls back to color
when a mono variant is absent).
CONTEXT.md and the three refactor ADRs are review material for this
branch's design discussions, not deliverables — untrack them and ignore
the paths so they stay on disk but out of the PR.
@Greenplumwine

Copy link
Copy Markdown
Contributor Author

关于 golangci-lint CI 失败的说明

本次 CI 失败由两个独立原因叠加,均不影响本 PR 的实际改动质量:

1. PR diff 超过 GitHub API 上限,action 的增量模式失效
本 PR 变更 458 个文件,超过 GitHub diff API 的 300 文件上限,action 拉取 patch 时返回 406 (too_large)(见日志首行),"only new issues on pull_request" 过滤无法工作,回退为对 cli/、client/ 两个模块的全量 lint。

2. 回退后报告的问题全部是存量基线,不在本 PR 改动范围内

  • cli/ 127 个、client/ 82 个问题全部位于本 PR 未触碰的文件(本 PR diff 中 cli/、client/ 文件数为 0,可核对 Files changed);
  • 这些 linter(errcheck/staticcheck/lll/revive)由两个模块各自的模块级配置启用,与本 PR 对根配置的改动无关(根配置仅新增注解驱动的 exhaustive 与 errcheck 豁免,只会减少不会新增报告项)。

本 PR 自身的 lint 状态:golangci-lint run --new-from-rev=origin/main ./...(根配置,diff-scoped)为 0 issues;一处 gofmt 疏漏已修复补推(da5edba3)。

后续:458 文件的 diff 对 reviewer 同样不友好(API 上限同样影响逐文件审阅),我们正在评估按依赖顺序拆分为多个 PR(统一调用核心 → 调用方迁移 → 前端)。欢迎对本 PR 的实际改动直接使用 diff-scoped lint 本地验证。

The workflow was added on the fork for local image builds and is not
part of the refactor — untrack it and ignore the path so it stays out
of this PR and future upstream-facing ones.
…role vocabulary

TextMessage("system", …)-style literals compile because Role is a typed
string and untyped constants convert implicitly — a v1 porting artifact
(v1 chat.Message.Role was a plain string with no constants). Typos in
literals compile too and fall through the adapters' exhaustive-enforced
switch defaults silently, so the constants turn a class of silent bugs
into compile errors. Mechanical sweep across the neutral layer
(callers + invoke's own seam tests); wire-layer protocol literals in
the adapters are untouched.
Three sites the mechanical pass missed: two multiline invoke.Message
composites with aligned fields (handler/model.go, handler/session/
image_upload.go) and one multiline TextMessage call in the engine's
empty-answer retry. The handler-layer types.Message literals (DB
persistence role vocabulary, a plain string by design) stay.
… modalities

The VLLM (vision) type is abolished: a vision model is a chat model that
declares image input. Type axis = wire-protocol shape (chat / embedding /
rerank / ASR, mirroring the invoke facets); input modality = capability axis
within chat. Output modality is rejected as a primary key — whisper vs
qwen-omni share audio-in/text-out but serve disjoint protocols (ADR 0004).

Backend:
- drop ModelTypeVLLM; wizard slot dispatch switches to explicit descriptor
  slots (LLM and multimodal slots share KnowledgeQA now); KB config assembly
  resolves the image-understanding model via VLMConfig.ModelID instead of a
  type scan
- debug endpoint: image debug folds into the chat case, gated by
  GetSupportsVision with a 400 for image uploads to text-only models
- create/update validate the closed type vocabulary and normalize chat
  InputModalities: text always present, values outside text/image/audio
  stripped (a catalog prefill must not resurrect banned values)
- provider catalog drops per-type VLLM entries (all equaled the chat URL)

Data migration 000096 (postgres) / 000017 (sqlite): VLLM rows become
KnowledgeQA with text+image stamped — the type WAS the vision declaration,
so this overrides an explicit text-only shard; other modalities (audio) are
preserved. Irreversible by design; verified against all four legacy row
shapes on a live jsonb schema and fresh sqlite databases.

Frontend: type tabs 5→4 with vision badge derived from modalities; editor
merges the vllm form (text checkbox fixed on, video removed, catalog prefill
whitelist-filtered); slot pickers select by the 'vision' pseudo-type
(KnowledgeQA ∧ supports_vision); debug drawer goes capability-driven; wkc
vlm kind lands as KnowledgeQA + supports_vision with name-based dedup.

client/cli: SDK type vocabulary converged; CLI help updated.

Note: pre-000096 code reads the converged rows fine, so down is a no-op.
…ible endpoints

自定义 OpenAI 兼容接口(ProviderGeneric)不在 thinkingCapsFor 厂商表里,
落 default 返回 Supported=false,导致编辑器对自定义端点整块隐藏思考开关
与档位——而运行时走的是同一个 openai 适配器(reasoning_effort),会话级
思考早已可用。声明与运行时能力对齐:加入 standard 档位表(low/medium/
high,可关闭,默认 medium)。

用户可见后果:自定义端点的对话模型编辑器恢复思考配置区;ark plan 等
自定义协议网关(如 doubao-seed / deepseek-v4.1-flash)可在模型级配置
思考,与阿里云/火山引擎厂商条目行为一致。
…r dispatch

Agent 直传图像时,附件管线产出的是 resource:// 句柄(本地资源表引用),
此前 chatExecute 未做入口级解析,openai 线协议把 resource:// 原样发给厂商,
火山 Ark 直接 400:"Only base64, http or https URLs are supported"。

按 design §6.6 的既定归宿补上入口级预处理(P1c 迁移项):chatExecute 统一
把 resource:// / local:// / storage:// 引用经 LocalImageResolver 内联为
data URI 后再分发适配器;data:/http(s) 引用原样透传,解析失败的引用保持
原样让厂商错误保持响亮。ollama 适配器的同款私有判断收编为
invoke.IsApplicationStoredImage。anthropic/gemini 适配器目前完全没有图像
映射(静默丢图),属于另一个待补缺口,不在本修范围。

真机回归对应:智能体开启图片上传后,doubao-seed(视觉对话模型)会话直传
图片不再 400,图像以 data URI 到达厂商。
模型管理页的思考主开关此前在运行时零消费——会话/智能体层照常发送思考
参数(用户真机发现:模型关了思考,对话面板与智能体配置仍处于开启态)。
按用户拍板的语义落地"模型级开关 = 硬门":

- 后端:ModelConfig 透传记录的 Chat.ThinkingEnabled,foldChatOptions
  (Chat/ChatStream 单点)在开关为显式 false 时强制 Thinking=&false 并清空
  档位——会话/智能体层要开也无效;nil(从未碰过编辑器开关的记录)不设门,
  行为不变。显式 &false 对支持关闭参数的协议(如 dashscope
  enable_thinking=false)是比"省略"更强的禁用信号。
- FE 对话页:所选模型的 chat 分片 thinking_enabled === false 时,会话思考
  触发按钮 + 面板整块消失(thinkingSupported 门控扩展),且已触碰的思考
  状态不再随请求发送。
- FE 智能体编辑器:绑定模型主开关关闭时 caps 置 undefined,ThinkingControls
  整块置灰并显示"当前模型不支持思考"(已存配置保留——智能体可换模型,
  数据不删,运行时硬门兜底)。

测试:entry_seam 新增 TestFoldChatOptionsMasterSwitchOff(强制关、显式关、
免拷贝、nil 不设门四态);全量 FE 893/893、tsc 0 错误。
并行会话收编:思考不可用(厂商未声明 / 模型主开关关闭)的 UX 从
"整块置灰 + 不支持说明"统一为"整块隐藏"——与会话面板消失语义一致;
thinking.unsupportedHint 键五语言同步移除,智能体编辑器思考行随
agentThinkingAvailable 一并隐藏。已存配置仍由调用侧保留。
…odel-mgmt-unified-invocation

# Conflicts:
#	internal/database/migration_sqlite_versioned_schema_test.go
#	internal/models/chat/anthropic_tools.go
#	internal/models/chat/ollama.go
#	internal/models/chat/openai_request.go
#	internal/models/invoke/special_tokens.go
#	internal/models/invoke/special_tokens_test.go
@Greenplumwine
Greenplumwine deleted the feat/model-mgmt-unified-invocation branch September 18, 2026 09:26
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