From 98c083bb3161dd4ad9821e2bd0b14e54d3d91dd4 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Fri, 18 Sep 2026 15:01:24 +0800 Subject: [PATCH] fix(web): match custom provider URL placeholders to protocol --- .../client/ui-settings-models/README.i18n.yaml | 4 ++-- packages/client/ui-settings-models/README.md | 2 +- packages/client/ui-settings-models/README.zh.md | 2 +- .../src/client/CustomProviderCard.tsx | 4 +++- .../ui-settings-models/src/client/locales.ts | 2 ++ .../tests/provider-form.client.spec.tsx | 14 +++++++++++--- 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/client/ui-settings-models/README.i18n.yaml b/packages/client/ui-settings-models/README.i18n.yaml index 94fe93fbe3..c3c33c6f90 100644 --- a/packages/client/ui-settings-models/README.i18n.yaml +++ b/packages/client/ui-settings-models/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-settings-models/README.md -README.md: 6feb95d987f11bcf81b9bfa2c67068567244dcec -README.zh.md: 9316d698bdf11f630f5815b9fffd0bbdc4546031 +README.md: c1567814b5de39f9b8819fcc8848e36c0a6ff842 +README.zh.md: baa0f02e8274a7146f9eade36d3ad464ac944b27 diff --git a/packages/client/ui-settings-models/README.md b/packages/client/ui-settings-models/README.md index 6feb95d987..c1567814b5 100644 --- a/packages/client/ui-settings-models/README.md +++ b/packages/client/ui-settings-models/README.md @@ -43,7 +43,7 @@ Expand **Customized settings → Model options** to edit each model. Both provid ### Adding and deleting providers -The add flow is a card carrying the dormant-directory provider select — a bare-mounted `llm-pi-ai` offers its whole installed catalog before any route exists. **Add a custom provider** declares a route pi-ai does not ship; the create card asks for a unique **Provider ID**, an endpoint, a protocol, and at least one uniquely-identified model, because nothing can default those. The endpoint must be a parseable HTTP or HTTPS URL; localhost, IPv4 and IPv6 literals, and custom ports remain valid. A syntax error blocks both discovery and creation at the field, while a request failure remains a separate provider error. **Fetch available models** asks the `llm/discoverModels` Remote about the endpoint the form shows, so adding a provider is one pass instead of save-then-return; the reply opens a searchable picker rather than being written, and nothing is written until **Add selected**. Each selected candidate copies its id, display name, context window, output-token cap, and disclosed input types into the editable row when disclosed, while an existing row retains its user-tuned values. Search matches model ids and optional display names without clearing hidden selections. **Select all** adds the visible results, while **Deselect all** clears the entire selection so hidden results cannot be adopted accidentally. A row is deletable only when the user layer alone carries it (removal restores the composition base), and its confirmation dialog names the provider. +The add flow is a card carrying the dormant-directory provider select — a bare-mounted `llm-pi-ai` offers its whole installed catalog before any route exists. **Add a custom provider** declares a route pi-ai does not ship; the create card asks for a unique **Provider ID**, an endpoint, a protocol, and at least one uniquely-identified model, because nothing can default those. The endpoint placeholder follows the selected protocol: OpenAI Chat Completions and Responses show `https://gateway.example/v1`, while Anthropic Messages shows `https://gateway.example` because its SDK appends `/v1/messages`. Changing the protocol preserves any address already entered. The endpoint must be a parseable HTTP or HTTPS URL; localhost, IPv4 and IPv6 literals, and custom ports remain valid. A syntax error blocks both discovery and creation at the field, while a request failure remains a separate provider error. **Fetch available models** asks the `llm/discoverModels` Remote about the endpoint the form shows, so adding a provider is one pass instead of save-then-return; the reply opens a searchable picker rather than being written, and nothing is written until **Add selected**. Each selected candidate copies its id, display name, context window, output-token cap, and disclosed input types into the editable row when disclosed, while an existing row retains its user-tuned values. Search matches model ids and optional display names without clearing hidden selections. **Select all** adds the visible results, while **Deselect all** clears the entire selection so hidden results cannot be adopted accidentally. A row is deletable only when the user layer alone carries it (removal restores the composition base), and its confirmation dialog names the provider. ### First-run dialogs diff --git a/packages/client/ui-settings-models/README.zh.md b/packages/client/ui-settings-models/README.zh.md index 9316d698bd..baa0f02e82 100644 --- a/packages/client/ui-settings-models/README.zh.md +++ b/packages/client/ui-settings-models/README.zh.md @@ -43,7 +43,7 @@ kind: "package-reference" ### 新增与删除提供方 -「新增」流程是一张承载休眠目录提供方选择框的卡片——裸挂载的 `llm-pi-ai` 在任何路由存在之前就能提供其完整的已安装 catalog。**添加自定义提供方**声明一条 pi-ai 不提供的路由;创建卡片会索要唯一的 **Provider ID**、端点、协议与至少一个可唯一识别的模型,因为没有东西能为它们兜底。端点必须是可解析的 HTTP 或 HTTPS URL;localhost、IPv4 与 IPv6 字面地址以及自定义端口仍然有效。语法错误会在字段处阻止询问与创建,请求失败则继续作为独立的提供方错误显示。**获取可用模型**通过 `llm/discoverModels` Remote 查询表单显示的端点,因此新增提供方一次即可完成,而非先保存再返回;回复打开的是可搜索选择器而非直接写入,只有点击**添加所选**才会写入。每个选中候选会在提供方公布相应信息时,把 id、显示名、上下文窗口、最大输出 token 数和已公布的输入类型复制进可编辑行;已经存在的行保留用户调整过的值。搜索会匹配模型 id 与可选显示名称,且不会清除隐藏项的勾选状态。**全选**会加入可见结果,而**取消全选**会清空全部勾选,以免意外采用隐藏结果。只有用户层单独携带某行时,该行才可删除(删除会恢复组合基线),其确认对话框会指名该提供方。 +「新增」流程是一张承载休眠目录提供方选择框的卡片——裸挂载的 `llm-pi-ai` 在任何路由存在之前就能提供其完整的已安装 catalog。**添加自定义提供方**声明一条 pi-ai 不提供的路由;创建卡片会索要唯一的 **Provider ID**、端点、协议与至少一个可唯一识别的模型,因为没有东西能为它们兜底。端点占位示例随所选协议变化:OpenAI Chat Completions 与 Responses 显示 `https://gateway.example/v1`,Anthropic Messages 显示 `https://gateway.example`,因为其 SDK 会追加 `/v1/messages`。切换协议会保留已输入的地址。端点必须是可解析的 HTTP 或 HTTPS URL;localhost、IPv4 与 IPv6 字面地址以及自定义端口仍然有效。语法错误会在字段处阻止询问与创建,请求失败则继续作为独立的提供方错误显示。**获取可用模型**通过 `llm/discoverModels` Remote 查询表单显示的端点,因此新增提供方一次即可完成,而非先保存再返回;回复打开的是可搜索选择器而非直接写入,只有点击**添加所选**才会写入。每个选中候选会在提供方公布相应信息时,把 id、显示名、上下文窗口、最大输出 token 数和已公布的输入类型复制进可编辑行;已经存在的行保留用户调整过的值。搜索会匹配模型 id 与可选显示名称,且不会清除隐藏项的勾选状态。**全选**会加入可见结果,而**取消全选**会清空全部勾选,以免意外采用隐藏结果。只有用户层单独携带某行时,该行才可删除(删除会恢复组合基线),其确认对话框会指名该提供方。 ### 首次运行弹窗 diff --git a/packages/client/ui-settings-models/src/client/CustomProviderCard.tsx b/packages/client/ui-settings-models/src/client/CustomProviderCard.tsx index 9b4953d86b..4f382e92a1 100644 --- a/packages/client/ui-settings-models/src/client/CustomProviderCard.tsx +++ b/packages/client/ui-settings-models/src/client/CustomProviderCard.tsx @@ -236,7 +236,9 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode { className={styles['input']} type="text" value={baseURL} - placeholder={t('customBaseUrlPlaceholder')} + placeholder={t(protocol === 'anthropic-messages' + ? 'customAnthropicBaseUrlPlaceholder' + : 'customBaseUrlPlaceholder')} aria-label={t('baseUrl')} aria-invalid={baseUrlInvalid} disabled={profileDisabled} diff --git a/packages/client/ui-settings-models/src/client/locales.ts b/packages/client/ui-settings-models/src/client/locales.ts index d06ee79c52..2b0f92d8df 100644 --- a/packages/client/ui-settings-models/src/client/locales.ts +++ b/packages/client/ui-settings-models/src/client/locales.ts @@ -93,6 +93,7 @@ export const en = { customBaseUrlInvalid: 'Enter a valid HTTP or HTTPS URL.', customNeedsModels: 'A custom provider needs at least one model.', customBaseUrlPlaceholder: 'https://gateway.example/v1', + customAnthropicBaseUrlPlaceholder: 'https://gateway.example', settingsPathUnresolvable: 'unresolvable settings path', create: 'Create provider', creating: 'Creating\u2026', @@ -204,6 +205,7 @@ export const zh: { [Key in keyof typeof en]: string } = { customBaseUrlInvalid: '请输入有效的 HTTP 或 HTTPS 地址。', customNeedsModels: '自定义提供方至少需要一个模型。', customBaseUrlPlaceholder: 'https://gateway.example/v1', + customAnthropicBaseUrlPlaceholder: 'https://gateway.example', settingsPathUnresolvable: '无法解析设置路径', create: '创建提供方', creating: '创建中\u2026', diff --git a/packages/client/ui-settings-models/tests/provider-form.client.spec.tsx b/packages/client/ui-settings-models/tests/provider-form.client.spec.tsx index 0659607e8d..9b6ff1f55a 100644 --- a/packages/client/ui-settings-models/tests/provider-form.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/provider-form.client.spec.tsx @@ -1372,10 +1372,18 @@ describe('hand-declared providers', () => { it('creates with the chosen protocol and no display name', async () => { const { mutate, onClose } = mountCard() + const baseUrl = screen.getByLabelText(en.baseUrl) + const protocol = screen.getByLabelText(en.customApi) + + expect(baseUrl.placeholder).toBe('https://gateway.example/v1') + fireEvent.change(protocol, { target: { value: 'openai-responses' } }) + expect(baseUrl.placeholder).toBe('https://gateway.example/v1') fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme' } }) - fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } }) - fireEvent.change(screen.getByLabelText(en.customApi), { target: { value: 'anthropic-messages' } }) + fireEvent.change(baseUrl, { target: { value: 'https://acme.test/anthropic' } }) + fireEvent.change(protocol, { target: { value: 'anthropic-messages' } }) + expect(baseUrl.placeholder).toBe('https://gateway.example') + expect(baseUrl.value).toBe('https://acme.test/anthropic') fireEvent.click(screen.getByRole('button', { name: en.addModel })) fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'm' } }) fireEvent.click(screen.getByText(en.create)) @@ -1387,7 +1395,7 @@ describe('hand-declared providers', () => { // nothing ever sets. The with-key case is covered above. expect(firstMutate(mutate).ops[0]?.value).toEqual({ api: 'anthropic-messages', - baseURL: 'https://acme.test/v1', + baseURL: 'https://acme.test/anthropic', models: [{ id: 'm' }], }) })