feat(providers): an OpenAI-compatible preset for any vLLM, LM Studio or gateway URL

Step 8 of the provider refactor plan, a new provider made of one record.
'custom' runs on @ai-sdk/openai-compatible with <think> extraction, merges
system messages (open-source chat templates reject several), has no default
URL (so a key alone does not count as connected, and the chat endpoint is
shown under the URL field) and lists models from its /models. Server side:
CUSTOM_API_KEY and CUSTOM_BASE_URL, ADMIN_CUSTOM_* in the admin panel.
env.example and the three provider docs gained a section; the picker count
in the e2e test is 25 and a new e2e test walks through the preset. The
behavior snapshot gained the new provider's 146 rows; its two 'No AI provider
configured' rows list CUSTOM_API_KEY now.
This commit is contained in:
dayuan.jiang
2026-10-12 21:17:51 +09:00
parent 717f7e9445
commit b2cadfa5bf
9 changed files with 246 additions and 9 deletions
+29 -1
View File
@@ -127,7 +127,7 @@ test("the provider picker searches, marks added ones, and Enter picks", async ({
dialog.getByTestId("pick-provider-anthropic"),
).not.toContainText("Added")
await expect(dialog.locator('[data-testid^="pick-provider-"]')).toHaveCount(
24,
25,
)
await search.fill("zzz")
@@ -596,6 +596,34 @@ test("Azure needs its resource URL before it counts as connected", async ({
await expect(dialog.getByTestId("test-models")).toBeEnabled()
})
test("the OpenAI-compatible preset takes any URL and shows where requests go", async ({
page,
}) => {
await openApp(page)
await openSettings(page)
const dialog = settingsDialog(page)
// Not among the popular ones: it is in the full picker
await dialog.getByRole("button", { name: /^See all \d+ providers/ }).click()
await dialog.getByTestId("pick-provider-custom").click()
await expect(
dialog.getByRole("heading", { name: "OpenAI-compatible", exact: true }),
).toBeVisible()
// No URL yet: a key alone does not count as connected
await dialog.locator("#api-key").fill("any")
await expect(dialog.getByTestId("step-1-todo")).toBeVisible()
await expect(
dialog.getByText("Fill in the API key and the base URL first."),
).toBeVisible()
// Its options start open; the URL field shows the chat endpoint
await dialog.locator("#base-url").fill("http://127.0.0.1:1234/v1")
await expect(
dialog.getByText(
"Requests go to http://127.0.0.1:1234/v1/chat/completions",
),
).toBeVisible()
await expect(dialog.getByTestId("step-1-done")).toBeVisible()
})
test("a model id typed for one provider does not follow to the next", async ({
page,
}) => {