Skip to content

feat(providers): support arbitrary model lists and optional fast models - #159

Merged
GoDiao merged 5 commits into
mainfrom
feat/issue-120-provider-model-lists
Oct 24, 2026
Merged

GoDiao merged 5 commits into
mainfrom
feat/issue-120-provider-model-lists

Conversation

@AIR-Xie

@AIR-Xie AIR-Xie commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary / 概要

This PR resolves #120 by replacing the four visible Claude role slots with an ordered provider model list and an optional provider-local fast model. It adds model discovery, migration, runtime role mapping, editable display names, and cross-provider session selection.

本 PR 解决 #120:将用户可见的四个 Claude 角色槽位替换为有序的 Provider 模型列表,并支持 Provider 内可选的快速模型;同时加入模型发现、旧数据迁移、运行时角色映射、显示名称编辑和跨 Provider 会话选模。

Issue #120 implementation — English

Issue requirement Implementation
1. Data model, migration, presets SavedProvider.models is now an ordered { id, name? }[]; fastModel optionally references an id in that list. Presets use the same shape. Legacy main, sonnet, opus, haiku values migrate in that order, are deduplicated, and set fastModel from a distinct haiku.
2. Server-side model discovery Discovery goes through the server/upstream client so credentials stay off the browser. OpenAI-compatible and Anthropic /v1/models responses preserve ids and optional names; Anthropic discovery follows has_more, last_id, and after_id, requests limit=1000, and has a 10-page safety cap.
3. Runtime role mapping The selected session model fills ANTHROPIC_MODEL, Sonnet, and Opus roles. The provider fast model fills Haiku and small-fast roles, falling back to the selected model. Titles and scheduled tasks follow the same provider-local rules.
4. Desktop editor and picker Users can fetch candidates, add arbitrary ids, edit display names, remove/reorder models, select a fast model, and choose models across providers. Edit-mode discovery uses the unsaved form values currently on screen.
5. Tests Coverage includes legacy migration, duplicates and empty values, idempotency, model discovery and errors, Anthropic pagination, names and ids, runtime mapping, picker behavior, settings JSON preservation, and fast-model update validation.

Review follow-ups implemented

  1. Unrelated Settings JSON edits now preserve all existing model rows, names, order, and fast-model selection; only changed runtime ids are merged.
  2. Provider mode no longer reads or writes the legacy managed model/modelContext fields, and ids containing : such as qwen3.5:9b are kept intact.
  3. Anthropic model discovery now supports pagination with a bounded loop and cursor validation.
  4. Editing a provider discovers models from the current form values; the obsolete saved-provider discovery endpoint and client/store methods were removed.
  5. Discovery shows localized success counts, empty-result status, and accessible alert errors in English and Chinese.
  6. Migration and provider-update tests cover empty input, duplicates, idempotency, fast-model removal/null, and rejection of an explicitly invalid fast model.
  7. Unused picker parameters and translation keys were removed. The two point-in-time report files were deleted and the implementation/verification record was moved into this PR description.

Issue #120 开发对照 — 中文

Issue 要求 开发内容
1. 数据模型、迁移和预设 SavedProvider.models 改为有序的 { id, name? }[];fastModel 可选,并且必须引用列表中的模型 ID。预设使用相同结构。旧版 main、sonnet、opus、haiku 按该顺序迁移、去重,并在 haiku 与 main 不同时设置快速模型。
2. 服务端模型发现 模型发现由服务端 upstream 客户端完成,API key 不进入浏览器。OpenAI-compatible 和 Anthropic /v1/models 均保留模型 ID 和可选名称;Anthropic 支持 has_more、last_id、after_id 分页,使用 limit=1000,并设置 10 页安全上限。
3. 运行时角色映射 会话选中的模型写入主模型、Sonnet 和 Opus 运行时角色;Provider 的快速模型写入 Haiku 和 small-fast 角色,未配置时回退到会话模型。标题和定时任务使用相同的 Provider 内映射规则。
4. 桌面端编辑器与选择器 支持获取候选模型、添加任意 ID、修改显示名称、删除/排序模型、选择快速模型,以及跨 Provider 选择会话模型。编辑 Provider 时,模型发现使用屏幕上尚未保存的表单值。
5. 测试 覆盖旧结构迁移、重复项和空值、迁移幂等性、模型发现和错误路径、Anthropic 分页、名称与 ID、运行时映射、选择器、Settings JSON 数据保留及快速模型更新校验。

Review 后续 7 项修复

  1. 修改无关 Settings JSON 字段时,保留已有模型行、名称、顺序和快速模型;只有确实变化的运行时模型 ID 会被合并。
  2. Provider 模式不再读取或写入旧的 model/modelContext;qwen3.5:9b 等包含冒号的模型 ID 保持完整。
  3. Anthropic 模型发现增加带上限和游标校验的分页。
  4. 编辑态发现模型使用当前表单值;删除旧的“按已保存 Provider 发现模型”服务端接口以及桌面端方法。
  5. 增加中英文的发现成功数量、空结果状态和可访问的错误提示。
  6. 迁移和 Provider 更新测试新增空输入、重复项、幂等性、快速模型删除/null,以及显式非法快速模型拒绝场景。
  7. 删除未使用的选择器参数和翻译键;删除两份阶段性报告文件,将开发与验证记录统一移入 PR 描述。

Schema / Schema 说明

type ProviderModel = {
  id: string
  name?: string
}

type SavedProvider = {
  // other provider fields...
  models: ProviderModel[]
  fastModel?: string
}

Rules / 规则:

  • models is ordered; the first model is the provider default when no session/task override exists. / models 有顺序;没有会话或任务覆盖值时,第一项是 Provider 默认模型。
  • Model ids must be non-empty and unique inside a provider. / 同一 Provider 内模型 ID 必须非空且唯一。
  • name is display-only; runtime requests always use id. / name 只用于显示,运行时请求始终使用 id。
  • fastModel, when present, must equal an id in models. / fastModel 存在时必须引用 models 中的 ID。
  • Legacy migration order is main → sonnet → opus → haiku, with duplicate ids removed. / 旧结构按 main → sonnet → opus → haiku 迁移并去重。

Runtime mapping / 运行时映射:

Runtime role Value / 值
ANTHROPIC_MODEL, ANTHROPIC_DEFAULT_SONNET_MODEL, ANTHROPIC_DEFAULT_OPUS_MODEL selected session/task model id / 会话或任务选中的模型 ID
ANTHROPIC_DEFAULT_HAIKU_MODEL, ANTHROPIC_SMALL_FAST_MODEL provider fastModel, otherwise selected model / Provider 快速模型,未配置则使用选中模型

Validation / 验证

Automated / 自动化:

  • Desktop TypeScript check: passed. / Desktop TypeScript 检查通过。
  • Review-focused desktop tests: 21 passed, 0 failed. / Review 相关桌面端测试 21 个通过、0 个失败。
  • Focused server tests: 174 passed, 3 skipped, 0 failed. / 服务端定向测试 174 个通过、3 个跳过、0 个失败。
  • Vite production build: passed, 4,419 modules transformed. / Vite 生产构建通过,转换 4,419 个模块。
  • git diff --check: passed. / git diff --check 通过。

Manual Ollama validation / Ollama 人工验证:

  • Local Ollama models used: qwen3.8:latest, qwen3.5:9b, and qwen3.5:4b.
  • 本地 Ollama 使用的模型:qwen3.8:latest、qwen3.5:9b、qwen3.5:4b。
  • PASS: changing unrelated Settings JSON values preserved five model rows, names, order, and fast-model selection. / PASS:修改无关 Settings JSON 值后,五条模型、名称、顺序和快速模型均保持不变。
  • PASS: session runtime kept the complete colon-containing id qwen3.5:9b; the conversation succeeded and legacy model/modelContext remained unchanged. / PASS:会话运行时保留完整的 qwen3.5:9b;对话成功,旧 model/modelContext 未被改写。
  • PASS: edit-mode discovery used the current unsaved URL and showed both error and success/count states. / PASS:编辑态模型发现使用当前未保存的 URL,并正确显示错误与成功/数量状态。
  • PASS: removing the selected fast model cleared the fast-model setting. / PASS:删除已选快速模型后,快速模型设置被正确清除。

Pending / 待验证:

  • Real OpenAI/OpenAI-compatible API validation is pending. / 真实 OpenAI/OpenAI-compatible API 仍待验证。
  • Real Anthropic API validation, including live pagination, is pending; pagination is covered by automated two-page tests. / 真实 Anthropic API(包括实机分页)仍待验证;两页分页场景已有自动化测试。
  • Native Windows Tauri sidecar rebuild was blocked by Bun target-package download error CERT_NOT_YET_VALID; manual validation used the current backend and frontend source in browser mode. / Windows 原生 Tauri sidecar 重编译因 Bun 目标包下载出现 CERT_NOT_YET_VALID 而受阻;人工验证使用当前前后端源码的浏览器模式完成。

Closes #120

@GoDiao GoDiao left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking on #120! This is a big one, and the core of it is right. Every model role is now read from the ordered list. The role env mapping follows the table in the issue. The migration keeps every id. The settings UI covers add, rename, remove and fast model. We went through it against the issue line by line, and almost everything is there. Below is what we'd like changed before merging, most important first.

1. Editing the settings JSON wipes the model list (must fix).
ProviderFormModal.tsx L1046-1062: any valid edit to the settings JSON textarea rebuilds models from the four env keys. That happens even for an unrelated key like skipWebFetchPreflight. updateSettingsJsonModels only ever writes the main and fast ids into those keys, so a 5-row list with display names collapses to at most 2 rows, without names, and that's what gets saved. Only touch the list when the pasted ids actually differ from the current mapping, and merge them in instead of replacing:

const current = normalizeModels(models)
const runtime = runtimeModelMapping(current, fastModel)
if (pastedMain && (pastedMain !== runtime.main || (pastedFast || pastedMain) !== runtime.fast)) {
  const head = current.find((m) => m.id === pastedMain) ?? { id: pastedMain }
  const merged = [head, ...current.filter((m) => m.id !== pastedMain)]
  if (pastedFast && !merged.some((m) => m.id === pastedFast)) merged.push({ id: pastedFast })
  setModels(merged)
  setFastModel(pastedFast && pastedFast !== pastedMain ? pastedFast : '')
}

Please add a test as well: a list of 5 named rows, edit an unrelated key, and assert that the list is unchanged.

2. Provider mode reads and writes model in settings.json again (must fix).
#150 settled this rule (docs/session-state-tech-design.md:553): in provider mode, the managed model / modelContext are neither read nor written, because a stale value there used to override the provider's model. This PR brings it back:

  • GET /api/models/current returns the saved model first, without checking that it belongs to the active provider.
  • PUT writes it again, and splits it at :, so qwen3.6:27b becomes qwen3.6 + context 27b.
  • resolveProviderDefaultModel gained an explicitModel branch.

Example: a user upgrading with claude-haiku-4-5 · 1m left in settings.json sees that model in the input box, while sessions run on the provider's first model. The test that pinned this, "should use the active provider main model over stale saved model keys" in settings.test.ts, was rewritten into a different case.

Please go back to main's behavior:

  • In GET's provider branch, use resolveProviderDefaultModel(activeProvider, managedSettings) and no context tier.
  • In PUT, return { ok: true, model: modelId } early when a provider is active, before any splitting.
  • Drop the explicitModel branch.
  • Restore the stale-model test.
  • Update the models.main wording at line 553 of that design doc to the list.

3. Anthropic model discovery stops at 20.
fetchUpstreamModels (upstream/client.ts L118-173) makes one GET /v1/models call. The Anthropic Models API pages with a default limit of 20 (has_more / last_id / after_id, limit up to 1000), so Anthropic-format providers get a silently truncated list. Send ?limit=1000 for the Anthropic format, and follow has_more with a small page cap. Please add a two-page test.

4. "Fetch models" in edit mode can hit the old endpoint.
When the API key field is empty, for example on an Ollama provider, handleDiscoverModels calls discoverProviderModels(provider.id). The server then uses the saved baseUrl / apiFormat, not what's on screen. Change the base URL, click Fetch, and you get the old host's models. Since saving with an empty key already clears the key, this branch doesn't buy anything. Always call discoverModels with the current form values. If nothing else uses POST /api/providers/:id/models and its store/api methods, they can go too.

5. A successful fetch shows nothing.
Results only land in the hidden <datalist>, so a fetch that returned 40 models, one that returned 0, and not clicking at all look the same. Please add a line under the button with role="status", such as "40 models found" or "No models returned" (en and zh keys). Also add role="alert" on the error text.

6. Tests for the migration and the fast-model update path.

  • persistence-upgrade.test.ts: the issue asks for "duplicates and an empty haiku". The fixture used to have haiku: '', and this PR changed it to 'model-fast', so that case is now gone. Please cover these cases. Then run the upgrade a second time and assert that nothing is migrated and no second .bak file is written; this runs on every launch.

    • { main: 'm', haiku: '' }: the list is [m], with no fastModel.
    • { main: 'm', haiku: 'm', sonnet: 's', opus: 'm' }: the list is [m, s], with no fastModel.
    • All four slots empty: the list is [].
  • providers.test.ts, updateProvider: there's nothing for fastModel yet, though every desktop save sends it. Please cover two cases: removing the fast model's row clears it, and fastModel: null clears it. While there, a small asymmetry: an explicit fastModel that isn't in the new models is silently dropped on update, but rejected on create. This makes them match:

    if (input.fastModel === null || (input.fastModel === undefined && input.models !== undefined && !updated.models.some((m) => m.id === updated.fastModel))) {

7. Small cleanups.

  • ModelSelector.tsx: buildProviderChoices no longer uses _availableModels. Drop the parameter, the argument and the useMemo dep.
  • en.ts / zh.ts: these 10 keys are now unused, so please remove them from both files:
    • mainModel, haikuModel, sonnetModel, opusModel
    • the four *ContextWindow keys
    • modelMapping, sameAsMain
  • docs/ISSUE_120_IMPLEMENTATION_REPORT_en.md / _zh.md: these are point-in-time status notes ("Complete locally", "pending", a report date), and they'll go stale right after merge. Please delete them, and move the requirement table and the list of pending real-API checks into the PR description.

No need to touch the CHANGELOG; we'll add the entry after merging. Once these are in, we'll run the manual checks on our side against real OpenAI-compatible and Anthropic providers. Thanks again, this unblocks #119 and #105!

@GoDiao GoDiao left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick turnaround! We went through 67e6bda against all seven points, and every one is addressed, with the tests we asked for. Two things left, both in ProviderFormModal.tsx:

1. Typing a model id in the settings JSON leaves a row for every prefix (our fault).
This comes from the merge snippet we suggested. We didn't think about typing. The textarea syncs on every keystroke, and every intermediate value inside the quotes is valid JSON. Each keystroke compares the pasted id with the head of the list from the previous keystroke. So it inserts a new head and keeps the old one. Changing A to gpt-4o in [A, B, C] ends with [gpt-4o, gpt-4, gpt-, gpt, gp, g, A, B, C]. Backspacing behaves the same way.

Merging against a snapshot taken when the textarea gains focus fixes it. Each keystroke then re-merges onto the same base:

const modelsSnapshotRef = useRef<{ models: ProviderModel[]; fastModel: string } | null>(null)

<textarea
  onFocus={() => { modelsSnapshotRef.current = { models, fastModel } }}
  onBlur={() => { modelsSnapshotRef.current = null }}
  ...

const base = modelsSnapshotRef.current ?? { models, fastModel }
const currentModels = normalizeModels(base.models)
const runtime = runtimeModelMapping(currentModels, base.fastModel)

Please add a test as well. Fire changes for g, gp, gpt on ANTHROPIC_MODEL, then assert the list gained only the final id.

2. Fetched models outlive the endpoint they came from.
After a fetch, changing the base URL, API key, API format or auth strategy keeps the "N models found" line and the suggestions from the old endpoint. Today only a preset change clears them. A fetch that is still in flight can also land after an edit and write the old endpoint's models back. Please clear the result on those edits, and drop responses from a superseded request:

const discoveryRequestRef = useRef(0)
const resetDiscovery = () => {
  discoveryRequestRef.current++
  setDiscoveredModels([])
  setDiscoveryError(null)
  setDiscoveryCount(null)
}

Call resetDiscovery() in the four connection handlers and in the preset change. In handleDiscoverModels, take const requestId = ++discoveryRequestRef.current. After the await, and in catch / finally, return early when requestId !== discoveryRequestRef.current. A test that fetches and then edits the base URL should assert that the status line and the suggestions are gone.

Optional while you're there: fetchModels in providerService.ts now has only one caller, discoverModels, so it can be inlined.

Once these are in, we'll run the real-API checks on our side. Thanks!

@AIR-Xie

AIR-Xie commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator Author

Final review follow-up and manual reproduction record / 最终 Review 修复与人工复现记录

Implemented in 958af98 (fix(providers): stabilize model discovery editing).

Requirement completion / Requirement 完成情况

  • The seven items from the first owner review remain complete; the owner confirmed all seven against 67e6bda.
  • Latest requirement 1 is complete: Settings JSON model merging now uses the model-list snapshot captured when the textarea receives focus. Progressive typing such as g → gp → gpt is always merged against the same snapshot, so intermediate prefixes cannot accumulate as model rows.
  • Latest requirement 2 is complete: changing the preset, Base URL, API key, API format, or auth strategy clears discovery results and invalidates the in-flight request. Superseded success, error, and completion updates are ignored.
  • Manual-review feedback is also complete: model discovery automatically recommends a fetched model when the Model ID is empty; the picker uses the shared styled Dropdown used by API Format; stale native/browser suggestions are no longer used; genuinely hand-entered model ids remain editable.
  • The optional suggestion to inline providerService.fetchModels was intentionally left unchanged because it is not a behavioral or merge requirement.

Automated checks for this commit:

  • ProviderFormModal.test.tsx: 12/12 passed, including progressive JSON typing and superseded discovery responses.
  • Desktop TypeScript (tsc --noEmit): passed.
  • Vite production build: passed, 4,419 modules transformed.
  • git diff --check: passed.

Manual environment:

  • Local Ollama: http://127.0.0.1:11434
  • Models: qwen3.8:latest, qwen3.5:9b, qwen3.5:4b
  • Current backend and frontend source were run in browser mode.

A — Settings JSON progressive model editing

Steps:

  1. Open Settings → Providers and edit the Ollama provider containing the existing ordered/named model list and fast-model selection.
  2. Focus the Settings JSON textarea. This focus event is important because it captures the merge snapshot.
  3. Change ANTHROPIC_MODEL to qwen3.5:9b through normal character-by-character editing.
  4. Save, close, and reopen the provider.

Expectations:

  • Exactly one complete qwen3.5:9b row exists.
  • No intermediate prefix rows such as q, qw, qwen, or qwen3.5: exist.
  • qwen3.5:9b becomes the first/default model because ANTHROPIC_MODEL selects the main model.
  • All original model rows remain present.
  • Existing display names are preserved.
  • The relative order of the other models is unchanged.
  • The selected fast model is unchanged.

Result: PASS. qwen3.5:9b became the default model, no prefix rows were created, the fast model stayed correct, and all other models retained their relative order and names.

A — Settings JSON 连续输入模型 ID

步骤:

  1. 打开 Settings → Providers,编辑包含原有有序模型列表、显示名称和快速模型的 Ollama Provider。
  2. 点击并聚焦 Settings JSON 文本框。聚焦动作会保存本次编辑使用的模型列表快照。
  3. 按正常逐字符输入方式,将 ANTHROPIC_MODEL 改成 qwen3.5:9b。
  4. 保存、关闭并重新打开 Provider。

预期:

  • 列表中只出现一个完整的 qwen3.5:9b。
  • 不出现 q、qw、qwen、qwen3.5: 等输入过程中的前缀模型行。
  • qwen3.5:9b 成为第一项/默认模型,因为 ANTHROPIC_MODEL 表示主模型。
  • 原有模型行全部保留。
  • 原有显示名称不丢失。
  • 其他模型的相对顺序不变。
  • 快速模型选择不变。

结果:PASS。qwen3.5:9b 正确成为默认模型,没有生成前缀行;快速模型、其他模型的相对顺序和名称均保持正确。


B — Automatic recommendation and styled model picker

Steps:

  1. Open the Ollama provider and confirm Base URL http://127.0.0.1:11434.
  2. Leave Model ID empty and click Fetch models.
  3. After discovery completes, inspect the Model ID field and open its dropdown.
  4. Change a connection field such as Base URL before fetching again.
  5. Restore the Ollama Base URL and fetch again.

Expectations:

  • Discovery reports three models.
  • With an empty Model ID, one fetched model is automatically placed in the Model ID field as the recommendation; the user does not need to type it manually.
  • The dropdown uses the same shared visual structure as the API Format dropdown, shows model names/ids, and marks the recommendation.
  • Free-text Model ID entry still works.
  • Changing a connection field immediately removes the old count and old dropdown candidates.
  • The native browser datalist/history is not used, so candidates from 11434 cannot remain as stale Model ID suggestions for another endpoint.
  • A fresh fetch after restoring 11434 shows only the current Ollama models.

Result: PASS. Automatic recommendation, dropdown appearance, editable Model ID, and candidate clearing all behaved as expected.

B — 自动推荐与统一样式的模型下拉菜单

步骤:

  1. 打开 Ollama Provider,确认 Base URL 为 http://127.0.0.1:11434。
  2. 保持 Model ID 为空,点击“获取模型”。
  3. 获取完成后检查 Model ID,并展开下拉菜单。
  4. 修改 Base URL 等任一连接字段,暂时不要再次获取。
  5. 恢复 Ollama 地址后重新获取。

预期:

  • 显示找到三个模型。
  • Model ID 为空时,系统自动将一个获取到的模型填入 Model ID 作为推荐,不需要人工输入。
  • 下拉菜单复用“API 格式”下拉菜单的视觉结构,显示模型名称/ID,并标记推荐项。
  • Model ID 仍可自由手工输入。
  • 修改连接字段后,旧数量提示和旧候选立即清除。
  • 不再使用浏览器原生 datalist/history,因此来自 11434 的旧候选不会残留到另一个 endpoint。
  • 恢复 11434 并重新获取后,只显示当前 Ollama 模型。

结果:PASS。自动推荐、下拉菜单样式、自由输入和旧候选清除均符合预期。


C — Ignore a delayed response from a superseded endpoint

Start a delayed test server from Windows PowerShell:

$bun = 'E:\Repo\PhD\GoDiao\windows\dreamcoder-main\.pr-worktree\bun-runner\package\bin\bun.exe'

& $bun -e "Bun.serve({ hostname: '127.0.0.1', port: 14567, async fetch() { await Bun.sleep(5000); return Response.json({ data: [{ id: 'stale-delayed-model' }], has_more: false }); } }); console.log('Delayed model server: http://127.0.0.1:14567')"

Server-start expectation: the console prints Delayed model server: http://127.0.0.1:14567, stays running, and does not return to the PowerShell prompt.

Steps and expectations:

  1. Open the Ollama provider without saving temporary test values. Clear Model ID to make an incorrect auto-selection obvious.
  2. Set Base URL to http://127.0.0.1:14567.
    • Expected immediately: previous discovery count, candidates, and errors are cleared; Fetch models is available.
  3. Click Fetch models.
    • Expected during the first five seconds: loading is visible; no result count, stale-delayed-model, or auto-selection is present.
  4. Within five seconds, change Base URL back to http://127.0.0.1:11434.
    • Expected immediately: loading stops; Fetch models becomes available; no result count/error remains; Model ID stays empty; no delayed candidate is visible.
  5. Wait at least 6–8 seconds so the old server has returned.
    • Expected after the old response: absolutely no UI state is restored by it. There is no 1 model found, no stale-delayed-model in the field or dropdown, no old error, and loading does not restart. Base URL remains 11434.
  6. Click Fetch models against Ollama.
    • Expected: discovery works normally, reports three models, and shows only qwen3.8:latest, qwen3.5:9b, and qwen3.5:4b; stale-delayed-model is absent.
  7. Cancel the provider form instead of saving test changes, then stop the delayed server with Ctrl+C.

Result: PASS. Editing Base URL immediately invalidated the in-flight request, its delayed success response was discarded, and the following Ollama discovery remained functional.

C — 丢弃已被连接变化取代的延迟响应

在 Windows PowerShell 中使用上面的命令启动延迟服务器。成功启动时应打印 Delayed model server: http://127.0.0.1:14567,进程保持运行且不会返回 PowerShell 提示符。

步骤与预期:

  1. 打开 Ollama Provider,不保存测试期间的临时配置;清空 Model ID,以便明显观察错误的自动推荐。
  2. 将 Base URL 改为 http://127.0.0.1:14567。
    • 立即预期:之前的模型数量、候选和错误全部清除;“获取模型”按钮可用。
  3. 点击“获取模型”。
    • 前五秒预期:界面显示加载;不显示结果数量、stale-delayed-model 或自动选择。
  4. 在五秒内将 Base URL 改回 http://127.0.0.1:11434。
    • 立即预期:加载立即停止;按钮恢复可用;没有数量或错误提示;Model ID 保持为空;没有延迟候选。
  5. 等待至少 6–8 秒,让旧服务器完成响应。
    • 旧响应完成后的预期:界面完全不被旧响应回写;不显示“已找到 1 个模型”,Model ID 和下拉菜单都没有 stale-delayed-model,不显示旧错误,加载状态不会重新出现,Base URL 仍为 11434。
  6. 对 Ollama 再次点击“获取模型”。
    • 预期:正常找到三个模型,仅包含 qwen3.8:latest、qwen3.5:9b 和 qwen3.5:4b,不存在 stale-delayed-model。
  7. 取消 Provider 表单,不保存临时测试值;在延迟服务器窗口按 Ctrl+C 退出。

结果:PASS。修改 Base URL 后,在途请求立即失效;延迟成功响应被丢弃;随后对 Ollama 的模型发现仍然正常。

Remaining live-provider scope / 剩余实机范围

  • Local Ollama A/B/C validation: complete and passed.
  • Real OpenAI/OpenAI-compatible API validation: still pending.
  • Real Anthropic API validation, including live pagination: still pending; pagination has automated two-page coverage.

@GoDiao GoDiao left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

English

Thanks for the follow-up and for the detailed manual test record! The discovery invalidation works as described, and the server side looks good end to end. We went through the whole PR again. Four things left, all in ProviderFormModal.tsx:

1. Make the model keys in the settings JSON read-only (a change of plan on our side, sorry).
The snapshot fixed the prefix rows, but one more bug remains. Changing A to AX and back to A leaves [AX, A, B] in the list, because nothing runs once the ids match the snapshot. This is the third bug we've hit in the JSON → list sync. Each one came from our own suggestions, so we'd rather remove the sync than patch it again. The keys don't do anything at runtime anyway:

  • The CLI env is built from the list (buildProviderManagedEnv).
  • Saving the active provider rewrites those keys in settings.json from the list (syncToSettings).

So the model list is the only source. In the JSON textarea, treat these keys as generated:

  • ANTHROPIC_MODEL
  • ANTHROPIC_DEFAULT_HAIKU_MODEL
  • ANTHROPIC_SMALL_FAST_MODEL
  • ANTHROPIC_DEFAULT_SONNET_MODEL
  • ANTHROPIC_DEFAULT_OPUS_MODEL
  • CLAUDE_CODE_MODEL_CONTEXT_WINDOWS

Concretely:

  • No reverse sync. onChange no longer reads these keys back into models, fastModel or the context inputs. Remove the whole merge block, modelsSnapshotRef, and the onFocus / onBlur handlers.
  • Write them back from the list. After parsing, run the same updateSettingsJsonModels / updateSettingsJsonModelContextWindows writes before setSettingsJson. The JSON then always shows what the list says, and an edit to one of these keys snaps back.
  • Add a hint. Put a short line under the textarea, in en and zh, along the lines of "Model variables are generated from the model list above".
  • Tests. Replace the progressive-typing, unrelated-key and merge tests with one test: edit ANTHROPIC_MODEL (and the context-windows key) in the JSON, save, and assert that models, fastModel and modelContextWindows are unchanged.

Base URL, API key, auth strategy and the other keys stay editable in the JSON as they are now.

2. Connection changes made in the JSON don't reset discovery.
The five form handlers call resetDiscovery(), but the JSON textarea also sets the base URL, API key and auth strategy, and it doesn't. Editing ANTHROPIC_BASE_URL in the JSON keeps the old endpoint's models and count. An in-flight fetch can still land, because the request id isn't bumped. onChange runs on every keystroke, so only reset when a value actually changes:

const connectionChanged =
  (env.ANTHROPIC_BASE_URL && env.ANTHROPIC_BASE_URL !== baseUrl) ||
  (validNextApiKey && validNextApiKey !== apiKey) ||
  (nextAuthStrategy && nextAuthStrategy !== authStrategy)
if (connectionChanged) resetDiscovery()

Here validNextApiKey is the key after the placeholder checks.

3. The model picker needs to be a filterable combobox.
A styled picker is the right direction. The shared Dropdown is a click-to-toggle menu, though, and it doesn't work as a text input's suggestion list:

  • It doesn't filter as you type. Endpoints like OpenRouter return hundreds of models in a 240px list.
  • A keyboard user can't open it. It only listens for clicks on its wrapper, so Enter and Space in an input do nothing. There's also no aria-expanded.
  • Clicking into the input to place the caret toggles the list.
  • The input moves between the bare branch and the Dropdown branch when results arrive or reset. It remounts and loses focus while you type.

Please add a small combobox component for this. The branch picker in components/shared/RepositoryLaunchControls.tsx (L250-320, L584) is a good reference in our codebase:

  • The Model ID input itself is the filter. It always renders in the same place in the tree, so it never remounts.
  • With discovered models present, the list opens on focus, on typing and on ArrowDown. It closes on Escape, on an outside click and on selection.
  • It shows the models whose id or name contains the input text, case-insensitive.
  • ArrowUp and ArrowDown move the highlight. Enter picks the highlighted model, and otherwise leaves the typed text alone. Free-text ids keep working.
  • On the input: role="combobox", aria-expanded, aria-controls and aria-activedescendant. On the list: role="listbox", with role="option" items.
  • The styling matches Dropdown and the branch menu.

Tests to add: filtering, picking with the keyboard, free text still being saved as typed, and focus staying in the input when a fetch completes.

4. Please drop the auto-recommend.
The auto-fill takes the first fetched model that isn't in the list yet. That's just the upstream order. A provider like OpenRouter returns hundreds of models, and the first one isn't a sensible pick. Since the combobox opens on focus, the user can choose the model themselves. Please remove the auto-fill, recommendedDiscoveredModel and the auto_awesome marker.

These are suggestions, not the only way to do it. If you have a better approach for any of them, we're happy to discuss it here.

Once these are in, we'll run the real-API checks on our side. Thanks again for sticking with this one!


中文

感谢跟进,也感谢这么详细的人工测试记录!模型获取的请求作废逻辑和描述的一样有效,服务端整体也没问题。我们把整个 PR 又看了一遍,还剩四件事,都在 ProviderFormModal.tsx:

1. 设置 JSON 里的模型相关 key 改成只读(这是我们这边改了方案,抱歉)。

快照解决了前缀行的问题,但还剩一个 bug:把 A 改成 AX 再改回 A,列表里还是 [AX, A, B],因为 id 和快照一致后就不再执行任何逻辑。这已经是"JSON 反写列表"出的第三个 bug,而且每个都来自我们自己的建议,所以我们宁可去掉这套同步,也不想再打补丁。而且这些 key 在运行时本来就不起作用:

  • CLI 的环境变量是从模型列表生成的(buildProviderManagedEnv);
  • 保存当前启用的服务商时,settings.json 里的这些 key 会按列表重写(syncToSettings)。

所以模型列表是唯一的来源。在 JSON 文本框里,下面这些 key 当作"由列表生成"处理:

  • ANTHROPIC_MODEL
  • ANTHROPIC_DEFAULT_HAIKU_MODEL
  • ANTHROPIC_SMALL_FAST_MODEL
  • ANTHROPIC_DEFAULT_SONNET_MODEL
  • ANTHROPIC_DEFAULT_OPUS_MODEL
  • CLAUDE_CODE_MODEL_CONTEXT_WINDOWS

具体来说:

  • 不再反向同步:onChange 不再把这些 key 读回 models、fastModel 或上下文输入。整段合并逻辑、modelsSnapshotRef、onFocus / onBlur 都删掉。
  • 从列表写回去:解析之后、setSettingsJson 之前,用现有的 updateSettingsJsonModels / updateSettingsJsonModelContextWindows 再写一遍。这样 JSON 显示的始终和列表一致,改这些 key 会弹回原值。
  • 加一行提示:在文本框下面加一句简短说明(中英文),意思是"模型相关变量由上方的模型列表生成"。
  • 测试:把逐字输入、改无关 key、合并这三个测试换成一个:在 JSON 里改 ANTHROPIC_MODEL(以及上下文窗口那个 key),保存,断言 models、fastModel、modelContextWindows 都没变。

base URL、API key、认证方式和其他 key 在 JSON 里照旧可以改。

2. 在 JSON 里改连接信息不会清空获取结果。

表单上的五个处理函数都会调用 resetDiscovery(),但 JSON 文本框也会设置 base URL、API key 和认证方式,却没有调用。在 JSON 里改 ANTHROPIC_BASE_URL 后,旧地址的模型和数量还留着;正在进行的请求也还可能写回来,因为请求编号没有递增。onChange 每敲一个键都会执行,所以只在值真的变了时才清空(代码见英文部分),其中 validNextApiKey 指排除占位符之后的 key。

3. 模型选择器要做成能筛选的 combobox。

用我们自己风格的选择器是对的方向。但共享的 Dropdown 是点击开关的菜单,不适合做输入框的候选列表:

  • 不能边打字边筛选。OpenRouter 这类地址会返回几百个模型,全挤在 240px 高的列表里。
  • 只用键盘打不开。它只监听外层的点击,在输入框里按回车或空格没有反应,也没有 aria-expanded。
  • 点进输入框放光标时,列表会跟着开关。
  • 拿到结果或清空时,输入框会在两种结构之间切换,被重建后打字时会丢焦点。

请为此写一个小的 combobox 组件。仓库里的分支选择器 components/shared/RepositoryLaunchControls.tsx(L250-320、L584)可以参考:

  • Model ID 输入框本身就是筛选框,始终渲染在同一位置,不会被重建。
  • 有获取结果时,获得焦点、打字或按 ↓ 打开列表;按 Esc、点外面或选中后关闭。
  • 显示 id 或名字包含输入内容的模型,不区分大小写。
  • ↑↓ 移动高亮,回车选中高亮项;没有高亮项时不改动用户输入的文字。手动输入任意 id 仍然有效。
  • 输入框加 role="combobox"、aria-expanded、aria-controls、aria-activedescendant;列表加 role="listbox",每项加 role="option"。
  • 样式和 Dropdown、分支菜单一致。

要加的测试:筛选、键盘选择、手动输入的内容按原样保存、获取完成时焦点仍在输入框里。

4. 请去掉自动推荐。

自动填入的是获取结果里第一个还不在列表中的模型,这只是上游返回的顺序。OpenRouter 这类服务商有几百个模型,取第一个并不合适。新的 combobox 获得焦点就会展开,用户可以自己挑。请删掉自动填入、recommendedDiscoveredModel 和 auto_awesome 标记。

以上只是我们的建议,不是唯一做法。如果你对其中任何一条有更好的思路,欢迎在这里一起讨论。

这些改完后,我们这边会跑真实 API 的手测。再次感谢你一直跟进这个 PR!

@AIR-Xie

AIR-Xie commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Final owner-review follow-up / 最终 Owner Review 跟进

Implemented and pushed in 6ae2b96 (fix(providers): address final model picker review). All four requirements from the latest review were checked point by point in the Tauri desktop client (not the browser build) and passed manual validation.

English

1. Settings JSON model keys are generated/read-only — PASS

Implementation:

  • Removed JSON → models / fastModel / context reverse synchronization, including the focus snapshot and merge logic.
  • After every successful JSON parse, these generated keys are immediately rewritten from the ordered model list, selected fast model, and model context inputs:
    • ANTHROPIC_MODEL
    • ANTHROPIC_DEFAULT_HAIKU_MODEL
    • ANTHROPIC_SMALL_FAST_MODEL
    • ANTHROPIC_DEFAULT_SONNET_MODEL
    • ANTHROPIC_DEFAULT_OPUS_MODEL
    • CLAUDE_CODE_MODEL_CONTEXT_WINDOWS
  • Added the requested English and Chinese hint below the textarea.
  • Replaced the former progressive/unrelated/merge tests with one source-of-truth test covering main model, fast model, context windows, save payload, and an unrelated JSON key.

Manual expectations and result:

  • Direct edits to generated model values snap back immediately.
  • Reordering the list, changing the fast-model selector, or changing a context-window input updates the generated JSON.
  • Model rows, names, order, fast model, and context values are not changed by JSON edits.
  • Unrelated JSON such as skipWebFetchPreflight remains editable and persists.
  • Result: PASS.

Clarification found during validation: top-level model / modelContext values are legacy standalone-mode fallback settings and are intentionally outside this generated-key set. They are ignored while a provider is active; the active provider still ran the selected Flint model.

2. JSON connection changes reset discovery and invalidate in-flight work — PASS

Implementation:

  • Valid parsed JSON compares Base URL, usable API key, and inferred auth strategy with current form state.
  • resetDiscovery() runs only when one of those effective connection values actually changes.
  • Reset clears the old count, candidates, errors, loading state, and invalidates the current request id.
  • A response from a superseded endpoint cannot write success, error, or completion state back into the form.

Manual expectations and result:

  • Editing an unrelated JSON key kept the current discovery count and candidates.
  • Editing ANTHROPIC_BASE_URL cleared the old result immediately.
  • A delayed response from 127.0.0.1:14567 was ignored after restoring the Ollama URL.
  • A following Ollama fetch still returned only the current Ollama models.
  • Result: PASS.

3. Model ID is a stable, filterable, accessible combobox — PASS

Implementation:

  • The text input is always mounted in the same tree position, so discovery completion/reset does not remount it or steal focus.
  • Filters discovered models by id or display name, case-insensitively.
  • Opens on focus, typing, and ArrowDown; closes on Escape, outside pointer-down, and selection.
  • ArrowUp/ArrowDown move the active option; Enter selects only a highlighted option and otherwise preserves free text.
  • Added combobox/listbox/option roles plus aria-expanded, aria-controls, and aria-activedescendant.
  • Uses the same visual tokens, elevation, spacing, hover state, and motion treatment as the shared dropdown/branch menus.
  • Added a localized no-match state.

Manual expectations and result:

  • Filtering, case-insensitive matching, mouse selection, keyboard selection, Escape/outside close, and focus stability all passed.
  • Arbitrary model ids remained editable and were added exactly as typed.
  • Result: PASS.

4. Auto-recommendation was removed — PASS

Implementation:

  • Removed first-result auto-fill.
  • Removed recommendedDiscoveredModel, newModelIdRef, and the auto_awesome recommendation marker.
  • Discovery now only supplies candidates; the user explicitly selects or types a model id.

Manual expectations and result:

  • Fetching with an empty Model ID left it empty.
  • No upstream-first model was presented as a recommendation.
  • Free-text entry continued to work.
  • Result: PASS.

Verification

Automated:

  • ProviderFormModal.test.tsx: 14/14 passed.
  • Desktop TypeScript: tsc --noEmit passed.
  • Vite production build passed; 4,419 modules transformed.
  • Full Desktop suite passed when excluding the existing unrelated mcpCatalogInstall.test.ts raw-resource collection blocker.
  • git diff --check passed.

Manual/live:

  • Client: current-source Tauri desktop client, with the sidecar built from this worktree.
  • Local Ollama: qwen3.8:latest, qwen3.5:9b, qwen3.5:4b — all four UI requirements passed.
  • OpenRouter API-key session using anthropic/recursive/flint-v1.0:free — active-provider model use confirmed.
  • Direct OpenAI API and direct Anthropic API/live pagination remain pending owner-side verification.

中文

1. Settings JSON 模型字段改为由列表生成、只读 — 通过

实现:

  • 删除 JSON 向 models、fastModel 和上下文输入反向同步的逻辑,包括快照与合并代码。
  • 每次 JSON 成功解析后,都从有序模型列表、快速模型和上下文输入重新写回 owner 指定的六类模型字段。
  • 在文本框下方增加中英文提示。
  • 用一个“列表为唯一来源”的测试替换原来的连续输入、无关字段和合并测试。

人工结果:

  • 直接修改生成字段会立即恢复。
  • 模型顺序、快速模型和上下文只能通过上方控件修改,并会同步到 JSON。
  • JSON 编辑不会改变模型行、名称、顺序、快速模型或上下文。
  • skipWebFetchPreflight 等无关字段仍可正常修改和保存。
  • 结果:通过。

验收期间确认:顶层 model / modelContext 是 standalone 模式的历史回退配置,不属于本次六个生成字段;Provider 激活时不会读取它们。实际启用的 Provider 仍正确使用 Flint 模型。

2. 从 JSON 修改连接信息会清空 discovery 并作废在途请求 — 通过

实现:

  • JSON 成功解析后,对有效的 Base URL、API key 和认证方式做实际值比较。
  • 只有连接值真正变化时才执行 resetDiscovery()。
  • 清空旧数量、候选、错误和加载状态,并递增请求编号,使旧 endpoint 的成功、失败和 finally 状态都无法回写。

人工结果:

  • 只改无关字段不会清空现有结果。
  • 修改 ANTHROPIC_BASE_URL 会立即清空旧结果。
  • 切回 Ollama 后,127.0.0.1:14567 的延迟响应没有重新写入界面。
  • 随后的 Ollama 获取仍只返回当前 Ollama 模型。
  • 结果:通过。

3. Model ID 改为稳定、可筛选、可访问的 combobox — 通过

实现:

  • 输入框始终位于同一 DOM 位置,获取完成或 reset 时不会重建和丢失焦点。
  • 按 id 或显示名称进行不区分大小写的筛选。
  • 聚焦、输入和 ↓ 打开;Esc、点击外部和选中后关闭。
  • ↑↓ 移动高亮;Enter 只选择高亮项,没有高亮时保留自由输入。
  • 补齐 combobox/listbox/option 角色和相关 ARIA 属性。
  • 使用与 Dropdown/分支菜单一致的视觉 token、阴影、间距、hover 和动效,并提供中英文无匹配提示。

人工结果:

  • 筛选、大小写不敏感匹配、鼠标选择、键盘选择、关闭行为和异步获取后的焦点保持均通过。
  • 任意模型 ID 可以按输入原样添加。
  • 结果:通过。

4. 删除自动推荐和自动填充 — 通过

实现:

  • 删除获取结果第一项自动填入。
  • 删除 recommendedDiscoveredModel、newModelIdRef 和 auto_awesome 标记。
  • 获取结果现在只作为候选,必须由用户明确选择或输入。

人工结果:

  • Model ID 为空时执行获取,完成后仍为空。
  • 不再把上游第一项标记为推荐。
  • 自由输入保持可用。
  • 结果:通过。

验证汇总

自动验证:

  • ProviderFormModal.test.tsx:14/14 通过。
  • Desktop TypeScript:通过。
  • Vite production build:通过,共转换 4,419 个模块。
  • 排除仓库已有且无关的 mcpCatalogInstall.test.ts raw-resource 收集阻塞后,Desktop 完整测试通过。
  • git diff --check:通过。

客户端/实机:

  • 使用本 worktree 源码及其 sidecar 启动的 Tauri 桌面客户端,不是网页端。
  • 本地 Ollama:qwen3.8:latest、qwen3.5:9b、qwen3.5:4b,四项人工检查全部通过。
  • OpenRouter API key + anthropic/recursive/flint-v1.0:free:已确认激活 Provider 的模型实际生效。
  • OpenAI 官方 API 与 Anthropic 官方 API/实机分页仍待 owner 验证。

@GoDiao GoDiao left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感谢更新,也感谢这么详细的测试记录!上一轮的四点都已经落实:

  • JSON 里的模型变量改了会弹回原值;
  • 在 JSON 里改连接信息会清空获取结果;
  • combobox 的表现和描述一致;
  • 自动填入也去掉了。

这次我们还用一个独立的配置目录把 PR 跑成了开发版应用,手动走了一遍服务商表单。其中两个问题在应用里复现了,审查中又发现了几处。抱歉清单还在继续。快速模型是这个 PR 新加的,所以这一块最需要仔细。

1. 编辑服务商时,预设的快速模型会被填回来(已复现)。
ProviderFormModal.tsx:484 在已保存的服务商没有快速模型时,会退回去用预设的 defaultFastModel:

const [fastModel, setFastModel] = useState(provider?.fastModel ?? initialPreset.defaultFastModel ?? '')

编辑模式下,initialPreset 就是这个服务商的真实预设。我们在应用里复现了两种情况:

  • 用户的选择被覆盖。
    1. 新建 DeepSeek,模型是 pro 和 flash,快速模型选"使用当前所选模型",保存。存储里正确地没有 fastModel。
    2. 重新打开编辑,表单显示 deepseek-v4-flash,JSON 里的 HAIKU 和 SMALL_FAST 也是 flash。
    3. 什么都不改直接保存,就写成了 fastModel: "deepseek-v4-flash"。
  • 迁移来的服务商完全存不了。
    1. 一份四个槽位都是 deepseek-v4-pro 的旧 DeepSeek 配置,迁移后是 models: [pro],没有快速模型,这一步是对的。
    2. 但编辑表单里的状态仍然是 deepseek-v4-flash。原生下拉框因为 flash 不在选项里,显示的是"使用当前所选模型",所以看上去一切正常。
    3. 保存时,哪怕只改了名字,也会被 assertFastModel 以 400 拒绝:"Fast model must be one of the provider models"。

表单不会显示保存错误。那个 console.error 在 main 上就有,我们会另外修。所以用户只会看到一个点了没反应的保存按钮。

修法是只在新建时使用预设的默认值:

const [fastModel, setFastModel] = useState(
  provider ? (provider.fastModel ?? '') : (initialPreset.defaultFastModel ?? ''),
)

另外请补一个测试:打开一个没有 fastModel 的 DeepSeek 服务商,保存,断言提交的是 fastModel: null。

2. 删掉一个上下文窗口不合法的模型后,保存按钮永远禁用(已复现)。
添加模型 test-b,上下文窗口填 1,这时会出现错误提示,保存按钮变灰。再删掉 test-b,它的输入框和错误提示都消失了,但保存按钮一直是灰的,尽管剩下的模型都是合法的。

原因是 updateModels 没有把被删模型从 modelContextInputs 里去掉,而 L587 会校验所有条目。只校验当前列表里的模型就够了,buildModelContextWindows 本来也只遍历 models:

models.some((model) => getModelContextWindowErrorKey(modelContextInputs[model.id] ?? ''))

现有校验的写法可以保持不变,关键是遍历 models,而不是 Object.values(modelContextInputs)。

3. src/server/__tests__/e2e/full-flow.test.ts:256 有一处无关的改动。
提交 22ac39f 把测试的结束条件从 session_patch 改回了 status。看起来是解决合并冲突时留下的,等于撤销了 #150 对这里的修改。服务端已经不发 status,所以这个条件永远不会成立,测试只能靠 3 秒超时结束。请恢复成 main 上的写法:

if (msg.type === 'session_patch' && msg.changes?.turn?.phase === 'idle' && messages.length > 2) {

4. 模型列表交互的测试。
ProviderFormModal.test.tsx 覆盖了添加模型、模型获取、手动输入和 JSON 只读,但没有覆盖排序、删除和切换快速模型。请补三个测试,每个操作完都点保存:

  • 上移一个模型,断言提交的顺序,以及 JSON 里的 ANTHROPIC_MODEL 变成了新的第一个;
  • 删掉当前的快速模型,断言提交的是 fastModel: null;
  • 把快速模型切到"使用当前所选模型",断言提交的是 fastModel: null。

5. POST /api/providers/models 的接口测试。
目前只直接调用 discoverModels,测了服务层成功的那条路径。请在 providers.test.ts 里通过 handleProvidersApi 补三种情况:

  • 成功,返回 200;
  • 请求体不合法,返回 400;
  • 上游返回 401,接口返回 502,错误码是 MODEL_DISCOVERY_FAILED。

6. 标题生成模型优先级的测试。
titleService.ts:60 现在用的是 fastModel || selectedModel || models[0]?.id,但 title-service.test.ts 里没有一个测试传过 selectedModel。请准备一个列表第一个模型和 selectedModel 不同的服务商,断言请求用的模型:

  • 没有 fastModel 时,应该是 selectedModel;
  • 有 fastModel 时,应该是快速模型。

另外一个小问题:在 combobox 的选项上按 Esc,会关掉整个弹窗。
列表展开时按 Tab,焦点会跳到选项按钮上。Esc 只在输入框上处理,所以在选项上按 Esc 会传到弹窗自己的 Esc 处理,把整个表单关掉,没保存的输入也丢了。给选项加上 tabIndex={-1},再在列表上处理 Esc,应该就够了。

和之前一样,这些都只是建议。如果你对其中任何一条有更好的思路,欢迎在这里一起讨论。这些改完之后,我们会跑真实 API 的手测。再次感谢你一直跟进这个 PR!

@AIR-Xie

AIR-Xie commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up review requirements completed

Implemented in commit 1956bdf.

  1. Do not restore a preset fast model while editing

    • Existing providers without fastModel now remain on "Use selected model".
    • Preset defaultFastModel is applied only when creating a provider.
    • Added a save regression test that expects fastModel: null.
  2. Ignore deleted model context errors

    • Context-window validation now checks only models that still exist in the current model list.
    • Deleting a model with an invalid context value immediately restores the ability to save.
  3. Restore the unrelated E2E condition

    • Restored the full-flow completion check to session_patch -> changes.turn.phase === idle.
  4. Add the three requested save interaction tests

    • Moving a model up verifies submitted order and generated ANTHROPIC_MODEL.
    • Deleting the current fast-model row verifies fastModel: null.
    • Selecting "Use selected model" verifies fastModel: null.
  5. Add API-level model-discovery tests

    • POST /api/providers/models: success returns 200.
    • Invalid body returns 400.
    • Upstream 401 returns 502 with MODEL_DISCOVERY_FAILED.
  6. Add title-model priority coverage

    • Without a fast model, title generation uses the session selected model rather than the first list entry.
    • With a fast model, title generation uses the fast model.
  7. Fix model suggestion keyboard behavior

    • Suggestion options are removed from the Tab order with tabIndex={-1}.
    • Escape closes only the suggestion list and does not close the provider form.

Verification

Automated:

  • Provider form tests: 20 passed.
  • Provider API and title-service tests: 90 passed.
  • Full-flow E2E: 32 passed.
  • Desktop regression suite: passed with the known unrelated raw-JSON collection case excluded.
  • Desktop TypeScript check and production build: passed.
  • Windows desktop sidecar rebuilt successfully.

Manual desktop validation:

  • A: Editing a provider without a fast model keeps "Use selected model".
  • B: Deleting a model with an invalid context value re-enables Save.
  • C: Reorder, fast-model deletion, and "Use selected model" persistence all passed.
  • D: Tab and Escape behavior in the model suggestion list passed.

All A-D checks were confirmed by the reporter on the desktop client before this commit was pushed.

@GoDiao GoDiao left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

感谢这一轮的修改,也感谢你一直耐心跟进这个 PR!上一轮的七条都已经落实,我们逐条对照了代码,新加的测试也确实覆盖了对应的问题。

这次我们还用一份真实的旧版 providers.json 跑了开发版应用,结果都正常:

  • 迁移后备份和原文件一致,模型从四个固定槽位变成了列表;
  • 迁移来的服务商可以正常编辑、保存;
  • 用 OpenRouter 获取模型、选择主模型、发消息都正常。

剩下的两处小问题由我们自己修,不需要你再改:

  • 获取模型时上游返回 4xx,现在接口统一返回 502。这是我们上一轮写错了:原来的 400 才是对的,你是按我们的说法改的。我们会把这一行和对应的测试一起改回来。
  • 服务商表单保存失败时只打了 console.error,用户看不到。这个在 main 上本来就有,之前说过会另外修。

另外,这个 PR 合并后,旧版本读不了迁移后的配置。我们会在发布说明里写明:降级前需要先用 providers.json.bak-before-migration-* 恢复。

批准,我们这边会合并。再次感谢!

@GoDiao
GoDiao merged commit 8509c85 into main Oct 24, 2026
4 checks passed
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.

Let providers have any number of models, with an optional fast model

2 participants