fix(web): edit a declared provider's name and protocol

The Models editor card curated its pi-ai fields by what every route has,
so `displayName` and `api` — the two a hand-declared route names for
itself — were asked for at creation and then reachable only through
settings.yaml. The editor now renders both for a route the directory
reports as declared, from the same namespace schema the create card
reads. A catalog route gets neither: it defaults its name from its
catalog entry, and its models each carry their own protocol, so a
route-level one could only override all of them.

Clearing the name unsets it and the route falls back to its id, which is
what the field's placeholder shows; storing the empty string would be
refused by the adapter. A declared profile naming no protocol selects
nothing rather than reading as if it had picked the first choice.

The Provider ID stays fixed: it is the settings dict key, it is
referenced from `agent-default-model` and every logged request header,
and it is the stem of a credential reference the page can never read
back to move.

Fixes #2204
This commit is contained in:
Yichen Jiang
2026-08-10 22:40:57 +08:00
parent cd226191a8
commit 72344fce93
14 changed files with 300 additions and 21 deletions

View File

@@ -28,6 +28,7 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/models-settings', import
const EMPTY_EXPECTED = join(SNAPSHOT_DIR, 'empty.expected.md')
const CONFIGURED_EXPECTED = join(SNAPSHOT_DIR, 'configured.expected.md')
const DECLARED_EXPECTED = join(SNAPSHOT_DIR, 'declared.expected.md')
const DECLARED_EDIT_EXPECTED = join(SNAPSHOT_DIR, 'declared-edit.expected.md')
const NATIVE_DELETE_EXPECTED = join(SNAPSHOT_DIR, 'native-delete.expected.md')
const DELETE_EXPECTED = join(SNAPSHOT_DIR, 'delete.expected.md')
const MODE = webSnapshotMode()
@@ -209,6 +210,37 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
it('reopens the name and protocol a declared route was created with', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-models-declared-identity'))
const dialog = page.getByRole('dialog', { name: '设置' })
await dialog.getByRole('button', { name: '编辑 Acme Gateway (acme-gateway)' }).click()
await dialog.getByText('自定义设置').click()
// The create card asked this route for a name and a protocol because
// nothing can default them; the editor reaches the same two fields rather
// than sending the user to settings.yaml for what only this route names.
const protocol = dialog.getByLabel('API 协议')
await protocol.waitFor({ timeout: 10_000 })
expect(await protocol.inputValue()).toBe('openai-completions')
const name = dialog.getByLabel('显示名称', { exact: true })
expect(await name.inputValue()).toBe('Acme Gateway')
const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(DECLARED_EDIT_EXPECTED, snapshot, MODE)
await protocol.selectOption('anthropic-messages')
await name.fill('Acme 网关')
await dialog.getByRole('button', { name: '保存', exact: true }).click()
await expect.poll(async () => dialog.getByLabel('API 协议').count(), { timeout: 10_000 }).toBe(0)
// The adapter re-resolved the route under the new protocol and re-registered
// it under the new name: an unserviceable profile would have been refused
// at the write instead, and a rename that did not re-register would leave
// the old label on the row.
await dialog.getByText('Acme 网关', { exact: true }).first().waitFor({ timeout: 10_000 })
const document = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
expect(document).toContain('api: anthropic-messages')
expect(document).toContain('displayName: Acme 网关')
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
it('confirms an identified provider deletion before removing its profile and key', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-models-delete'))
const settingsDialog = page.getByRole('dialog', { name: '设置' })
@@ -243,8 +275,8 @@ describe('web e2e: Models settings page configures a dormant provider', () => {
it.skipIf(MODE === 'record')('keeps the fixture inventory closed', async () => {
await assertFixtureInventory(SNAPSHOT_DIR, [
'configured.expected.md', 'declared.expected.md', 'delete.expected.md',
'empty.expected.md', 'native-delete.expected.md',
'configured.expected.md', 'declared-edit.expected.md', 'declared.expected.md',
'delete.expected.md', 'empty.expected.md', 'native-delete.expected.md',
])
})
})

View File

@@ -0,0 +1,65 @@
- dialog "设置":
- navigation:
- text: 设置
- button "通用设置":
- img
- text: 通用设置
- button "模型":
- img
- text: 模型
- button "Agent 预设":
- img
- text: Agent 预设
- button "打开配置文件"
- button "关闭":
- img
- text: 关闭
- heading "模型" [level=2]
- paragraph: 填入各提供方的 API 密钥即可使用其模型。
- list:
- listitem:
- text: minimax-cn
- img "API 密钥已配置"
- button "编辑 minimax-cn": 编辑
- button "删除 minimax-cn": 删除
- listitem:
- text: Acme Gateway 自定义
- button "编辑 Acme Gateway (acme-gateway)": 编辑
- button "删除 Acme Gateway (acme-gateway)": 删除
- text: Acme Gateway acme-gateway API 密钥
- textbox "API 密钥":
- /placeholder: 输入 API 密钥,或留空使用环境认证
- group:
- text: 自定义设置 显示名称
- textbox "显示名称":
- /placeholder: acme-gateway
- text: Acme Gateway
- text: API 地址
- textbox "API 地址":
- /placeholder: https://gateway.acme.example/v1
- text: https://gateway.acme.example/v1
- text: API 协议
- combobox "API 协议":
- option "openai-completions" [selected]
- option "openai-responses"
- option "anthropic-messages"
- region "模型目录":
- text: 模型目录 已自定义模型目录
- button "恢复默认模型"
- button "获取可用模型"
- textbox "模型 ID 1":
- /placeholder: 模型 ID
- text: acme-large
- textbox "显示名称 1":
- /placeholder: 显示名称
- button "容量 1"
- button "删除模型 1"
- button "添加模型"
- button "取消"
- button "保存"
- button "添加提供方":
- img
- text: 添加提供方
- button "添加自定义提供方":
- img
- text: 添加自定义提供方