Skip to content

feat(chat): Composer 模型行显示输出上限与供应商图标 - #1039

Open
lenmei233 wants to merge 5 commits into
vastsa:mainfrom
lenmei233:feat/composer-model-output-limit-icons
Open

lenmei233 wants to merge 5 commits into
vastsa:mainfrom
lenmei233:feat/composer-model-output-limit-icons

Conversation

@lenmei233

@lenmei233 lenmei233 commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

改动说明

Issue #1028 提了三件事:显示模型上下文与输出长度、获取模型和图标,并吐槽自制图标"好难看、不统一"。核对主线后:上下文长度上游已经做了;输出长度是真实缺口;图标完全缺失。本 PR 补齐后两项。

一、Composer 模型行显示输出上限

行内数据一直都在(ModelInfo.maxTokens / limit.output),只是选择器没读。现在每行与设置行读法一致地显示「上下文窗口 · 输出上限」一对数值,共用同一个紧凑格式化函数;服务未发布的那一项显示破折号,不编造数字。刻意保持窄范围:选择器只报告模型的已发布值,用户在「高级」里按模型填写的覆盖值仍留在设置页与运行时,与上下文窗口现有行为一致。

二、模型行由厂商标识

选择器此前只用文字标识每一行,列表滚过标题后就看不出某行属于哪个厂商。两轮迭代后定型的做法:

  • 按显示名选图标不可靠——行名可由用户编辑、多行可归属同一目录厂商、自定义名会借用他人美术资源。运行时从 models.dev 拉图标同样不可靠:CSP 多一个外部源、离线下列表不一致、还会发出暴露用户配置了哪些供应商的外发请求。
  • 因此图标按解析出的目录 key 选择,由主进程填充、渲染层不做二次推导,所以不存在第二事实来源,改过名的行也不会显示别的厂商图标。
  • 一个自定义端点往往同时服务多个厂商,所以每行优先用它自己的厂商 key(ModelInfo.catalogVendorKey),而不是整个 provider 组的 key;组标题没有行可读,才用 provider 级 key。
  • 该厂商归属从模型 ID 的厂商路由(openai/gpt-6-astra 指名 openai)读出,扫描范围是目录为该 ID 自带的候选索引(与 findModel 同一套),再经既有的提供商别名表映射。它指名的是权重归属者,而不是本次查找恰好命中的记录由谁发布——否则中转网关自己的记录会让 MiniMax 行显示网关图标。纯元数据:不改任何能力、上限或线模型 ID,findModel 本身未动。
    它指名的是权重归属者,而不是本次查找恰好命中的记录由谁发布——否则中转网关自己的记录会让 MiniMax 行显示网关图标。纯元数据:不改任何能力、上限或线模型 ID,findModel 本身未动。
  • 对不带路由的裸 ID 用第二条信号:若发布者自身的 key 就是已知厂商,即该厂商在描述自家模型——小米的 mimo-v2.6-pro 因此归到 xiaomi(xiaomi 据此加入 MODEL_VENDOR_PREFIXES)。这条规则对中转网关是封闭的:网关的 key(opencode-go、nano-gpt、requesty)不在厂商集合里,所以只是转载某模型的网关无法冒领其图标;厂商路由始终优先于发布者 key。这也顺带修好了 muse-spark-1.3-contributor(原显示通用图标,实际由 meta 自己发布)。两种信号都指认不出的 ID 仍走回退。
    图标随发布打包(16 个厂商,UPSTREAM.md 记录来源、抓取日期、MIT 许可与刷新方式),并由 scripts/build-provider-marks.mjs 编译为内联 currentColor 组件。它们是路径而非图片:运行时不拉取、没有会失败的 <img>、也不占 CSP 表面;继承主题文字颜色使各厂商保持同一视觉重量,而不是堆一排高饱和彩色 logo。目录无法指认厂商的行(含未知自由格式 ID)统一回退到共享通用图标——任何一行都不会缺少身份标识,缺失美术资源只是外观退化。

图标是装饰性的(aria-hidden),未新增任何用户可见字符串,十个 locale 不变。

途中被否掉的方案(已记录进 ADR)

第一版把 SVG 当资产 URL 导入、用 CSS mask-image 上色。它只在 Vite 把资产输出为真实文件时生效:4 KB 以下的资产会被内联成 data: URL,而 mask 无法从中取样绘制,元素就只剩自己的背景色——表现为纯灰方块。本次打包的多个图标都在该阈值以下,所以故障是局部的、看起来像某些厂商的单独 bug。改为内联 path 后阈值、<img>、mask 表面一并消失。

实现要点

  • 两个纯附加可选字段:ProviderPublic.catalogProviderKey、ModelInfo.catalogVendorKey。无 IPC 契约变更(仅多两个属性)、无数据库迁移、无持久化格式变更、无 Plugin SDK 变更。不发送该字段的旧生产者正常工作并走回退。
  • 生成文件 src/lib/provider-marks.tsx 已提交(构建无需先跑生成器),build:deps 仍会重新生成,并有测试在「重新生成结果与已提交文件不一致」时失败——避免改了 SVG 却发出旧美术资源。
  • 新增 ADR provider-brand-marks 记录决策、备选方案与被否的 mask 方案;同步中英两份规格、UX 组件规范与决策日志(本 PR 决策号为 D628,主线此前已占用 D625)。

测试

  • 厂商归属解析跑在真实发布快照上而非 fixture——fixture 只能证明 resolver 自洽。覆盖:每个 ID 的归属厂商、无名路由保持无名、结论不依赖记录由谁发布、provider 级解析不回退。
  • 图标表 / 组件 / 资产目录三者一致;同一厂商的兄弟目录 key 共用一张美术资源;每个 vendored 文件都是可上色单色 path、无 script 与远程引用;生成可复现。
  • 渲染契约覆盖限额成对、图标出现在组标题与行首、未知厂商回退。
  • helper 单测覆盖两种已发布取值形态与"未发布"。
  • 上述新增断言均已在改动前验证为失败、改动后通过。
  • Real-render 冒烟:把真实 ComposerModelList 放进 React SSR 渲染,确认品牌图标出现在组标题与行首、限额成对、未发布为 —、未知供应商回退通用图标。

本地实机验证(Windows)

除了静态检查,本 PR 是在跑起来的真实客户端里确认的:在独立 worktree 用 PI_DESKTOP_DEV=1 启动(数据目录 ~/.pi-desktop-dev,与真实 profile 隔离),Rust host 握手与 sidecar 均正常启动。据此定位并修掉了两个只有实机才能看到的问题——自定义网关上全部回退、以及 mask 导致的局部灰块。

效果预览

image

已验证

JS 全量构建、desktop typecheck、biome lint、check:pr-base、check:agent-policy 均通过。shared 1110 项全绿。desktop 测试 2958 项中 2899 通过;52 项失败与未经本 PR 改动的 origin/main@05225d9d6 完全一致(在临时 worktree 实测比对,非推断),合并不引入回归。

未验证

  • test:e2e:transcript 未能跑通,但与本次改动无关:该套件需要 Electron 渲染进程,本机跑不通;已在 origin/main 上另建独立 worktree 跑同一套,失败消息一字不差,故确认为环境限制。
  • 未跑会向真实模型发起生图请求的 e2e-image-chat(需明确授权);未跑 verify:ui:*(未获授权)。
  • 本次未改 Rust,未运行 cargo 相关检查;CI 的 Rust job 自行验证。
  • 图标覆盖 16 个精选厂商。厂商集合之外的模型(如 StepFun)沿用通用图标——这是刻意取舍,见 ADR「无一行声称它无法证明的品牌」;补一个新厂商只需加一个 SVG + 一个厂商集合条目。

合并期间的冲突处理

origin/main 在评审期间前进了,做过一次非破坏性合并(分支已共享,不做 rebase 强推)。六个文件冲突均为"两侧都对",分别保留双方;其中主线已占用 D625,故本 PR 决策号改为 D628(全仓空闲),中英文两份决策日志同步。

fixes #1028

The Composer picker already reported each model's context window but not its
output ceiling, so a user comparing models there saw half the shape Settings
shows on the very same row. The published record already carries both limits
(ModelInfo.maxTokens / limit.output); the row simply was not reading it.

Each row now renders the pair through the one shared compact formatter, in
the same order and the same typography as the settings row, so "200K · 8K"
means the same thing in both places. A limit the service never published
renders as an em dash — the row never invents a number — and a record that
publishes neither keeps the row free of a limits pair altogether.

Scope stays deliberately narrow: the picker reports what models.dev states,
and a per-model override typed into Advanced keeps its existing home in
Settings and at runtime, exactly as the context window already behaves.

Tests cover the helper (both published-value shapes plus an unpublished one)
and the row contract, and both fail against the previous implementation.

fixes vastsa#1028
The Composer model picker named each row only by text, so a list scrolling
past its heading stopped saying which vendor a model belonged to. The reporter
of vastsa#1028 hand-drew icons and found them "ugly and inconsistent" — the expected
result of inventing per-row artwork by hand.

Picking a mark by display name would be unsound: a row's name is user editable,
several rows resolve to one catalog vendor, and a custom name would borrow a
vendor's artwork. Fetching marks from models.dev at runtime would be just as
unsound: an extra origin in the CSP, a different list offline, and an outbound
request naming the providers a user configured. So the mark is chosen by the
catalog key that already places the row's metadata — resolved once in Main,
through the alias mapping that exists today, with no second source of truth in
the renderer — and the artwork ships with the release.

A curated set of monochrome marks is vendored with an UPSTREAM.md recording
source, date, licence, and refresh procedure. They render through a CSS mask,
so a run of vendors keeps one visual weight and follows the theme text color
instead of filling the list with saturated logos. A key with no vendored mark,
including every row the catalog cannot place, falls back to the shared generic
mark: no row is left without an identity, and a missing artwork is a cosmetic
fallback rather than a broken list.

Tests cover the table/asset/enforcement set staying in step, the mark staying a
monochrome maskable path, the vendored files carrying no script or remote
reference, the enrichment wiring, and the row/heading markup. The two new
rendering assertions fail against the previous implementation.

fixes vastsa#1028
origin/main advanced past this branch's base while the pull request was under
review. Integrate it with a merge rather than a rebase, because the branch is
already shared through the open PR and rewriting its history would force a
push onto it.

Six files conflicted, and none of them needed a judgement call about which
change wins — both sides were right:

- `provider-ipc.ts` — origin/main restructured the catalog dependency into a
  multi-line union and added `anthropicThinkingFor`; keep that shape and add
  `providerKeyForRow` beside it, since both surfaces are used.
- `provider-catalog.ts` — origin/main replaced the enrichment guard with
  `catalogModelConfig.source !== "generic"`. Keep the new guard and keep the
  `catalogProviderKey` spread; they publish different fields.
- `ComposerModelList.tsx` — origin/main turned the reasoning/vision badges
  into icon marks with an accessible label, and read the configured alias for
  the row label. Keep both, and keep the output limit beside the context
  window, so a row now reads: mark, id, badges, `context · output`.
- `composer-menus.css` — origin/main deleted the thinking-list truncation rule
  because nothing references it any more. Respect that deletion and keep the
  two new limit rules.
- Both decisions logs appended a section at the end. Keep both entries.

One real collision surfaced here: origin/main had already spent **D625** on the
model-settings redesign, and this change had claimed the same id for the
provider brand marks. Renumber ours to D628 — the next id free across the
repository — in both the English log and its Chinese mirror, so the mirror
stays a one-to-one translation of the source of truth.

Validation: desktop typecheck and a full JS build pass. The desktop suite
reports 52 failures, which is the same set the untouched `origin/main` reports
in this environment — measured in a throwaway worktree at `origin/main`, not
assumed — so the merge introduces no regression. Seven of those failures are
new only relative to this branch's previous base, because the bundled
models.dev snapshot tests that origin/main added do not load a snapshot here;
they fail identically without this change.
models.dev publishes Xiaomi's mimo models under a `xiaomi` key with ids like
`mimo-v2.6-pro` — no `xiaomi/` route anywhere in the id. Reading the owner off
the id alone therefore named nothing, and every mimo row kept the generic mark
even though the catalog states the owner outright.

So add a second owner signal: when a publisher's own key is a known vendor, that
is the vendor describing its own model, which is the same claim a route in the id
makes. `xiaomi` joins `MODEL_VENDOR_PREFIXES` so the publisher qualifies. This is
bounded in the direction that matters: a reseller never qualifies, because its
key (`opencode-go`, `nano-gpt`, `requesty`) is not in the vendor set, so a
gateway whose row merely republishes a model cannot claim its mark. A route
still wins, because the id states its owner more directly than the host that
listed it.

The effect is not limited to Xiaomi. `muse-spark-1.3-contributor` was generic for
the same reason and now resolves to `meta`, which is who publishes it — the rule
covers the class rather than one vendor.

Vendor `xiaomi.svg` from models.dev and map the three `xiaomi-token-plan-*`
billing-region keys to the same artwork, since they publish the same models.

Tests run against the shipped snapshot rather than a fixture, because a fixture
would only prove the resolver agrees with itself: every mimo spelling through
the vendor route, the bare id, and the resellers' variants; a reseller not
becoming the owner of an id it republishes; and a spelling no record owns still
resolving to nothing rather than being guessed.

fixes vastsa#1028
@lenmei233
lenmei233 force-pushed the feat/composer-model-output-limit-icons branch from c03076f to efbd72e Compare September 26, 2026 08:23
@lenmei233

Copy link
Copy Markdown
Contributor Author

@vastsa 经过多次完善,我认为已经可以merge了

This branch has not been deployed

No deployments
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.

[Feature] 额外功能

1 participant