Files
next-ai-draw-io/tests/e2e/model-test.spec.ts
T
dayuan.jiang c24aae6de0 refactor(providers): one model factory for chat and the settings Test button
- getAIModel resolves credentials (client key, server env vars, the
  existing SSRF rules) and createModel builds the model by SDK. The
  provider-by-provider switch shrinks from 24 cases to the few that
  differ (lib/ai-providers.ts 1531 -> 1106 lines)
- /api/validate-model calls getAIModel instead of its own 24-case switch
  (503 -> 175 lines), which had drifted from the chat: it built Azure
  with createOpenAI, Kimi and MiMo with createOpenAI instead of
  createDeepSeek, and the official OpenAI endpoint with Chat Completions.
  A passing test now means the chat works
- Plain OpenAI-compatible providers (SiliconFlow, SGLang, ModelScope,
  GLM, Qwen, Qiniu, Novita, Atlas Cloud, EdgeOne, Doubao, MiniMax in
  OpenAI mode, AIHubMix on a custom URL) use @ai-sdk/openai-compatible,
  which reads reasoning_content, so their reasoning shows, and accepts
  SGLang's stream as is (its 95-line stream rewrite is gone).
  includeUsage keeps token usage for quotas. <think> tags in their text
  become reasoning (extractReasoningMiddleware)
- SGLang without a base URL used OpenAI's endpoint; it now defaults to
  http://127.0.0.1:8000/v1 like the Test button did
- Chat requests to a client base URL refuse redirects, as the Test
  button already did (redirectGuardedFetch moves to lib/ssrf-protection)
- The Test button streams like the chat (the ModelScope special case is
  gone), times out after 15 s, does not retry, asks the model to call a
  ping tool and warns when it answers without one, and tests all models
  at once. The time each test took shows on its check mark
- Unknown provider names are rejected with Object.hasOwn, and the error
  texts list providers from PROVIDER_INFO instead of hand-kept lists
2026-10-04 13:31:47 +09:00

68 lines
2.4 KiB
TypeScript

import { expect, test } from "@playwright/test"
import { getIframe } from "./lib/fixtures"
// One provider with three models; the test endpoint answers each differently
const CONFIG = {
version: 1,
providers: [
{
id: "p1",
provider: "glm",
apiKey: "test-key",
models: [
{ id: "m1", modelId: "model-ok" },
{ id: "m2", modelId: "model-no-tools" },
{ id: "m3", modelId: "model-broken" },
],
},
],
}
test("the Test button checks all models at once and shows each result", async ({
page,
}) => {
await page.addInitScript((config) => {
localStorage.setItem(
"next-ai-draw-io-model-configs",
JSON.stringify(config),
)
}, CONFIG)
const started: string[] = []
await page.route("**/api/validate-model", async (route) => {
const { modelId } = route.request().postDataJSON()
started.push(modelId)
// Answer only once all three requests arrived: they run in parallel
while (started.length < 3) await new Promise((r) => setTimeout(r, 50))
const answers: Record<string, object> = {
"model-ok": { valid: true, responseTime: 1234 },
"model-no-tools": {
valid: true,
responseTime: 800,
warning:
"Connected, but the model answered without calling a tool.",
},
"model-broken": { valid: false, error: "Model not found" },
}
await route.fulfill({ json: answers[modelId] })
})
await page.goto("/", { waitUntil: "networkidle" })
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
await page.locator("button:has(svg.lucide-bot)").first().click()
await page.getByText("Configure Models...").click()
const dialog = page.locator('[role="dialog"]')
await dialog.getByText("GLM (Zhipu)").first().click()
await dialog.getByRole("button", { name: "Test", exact: true }).click()
await expect(
dialog.getByText("answered without calling a tool"),
).toBeVisible({ timeout: 15000 })
await expect(dialog.getByText("Model not found")).toBeVisible()
await expect(dialog.locator('[title="1.2 s"]')).toBeVisible()
expect(started.sort()).toEqual([
"model-broken",
"model-no-tools",
"model-ok",
])
})