Repository navigation
feat(providers): support arbitrary model lists and optional fast models - #159
Conversation
GoDiao
left a comment
There was a problem hiding this comment.
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/currentreturns the savedmodelfirst, without checking that it belongs to the active provider.PUTwrites it again, and splits it at:, soqwen3.6:27bbecomesqwen3.6+ context27b.resolveProviderDefaultModelgained anexplicitModelbranch.
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
explicitModelbranch. - Restore the stale-model test.
- Update the
models.mainwording 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 emptyhaiku". The fixture used to havehaiku: '', 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.bakfile is written; this runs on every launch.{ main: 'm', haiku: '' }: the list is[m], with nofastModel.{ main: 'm', haiku: 'm', sonnet: 's', opus: 'm' }: the list is[m, s], with nofastModel.- All four slots empty: the list is
[].
-
providers.test.ts,updateProvider: there's nothing forfastModelyet, though every desktop save sends it. Please cover two cases: removing the fast model's row clears it, andfastModel: nullclears it. While there, a small asymmetry: an explicitfastModelthat isn't in the newmodelsis 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:buildProviderChoicesno longer uses_availableModels. Drop the parameter, the argument and theuseMemodep.en.ts/zh.ts: these 10 keys are now unused, so please remove them from both files:mainModel,haikuModel,sonnetModel,opusModel- the four
*ContextWindowkeys 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
left a comment
There was a problem hiding this comment.
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!
Final review follow-up and manual reproduction record / 最终 Review 修复与人工复现记录Implemented in Requirement completion / Requirement 完成情况
Automated checks for this commit:
Manual environment:
A — Settings JSON progressive model editingSteps:
Expectations:
Result: PASS. A — Settings JSON 连续输入模型 ID步骤:
预期:
结果:PASS。 B — Automatic recommendation and styled model pickerSteps:
Expectations:
Result: PASS. Automatic recommendation, dropdown appearance, editable Model ID, and candidate clearing all behaved as expected. B — 自动推荐与统一样式的模型下拉菜单步骤:
预期:
结果:PASS。自动推荐、下拉菜单样式、自由输入和旧候选清除均符合预期。 C — Ignore a delayed response from a superseded endpointStart 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 Steps and expectations:
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 中使用上面的命令启动延迟服务器。成功启动时应打印 步骤与预期:
结果:PASS。修改 Base URL 后,在途请求立即失效;延迟成功响应被丢弃;随后对 Ollama 的模型发现仍然正常。 Remaining live-provider scope / 剩余实机范围
|
GoDiao
left a comment
There was a problem hiding this comment.
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_MODELANTHROPIC_DEFAULT_HAIKU_MODELANTHROPIC_SMALL_FAST_MODELANTHROPIC_DEFAULT_SONNET_MODELANTHROPIC_DEFAULT_OPUS_MODELCLAUDE_CODE_MODEL_CONTEXT_WINDOWS
Concretely:
- No reverse sync.
onChangeno longer reads these keys back intomodels,fastModelor the context inputs. Remove the whole merge block,modelsSnapshotRef, and theonFocus/onBlurhandlers. - Write them back from the list. After parsing, run the same
updateSettingsJsonModels/updateSettingsJsonModelContextWindowswrites beforesetSettingsJson. 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 thatmodels,fastModelandmodelContextWindowsare 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
Dropdownbranch 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-controlsandaria-activedescendant. On the list:role="listbox", withrole="option"items. - The styling matches
Dropdownand 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_MODELANTHROPIC_DEFAULT_HAIKU_MODELANTHROPIC_SMALL_FAST_MODELANTHROPIC_DEFAULT_SONNET_MODELANTHROPIC_DEFAULT_OPUS_MODELCLAUDE_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!
Final owner-review follow-up / 最终 Owner Review 跟进Implemented and pushed in English1. Settings JSON model keys are generated/read-only — PASSImplementation:
Manual expectations and result:
Clarification found during validation: top-level 2. JSON connection changes reset discovery and invalidate in-flight work — PASSImplementation:
Manual expectations and result:
3. Model ID is a stable, filterable, accessible combobox — PASSImplementation:
Manual expectations and result:
4. Auto-recommendation was removed — PASSImplementation:
Manual expectations and result:
VerificationAutomated:
Manual/live:
中文1. Settings JSON 模型字段改为由列表生成、只读 — 通过实现:
人工结果:
验收期间确认:顶层 2. 从 JSON 修改连接信息会清空 discovery 并作废在途请求 — 通过实现:
人工结果:
3. Model ID 改为稳定、可筛选、可访问的 combobox — 通过实现:
人工结果:
4. 删除自动推荐和自动填充 — 通过实现:
人工结果:
验证汇总自动验证:
客户端/实机:
|
GoDiao
left a comment
There was a problem hiding this comment.
感谢更新,也感谢这么详细的测试记录!上一轮的四点都已经落实:
- JSON 里的模型变量改了会弹回原值;
- 在 JSON 里改连接信息会清空获取结果;
- combobox 的表现和描述一致;
- 自动填入也去掉了。
这次我们还用一个独立的配置目录把 PR 跑成了开发版应用,手动走了一遍服务商表单。其中两个问题在应用里复现了,审查中又发现了几处。抱歉清单还在继续。快速模型是这个 PR 新加的,所以这一块最需要仔细。
1. 编辑服务商时,预设的快速模型会被填回来(已复现)。
ProviderFormModal.tsx:484 在已保存的服务商没有快速模型时,会退回去用预设的 defaultFastModel:
const [fastModel, setFastModel] = useState(provider?.fastModel ?? initialPreset.defaultFastModel ?? '')编辑模式下,initialPreset 就是这个服务商的真实预设。我们在应用里复现了两种情况:
- 用户的选择被覆盖。
- 新建 DeepSeek,模型是 pro 和 flash,快速模型选"使用当前所选模型",保存。存储里正确地没有
fastModel。 - 重新打开编辑,表单显示
deepseek-v4-flash,JSON 里的 HAIKU 和 SMALL_FAST 也是 flash。 - 什么都不改直接保存,就写成了
fastModel: "deepseek-v4-flash"。
- 新建 DeepSeek,模型是 pro 和 flash,快速模型选"使用当前所选模型",保存。存储里正确地没有
- 迁移来的服务商完全存不了。
- 一份四个槽位都是
deepseek-v4-pro的旧 DeepSeek 配置,迁移后是models: [pro],没有快速模型,这一步是对的。 - 但编辑表单里的状态仍然是
deepseek-v4-flash。原生下拉框因为 flash 不在选项里,显示的是"使用当前所选模型",所以看上去一切正常。 - 保存时,哪怕只改了名字,也会被
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!
Follow-up review requirements completedImplemented in commit
VerificationAutomated:
Manual desktop validation:
All A-D checks were confirmed by the reporter on the desktop client before this commit was pushed. |
GoDiao
left a comment
There was a problem hiding this comment.
感谢这一轮的修改,也感谢你一直耐心跟进这个 PR!上一轮的七条都已经落实,我们逐条对照了代码,新加的测试也确实覆盖了对应的问题。
这次我们还用一份真实的旧版 providers.json 跑了开发版应用,结果都正常:
- 迁移后备份和原文件一致,模型从四个固定槽位变成了列表;
- 迁移来的服务商可以正常编辑、保存;
- 用 OpenRouter 获取模型、选择主模型、发消息都正常。
剩下的两处小问题由我们自己修,不需要你再改:
- 获取模型时上游返回 4xx,现在接口统一返回 502。这是我们上一轮写错了:原来的 400 才是对的,你是按我们的说法改的。我们会把这一行和对应的测试一起改回来。
- 服务商表单保存失败时只打了
console.error,用户看不到。这个在 main 上本来就有,之前说过会另外修。
另外,这个 PR 合并后,旧版本读不了迁移后的配置。我们会在发布说明里写明:降级前需要先用 providers.json.bak-before-migration-* 恢复。
批准,我们这边会合并。再次感谢!
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
SavedProvider.modelsis now an ordered{ id, name? }[];fastModeloptionally references an id in that list. Presets use the same shape. Legacymain,sonnet,opus,haikuvalues migrate in that order, are deduplicated, and setfastModelfrom a distincthaiku./v1/modelsresponses preserve ids and optional names; Anthropic discovery followshas_more,last_id, andafter_id, requestslimit=1000, and has a 10-page safety cap.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.Review follow-ups implemented
model/modelContextfields, and ids containing:such asqwen3.5:9bare kept intact.Issue #120 开发对照 — 中文
SavedProvider.models改为有序的{ id, name? }[];fastModel可选,并且必须引用列表中的模型 ID。预设使用相同结构。旧版main、sonnet、opus、haiku按该顺序迁移、去重,并在haiku与main不同时设置快速模型。/v1/models均保留模型 ID 和可选名称;Anthropic 支持has_more、last_id、after_id分页,使用limit=1000,并设置 10 页安全上限。Review 后续 7 项修复
model/modelContext;qwen3.5:9b等包含冒号的模型 ID 保持完整。Schema / Schema 说明
Rules / 规则:
modelsis ordered; the first model is the provider default when no session/task override exists. /models有顺序;没有会话或任务覆盖值时,第一项是 Provider 默认模型。nameis display-only; runtime requests always useid. /name只用于显示,运行时请求始终使用id。fastModel, when present, must equal an id inmodels. /fastModel存在时必须引用models中的 ID。main → sonnet → opus → haiku, with duplicate ids removed. / 旧结构按main → sonnet → opus → haiku迁移并去重。Runtime mapping / 运行时映射:
ANTHROPIC_MODEL,ANTHROPIC_DEFAULT_SONNET_MODEL,ANTHROPIC_DEFAULT_OPUS_MODELANTHROPIC_DEFAULT_HAIKU_MODEL,ANTHROPIC_SMALL_FAST_MODELfastModel, otherwise selected model / Provider 快速模型,未配置则使用选中模型Validation / 验证
Automated / 自动化:
git diff --check: passed. /git diff --check通过。Manual Ollama validation / Ollama 人工验证:
qwen3.8:latest,qwen3.5:9b, andqwen3.5:4b.qwen3.8:latest、qwen3.5:9b、qwen3.5:4b。qwen3.5:9b; the conversation succeeded and legacymodel/modelContextremained unchanged. / PASS:会话运行时保留完整的qwen3.5:9b;对话成功,旧model/modelContext未被改写。Pending / 待验证:
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