feat(settings): providers that can list their models fetch the list instead of a hand-written one

Step 7 of the provider refactor plan, an intended UI behavior change. The
hand-written SUGGESTED_MODELS table (179 ids for 19 providers) is gone. The
six providers that cannot list their models online (Bedrock, Vertex AI,
Azure, EdgeOne, Doubao, MiniMax) keep a short list on their record, still
get its first model when added, and still show quick-add chips and the admin
panel's dropdown. For the other 18 the settings dialog fetches the provider's
own list once the user has entered credentials on the page while it has no
model yet (600 ms after the typing pauses), and opens the picker with the
result; the 'Get all models' button and the model id input work as before.
Credentials a provider already had when its page opened fetch nothing by
themselves. The admin panel shows the 'suggested' dropdown only for the six.

Two e2e tests follow the change: the first-provider flow mocks the list
endpoint and picks the model from the list; the empty-list test checks that a
new key fetches again and opens the list once it has models.
This commit is contained in:
dayuan.jiang
2026-10-12 21:17:51 +09:00
parent e1c083dae0
commit 717f7e9445
5 changed files with 80 additions and 289 deletions
+30 -11
View File
@@ -51,26 +51,39 @@ test("a first provider: pick it, fill in the key, test, use it", async ({
tested.push(route.request().postDataJSON().modelId)
await route.fulfill({ json: { valid: true, responseTime: 1400 } })
})
const listedWith: string[] = []
await page.route("**/api/provider-models", async (route) => {
listedWith.push(route.request().postDataJSON().apiKey)
await route.fulfill({
json: {
models: [{ id: "deepseek-v4-pro" }, { id: "deepseek-chat" }],
},
})
})
await openApp(page)
await openSettings(page)
const dialog = settingsDialog(page)
await expect(dialog.getByText("Use your own API key")).toBeVisible()
await dialog.getByTestId("pick-provider-deepseek").click()
// The first suggested model is added, and the key field has the cursor
// No model yet (DeepSeek lists its own), and the key field has the cursor
await expect(
dialog.getByRole("heading", { name: "DeepSeek", exact: true }),
).toBeVisible()
await expect(dialog.locator('input[title="deepseek-v4-pro"]')).toBeVisible()
await expect(dialog.locator("#api-key")).toBeFocused()
await expect(dialog.getByTestId("step-1-todo")).toBeVisible()
await expect(dialog.getByTestId("step-2-done")).toBeVisible()
await expect(dialog.getByTestId("step-2-todo")).toBeVisible()
const testButton = dialog.getByTestId("test-models")
await expect(testButton).toBeDisabled()
await expect(dialog.getByText("Fill in the API key first.")).toBeVisible()
// The key is in: the list is fetched once and opens; pick a model
await dialog.locator("#api-key").fill("sk-1234")
await expect(dialog.getByTestId("step-1-done")).toBeVisible()
await page.getByRole("option", { name: "deepseek-v4-pro" }).click()
expect(listedWith).toEqual(["sk-1234"])
await expect(dialog.locator('input[title="deepseek-v4-pro"]')).toBeVisible()
await expect(dialog.getByTestId("step-2-done")).toBeVisible()
await expect(testButton).toHaveText("Test deepseek-v4-pro")
await testButton.click()
// The visible panel, not the screen reader's copy
@@ -648,10 +661,11 @@ test("a model list that loads leaves a refused test's mark", async ({
})
test("an empty model list says so and opens nothing", async ({ page }) => {
let models: { id: string }[] = []
await page.route("**/api/provider-models", (route) =>
route.fulfill({ json: { models: [] } }),
route.fulfill({ json: { models } }),
)
// OpenAI has suggested models: they come back once the key changes
// OpenAI lists its own models: nothing to browse until a list arrives
await openApp(page, {
config: {
version: 1,
@@ -663,16 +677,21 @@ test("an empty model list says so and opens nothing", async ({ page }) => {
await openSettings(page)
const dialog = settingsDialog(page)
await dialog.getByTestId("provider-row-o1").click()
// A key the provider already had fetches nothing by itself
await page.waitForTimeout(800)
await expect(
dialog.getByRole("button", { name: "Browse models" }),
).toHaveCount(0)
await dialog.getByRole("button", { name: /^Get all models from/ }).click()
await expect(
dialog.getByText("The provider returned no models."),
).toBeVisible()
await dialog.locator("#api-key").fill("k-new")
await expect(
dialog.getByRole("button", { name: "Browse models" }),
).toBeVisible()
await page.waitForTimeout(300)
// The picker did not open by itself
await expect(page.getByRole("listbox")).toHaveCount(0)
await expect(dialog.locator("#api-key")).toBeFocused()
// A new key fetches the list again by itself and opens it once it has models
models = [{ id: "fresh-model" }]
await dialog.locator("#api-key").fill("k-new")
await expect(
page.getByRole("option", { name: "fresh-model" }),
).toBeVisible()
})
+2 -2
View File
@@ -12,7 +12,7 @@ import { envVarsOf } from "@/lib/ai-providers"
import { i18n } from "@/lib/i18n/config"
import { POPULAR_PROVIDERS, PROVIDER_GROUPS } from "@/lib/provider-setup"
import { PROVIDER_NAMES, PROVIDERS, publicDefaultUrl } from "@/lib/providers"
import { PROVIDER_INFO, SUGGESTED_MODELS } from "@/lib/types/model-config"
import { PROVIDER_INFO } from "@/lib/types/model-config"
vi.mock("@/lib/admin/settings", () => ({
loadSettings: () => ({}),
@@ -82,7 +82,7 @@ describe("the provider record", () => {
if (modelList) {
expect([p, models]).toEqual([p, undefined])
} else {
expect([p, models]).toEqual([p, SUGGESTED_MODELS[p]])
expect([p, models?.length ?? 0]).not.toEqual([p, 0])
}
}
})