mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-13 13:09:52 +08:00
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.
160 lines
5.6 KiB
TypeScript
160 lines
5.6 KiB
TypeScript
/**
|
|
* The rules the provider record (lib/providers.ts) must keep, and the parts
|
|
* of the UI that follow its order. The record replaced thirteen tables keyed
|
|
* by provider name; while they were being replaced, a fixture of their old
|
|
* values checked the derived ones, and the behavior snapshot
|
|
* (provider-matrix.test.ts) still holds every scenario's answer.
|
|
*/
|
|
import { readFileSync } from "node:fs"
|
|
import path from "node:path"
|
|
import { describe, expect, it, vi } from "vitest"
|
|
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 } from "@/lib/types/model-config"
|
|
|
|
vi.mock("@/lib/admin/settings", () => ({
|
|
loadSettings: () => ({}),
|
|
getEnvFallback: (key: string) => process.env[key] ?? null,
|
|
}))
|
|
|
|
const sorted = (list: Iterable<string>) => [...list].sort()
|
|
|
|
describe("the provider record", () => {
|
|
it("is what the rest of the app imports as PROVIDER_INFO", () => {
|
|
expect(PROVIDER_INFO).toBe(PROVIDERS)
|
|
expect(PROVIDER_NAMES).toHaveLength(24)
|
|
})
|
|
|
|
it("has 21 providers taking part in server key auto-detection", () => {
|
|
// credential apiKey or vertexKey: the providers whose <P>_API_KEY the
|
|
// server must have; aws, optionalKey and none need no key
|
|
const detected = PROVIDER_NAMES.filter((p) =>
|
|
["apiKey", "vertexKey"].includes(PROVIDERS[p].credential),
|
|
)
|
|
expect(detected).toHaveLength(21)
|
|
// Ollama has a key variable too, an optional one
|
|
const withKeyVar = PROVIDER_NAMES.filter((p) => !!envVarsOf(p).apiKey)
|
|
expect(sorted(detected)).toEqual(
|
|
sorted(withKeyVar.filter((p) => p !== "ollama")),
|
|
)
|
|
})
|
|
|
|
it("writes every note and hint in every UI language", () => {
|
|
for (const p of PROVIDER_NAMES) {
|
|
for (const copy of [PROVIDERS[p].note, PROVIDERS[p].baseUrlHint]) {
|
|
if (!copy) continue
|
|
expect(sorted(Object.keys(copy))).toEqual(sorted(i18n.locales))
|
|
for (const text of Object.values(copy)) {
|
|
expect(text.trim()).not.toBe("")
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
it("only calls https defaults on its own", () => {
|
|
expect(publicDefaultUrl("sglang")).toBeUndefined()
|
|
expect(publicDefaultUrl("openai")).toBe("https://api.openai.com/v1")
|
|
expect(publicDefaultUrl("bedrock")).toBeUndefined()
|
|
})
|
|
|
|
it("gives the no-key and fixed-credential providers no key variable to detect", () => {
|
|
for (const p of PROVIDER_NAMES) {
|
|
const { credential, envPrefix, fixedCredentials } = PROVIDERS[p]
|
|
if (credential === "none") expect([p, envPrefix]).toEqual([p, null])
|
|
if (credential === "aws") expect([p, envPrefix]).toEqual([p, null])
|
|
if (fixedCredentials) {
|
|
expect(["aws", "vertexKey", "optionalKey"]).toContain(
|
|
credential,
|
|
)
|
|
}
|
|
}
|
|
const fixed = PROVIDER_NAMES.filter(
|
|
(p) => PROVIDERS[p].fixedCredentials,
|
|
)
|
|
expect(fixed).toEqual(["bedrock", "vertexai", "ollama"])
|
|
})
|
|
|
|
it("keeps a short model list exactly for the providers that cannot list models", () => {
|
|
for (const p of PROVIDER_NAMES) {
|
|
const { modelList, models } = PROVIDERS[p]
|
|
if (modelList) {
|
|
expect([p, models]).toEqual([p, undefined])
|
|
} else {
|
|
expect([p, models?.length ?? 0]).not.toEqual([p, 0])
|
|
}
|
|
}
|
|
})
|
|
|
|
it("names every server variable in env.example", () => {
|
|
// env.example is written by hand; a new provider adds its two lines
|
|
const example = readFileSync(
|
|
path.join(process.cwd(), "env.example"),
|
|
"utf8",
|
|
)
|
|
for (const p of PROVIDER_NAMES) {
|
|
const { apiKey, baseUrl } = envVarsOf(p)
|
|
for (const name of [apiKey, baseUrl]) {
|
|
if (!name) continue
|
|
expect([p, name, example.includes(name)]).toEqual([
|
|
p,
|
|
name,
|
|
true,
|
|
])
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
describe("the picker's order", () => {
|
|
it("groups every provider once, in record order", () => {
|
|
expect(PROVIDER_GROUPS).toEqual([
|
|
{
|
|
id: "vendors",
|
|
providers: [
|
|
"openai",
|
|
"anthropic",
|
|
"google",
|
|
"deepseek",
|
|
"qwen",
|
|
"doubao",
|
|
"glm",
|
|
"kimi",
|
|
"minimax",
|
|
"mimo",
|
|
],
|
|
},
|
|
{
|
|
id: "aggregators",
|
|
providers: [
|
|
"openrouter",
|
|
"aihubmix",
|
|
"siliconflow",
|
|
"modelscope",
|
|
"novita",
|
|
"atlascloud",
|
|
"qiniu",
|
|
"gateway",
|
|
],
|
|
},
|
|
{
|
|
id: "cloud",
|
|
providers: ["bedrock", "vertexai", "azure", "edgeone"],
|
|
},
|
|
{ id: "selfHosted", providers: ["ollama", "sglang"] },
|
|
])
|
|
})
|
|
|
|
it("offers the same popular providers", () => {
|
|
expect(POPULAR_PROVIDERS).toEqual([
|
|
"openai",
|
|
"anthropic",
|
|
"google",
|
|
"deepseek",
|
|
"openrouter",
|
|
"qwen",
|
|
])
|
|
})
|
|
})
|