mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-13 04:59:51 +08:00
refactor(providers): one record per provider, ten name-keyed tables derived from it
Step 2 of the provider refactor plan. lib/providers.ts holds one record per provider (SDK kind, credential kind, env prefix, default URL, picker group, notes and base URL hints in four languages, and the short model list of the six providers that cannot list models). ProviderName is now the record's key set and lib/types/model-config.ts re-exports the record as PROVIDER_INFO, so the twelve importers are unchanged. Derived from the record, under their old names and in their old files: FIXED_CRED_PROVIDERS, PROVIDER_LOGO_MAP and chatRequestUrl's own-path list (model-config.ts), SINGLE_SYSTEM_PROVIDERS, PROVIDER_ENV_VARS and SDK_KNOWS_ENDPOINT (ai-providers.ts), PROVIDER_GROUPS and OPTIONS_OPEN (provider-setup.ts), NO_KEY_NEEDED (provider-models route). tests/unit/providers.test.ts freezes the old tables' values and checks the derived ones against them. The record is written in the picker's order, so the provider lists in two 'No AI provider configured' messages change order; those two snapshot rows are the only snapshot change.
This commit is contained in:
@@ -3260,8 +3260,8 @@
|
||||
{"noProvider":true,"env":"only MIMO_API_KEY","serverProvider":"mimo","getAIModel":{"provider":"mimo","modelId":"test-model","model":{"factory":"deepseek","method":"default","modelId":"test-model","options":{"apiKey":"server-key","baseURL":"https://api.xiaomimimo.com/v1"}}}}
|
||||
{"noProvider":true,"env":"only ATLASCLOUD_API_KEY","serverProvider":"atlascloud","getAIModel":{"provider":"atlascloud","modelId":"test-model","model":{"wrapped":{"factory":"openai-compatible","method":"default","modelId":"test-model","options":{"name":"atlascloud","apiKey":"server-key","baseURL":"https://api.atlascloud.ai/v1","includeUsage":true}},"middleware":{"middleware":"extractReasoning","tagName":"think"}}}}
|
||||
{"noProvider":true,"env":"vertex and openai","serverProvider":null,"getAIModel":{"error":"Multiple AI providers configured (openai, vertexai). Please set AI_PROVIDER to specify which one to use."}}
|
||||
{"noProvider":true,"env":"only OLLAMA_API_KEY","serverProvider":null,"getAIModel":{"error":"No AI provider configured. Please set one of the following API keys in your .env.local file:\n- OPENAI_API_KEY for openai\n- ANTHROPIC_API_KEY for anthropic\n- GOOGLE_GENERATIVE_AI_API_KEY for google\n- GOOGLE_VERTEX_API_KEY for vertexai\n- AZURE_API_KEY for azure\n- OPENROUTER_API_KEY for openrouter\n- AIHUBMIX_API_KEY for aihubmix\n- DEEPSEEK_API_KEY for deepseek\n- SILICONFLOW_API_KEY for siliconflow\n- SGLANG_API_KEY for sglang\n- AI_GATEWAY_API_KEY for gateway\n- DOUBAO_API_KEY for doubao\n- MODELSCOPE_API_KEY for modelscope\n- GLM_API_KEY for glm\n- QWEN_API_KEY for qwen\n- QINIU_API_KEY for qiniu\n- KIMI_API_KEY for kimi\n- MINIMAX_API_KEY for minimax\n- NOVITA_API_KEY for novita\n- MIMO_API_KEY for mimo\n- ATLASCLOUD_API_KEY for atlascloud\n- AWS_ACCESS_KEY_ID for bedrock\nOr set AI_PROVIDER=ollama for local Ollama."}}
|
||||
{"noProvider":true,"env":"no key at all","serverProvider":null,"getAIModel":{"error":"No AI provider configured. Please set one of the following API keys in your .env.local file:\n- OPENAI_API_KEY for openai\n- ANTHROPIC_API_KEY for anthropic\n- GOOGLE_GENERATIVE_AI_API_KEY for google\n- GOOGLE_VERTEX_API_KEY for vertexai\n- AZURE_API_KEY for azure\n- OPENROUTER_API_KEY for openrouter\n- AIHUBMIX_API_KEY for aihubmix\n- DEEPSEEK_API_KEY for deepseek\n- SILICONFLOW_API_KEY for siliconflow\n- SGLANG_API_KEY for sglang\n- AI_GATEWAY_API_KEY for gateway\n- DOUBAO_API_KEY for doubao\n- MODELSCOPE_API_KEY for modelscope\n- GLM_API_KEY for glm\n- QWEN_API_KEY for qwen\n- QINIU_API_KEY for qiniu\n- KIMI_API_KEY for kimi\n- MINIMAX_API_KEY for minimax\n- NOVITA_API_KEY for novita\n- MIMO_API_KEY for mimo\n- ATLASCLOUD_API_KEY for atlascloud\n- AWS_ACCESS_KEY_ID for bedrock\nOr set AI_PROVIDER=ollama for local Ollama."}}
|
||||
{"noProvider":true,"env":"only OLLAMA_API_KEY","serverProvider":null,"getAIModel":{"error":"No AI provider configured. Please set one of the following API keys in your .env.local file:\n- OPENAI_API_KEY for openai\n- ANTHROPIC_API_KEY for anthropic\n- GOOGLE_GENERATIVE_AI_API_KEY for google\n- DEEPSEEK_API_KEY for deepseek\n- QWEN_API_KEY for qwen\n- DOUBAO_API_KEY for doubao\n- GLM_API_KEY for glm\n- KIMI_API_KEY for kimi\n- MINIMAX_API_KEY for minimax\n- MIMO_API_KEY for mimo\n- OPENROUTER_API_KEY for openrouter\n- AIHUBMIX_API_KEY for aihubmix\n- SILICONFLOW_API_KEY for siliconflow\n- MODELSCOPE_API_KEY for modelscope\n- NOVITA_API_KEY for novita\n- ATLASCLOUD_API_KEY for atlascloud\n- QINIU_API_KEY for qiniu\n- AI_GATEWAY_API_KEY for gateway\n- GOOGLE_VERTEX_API_KEY for vertexai\n- AZURE_API_KEY for azure\n- SGLANG_API_KEY for sglang\n- AWS_ACCESS_KEY_ID for bedrock\nOr set AI_PROVIDER=ollama for local Ollama."}}
|
||||
{"noProvider":true,"env":"no key at all","serverProvider":null,"getAIModel":{"error":"No AI provider configured. Please set one of the following API keys in your .env.local file:\n- OPENAI_API_KEY for openai\n- ANTHROPIC_API_KEY for anthropic\n- GOOGLE_GENERATIVE_AI_API_KEY for google\n- DEEPSEEK_API_KEY for deepseek\n- QWEN_API_KEY for qwen\n- DOUBAO_API_KEY for doubao\n- GLM_API_KEY for glm\n- KIMI_API_KEY for kimi\n- MINIMAX_API_KEY for minimax\n- MIMO_API_KEY for mimo\n- OPENROUTER_API_KEY for openrouter\n- AIHUBMIX_API_KEY for aihubmix\n- SILICONFLOW_API_KEY for siliconflow\n- MODELSCOPE_API_KEY for modelscope\n- NOVITA_API_KEY for novita\n- ATLASCLOUD_API_KEY for atlascloud\n- QINIU_API_KEY for qiniu\n- AI_GATEWAY_API_KEY for gateway\n- GOOGLE_VERTEX_API_KEY for vertexai\n- AZURE_API_KEY for azure\n- SGLANG_API_KEY for sglang\n- AWS_ACCESS_KEY_ID for bedrock\nOr set AI_PROVIDER=ollama for local Ollama."}}
|
||||
{"noProvider":true,"env":"AI_PROVIDER=openai","getAIModel":{"error":"OPENAI_API_KEY environment variable is required for openai provider. Please set it in your .env.local file."}}
|
||||
{"noProvider":true,"env":"AI_PROVIDER=anthropic","getAIModel":{"error":"Either ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN environment variable is required for anthropic provider. Please set one in your .env.local file."}}
|
||||
{"noProvider":true,"env":"AI_PROVIDER=google","getAIModel":{"error":"GOOGLE_GENERATIVE_AI_API_KEY environment variable is required for google provider. Please set it in your .env.local file."}}
|
||||
|
||||
@@ -0,0 +1,510 @@
|
||||
/**
|
||||
* The provider record (lib/providers.ts) replaces thirteen tables keyed by
|
||||
* provider name. This file freezes those tables' values as they were written
|
||||
* before the refactor and checks that the record derives the same values.
|
||||
* The fixture goes away once the derived tables have no readers left; the
|
||||
* last two tests stay.
|
||||
*/
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { SINGLE_SYSTEM_PROVIDERS } from "@/lib/ai-providers"
|
||||
import { i18n } from "@/lib/i18n/config"
|
||||
import enDict from "@/lib/i18n/dictionaries/en.json"
|
||||
import jaDict from "@/lib/i18n/dictionaries/ja.json"
|
||||
import zhDict from "@/lib/i18n/dictionaries/zh.json"
|
||||
import zhHantDict from "@/lib/i18n/dictionaries/zh-Hant.json"
|
||||
import {
|
||||
OPTIONS_OPEN,
|
||||
POPULAR_PROVIDERS,
|
||||
PROVIDER_GROUPS,
|
||||
} from "@/lib/provider-setup"
|
||||
import { PROVIDER_NAMES, PROVIDERS, publicDefaultUrl } from "@/lib/providers"
|
||||
import {
|
||||
chatRequestUrl,
|
||||
FIXED_CRED_PROVIDERS,
|
||||
PROVIDER_INFO,
|
||||
PROVIDER_LOGO_MAP,
|
||||
SUGGESTED_MODELS,
|
||||
} from "@/lib/types/model-config"
|
||||
|
||||
vi.mock("@/lib/admin/settings", () => ({
|
||||
loadSettings: () => ({}),
|
||||
getEnvFallback: (key: string) => process.env[key] ?? null,
|
||||
}))
|
||||
|
||||
// The tables as they were before lib/providers.ts existed
|
||||
const fixture = {
|
||||
providerNames: [
|
||||
"openai",
|
||||
"anthropic",
|
||||
"google",
|
||||
"vertexai",
|
||||
"azure",
|
||||
"bedrock",
|
||||
"ollama",
|
||||
"openrouter",
|
||||
"aihubmix",
|
||||
"deepseek",
|
||||
"siliconflow",
|
||||
"sglang",
|
||||
"gateway",
|
||||
"edgeone",
|
||||
"doubao",
|
||||
"modelscope",
|
||||
"glm",
|
||||
"qwen",
|
||||
"qiniu",
|
||||
"kimi",
|
||||
"minimax",
|
||||
"novita",
|
||||
"mimo",
|
||||
"atlascloud",
|
||||
],
|
||||
providerInfo: {
|
||||
openai: {
|
||||
label: "OpenAI",
|
||||
defaultBaseUrl: "https://api.openai.com/v1",
|
||||
apiKeyUrl: "https://platform.openai.com/api-keys",
|
||||
modelList: "openai",
|
||||
},
|
||||
anthropic: {
|
||||
label: "Anthropic",
|
||||
defaultBaseUrl: "https://api.anthropic.com/v1",
|
||||
apiKeyUrl: "https://platform.claude.com/settings/keys",
|
||||
modelList: "anthropic",
|
||||
},
|
||||
google: {
|
||||
label: "Google",
|
||||
defaultBaseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||
apiKeyUrl: "https://aistudio.google.com/apikey",
|
||||
modelList: "google",
|
||||
},
|
||||
vertexai: { label: "Google Vertex AI" },
|
||||
azure: {
|
||||
label: "Azure OpenAI",
|
||||
defaultBaseUrl: "https://your-resource.openai.azure.com/openai",
|
||||
},
|
||||
bedrock: { label: "Amazon Bedrock" },
|
||||
ollama: {
|
||||
label: "Ollama",
|
||||
defaultBaseUrl: "https://ollama.com/api",
|
||||
apiKeyUrl: "https://ollama.com/settings/keys",
|
||||
modelList: "ollama",
|
||||
},
|
||||
openrouter: {
|
||||
label: "OpenRouter",
|
||||
defaultBaseUrl: "https://openrouter.ai/api/v1",
|
||||
apiKeyUrl: "https://openrouter.ai/keys",
|
||||
modelList: "openrouter",
|
||||
},
|
||||
aihubmix: {
|
||||
label: "AIHubMix",
|
||||
defaultBaseUrl: "https://aihubmix.com/v1",
|
||||
apiKeyUrl: "https://aihubmix.com/token",
|
||||
modelList: "aihubmix",
|
||||
},
|
||||
deepseek: {
|
||||
label: "DeepSeek",
|
||||
defaultBaseUrl: "https://api.deepseek.com/v1",
|
||||
apiKeyUrl: "https://platform.deepseek.com/api_keys",
|
||||
modelList: "openai",
|
||||
},
|
||||
siliconflow: {
|
||||
label: "SiliconFlow",
|
||||
defaultBaseUrl: "https://api.siliconflow.cn/v1",
|
||||
apiKeyUrl: "https://cloud.siliconflow.cn/account/ak",
|
||||
modelList: "openai",
|
||||
},
|
||||
sglang: {
|
||||
label: "SGLang",
|
||||
defaultBaseUrl: "http://127.0.0.1:8000/v1",
|
||||
modelList: "openai",
|
||||
},
|
||||
gateway: {
|
||||
label: "AI Gateway",
|
||||
defaultBaseUrl: "https://ai-gateway.vercel.sh/v1/ai",
|
||||
apiKeyUrl: "https://vercel.com/ai-gateway",
|
||||
modelList: "gateway",
|
||||
},
|
||||
edgeone: { label: "EdgeOne Pages" },
|
||||
doubao: {
|
||||
label: "Doubao (ByteDance)",
|
||||
defaultBaseUrl: "https://ark.cn-beijing.volces.com/api/v3",
|
||||
apiKeyUrl:
|
||||
"https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey",
|
||||
},
|
||||
modelscope: {
|
||||
label: "ModelScope",
|
||||
defaultBaseUrl: "https://api-inference.modelscope.cn/v1",
|
||||
apiKeyUrl: "https://modelscope.cn/my/myaccesstoken",
|
||||
modelList: "openai",
|
||||
},
|
||||
glm: {
|
||||
label: "GLM (Zhipu)",
|
||||
defaultBaseUrl: "https://open.bigmodel.cn/api/paas/v4",
|
||||
apiKeyUrl: "https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys",
|
||||
modelList: "openai",
|
||||
},
|
||||
qwen: {
|
||||
label: "Qwen (Alibaba)",
|
||||
defaultBaseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
apiKeyUrl: "https://bailian.console.aliyun.com/?tab=model#/api-key",
|
||||
modelList: "openai",
|
||||
},
|
||||
qiniu: {
|
||||
label: "Qiniu",
|
||||
defaultBaseUrl: "https://api.qnaigc.com/v1",
|
||||
apiKeyUrl: "https://www.qiniu.com/ai/models",
|
||||
modelList: "openai",
|
||||
},
|
||||
kimi: {
|
||||
label: "Kimi (Moonshot)",
|
||||
defaultBaseUrl: "https://api.moonshot.cn/v1",
|
||||
apiKeyUrl: "https://platform.moonshot.cn/console/api-keys",
|
||||
modelList: "openai",
|
||||
},
|
||||
minimax: {
|
||||
label: "MiniMax",
|
||||
defaultBaseUrl: "https://api.minimaxi.com/anthropic",
|
||||
apiKeyUrl:
|
||||
"https://platform.minimaxi.com/user-center/basic-information/interface-key",
|
||||
},
|
||||
novita: {
|
||||
label: "Novita AI",
|
||||
defaultBaseUrl: "https://api.novita.ai/openai",
|
||||
apiKeyUrl: "https://novita.ai/dashboard/key",
|
||||
modelList: "openai",
|
||||
},
|
||||
mimo: {
|
||||
label: "MiMo (Xiaomi)",
|
||||
defaultBaseUrl: "https://api.xiaomimimo.com/v1",
|
||||
apiKeyUrl: "https://platform.xiaomimimo.com/#/console/api-keys",
|
||||
modelList: "openai",
|
||||
},
|
||||
atlascloud: {
|
||||
label: "Atlas Cloud",
|
||||
defaultBaseUrl: "https://api.atlascloud.ai/v1",
|
||||
apiKeyUrl: "https://www.atlascloud.ai/console/api-keys",
|
||||
modelList: "openai",
|
||||
},
|
||||
} as Record<
|
||||
string,
|
||||
{
|
||||
label: string
|
||||
defaultBaseUrl?: string
|
||||
apiKeyUrl?: string
|
||||
modelList?: string
|
||||
}
|
||||
>,
|
||||
fixedCredProviders: ["bedrock", "vertexai", "ollama"],
|
||||
providerLogoMap: {
|
||||
openai: "openai",
|
||||
anthropic: "anthropic",
|
||||
google: "google",
|
||||
azure: "azure",
|
||||
bedrock: "amazon-bedrock",
|
||||
openrouter: "openrouter",
|
||||
aihubmix: "aihubmix",
|
||||
deepseek: "deepseek",
|
||||
siliconflow: "siliconflow",
|
||||
sglang: "openai",
|
||||
gateway: "vercel",
|
||||
edgeone: "tencent-cloud",
|
||||
vertexai: "google",
|
||||
doubao: "bytedance",
|
||||
modelscope: "modelscope",
|
||||
minimax: "minimax",
|
||||
novita: "novita",
|
||||
mimo: "xiaomi",
|
||||
atlascloud: "openai",
|
||||
} as Record<string, string>,
|
||||
ownPaths: [
|
||||
"google",
|
||||
"vertexai",
|
||||
"azure",
|
||||
"bedrock",
|
||||
"gateway",
|
||||
"minimax",
|
||||
"edgeone",
|
||||
],
|
||||
singleSystemProviders: [
|
||||
"minimax",
|
||||
"glm",
|
||||
"qwen",
|
||||
"kimi",
|
||||
"qiniu",
|
||||
"novita",
|
||||
"mimo",
|
||||
],
|
||||
providerEnvVars: {
|
||||
bedrock: null,
|
||||
openai: "OPENAI_API_KEY",
|
||||
anthropic: "ANTHROPIC_API_KEY",
|
||||
google: "GOOGLE_GENERATIVE_AI_API_KEY",
|
||||
vertexai: "GOOGLE_VERTEX_API_KEY",
|
||||
azure: "AZURE_API_KEY",
|
||||
ollama: null,
|
||||
openrouter: "OPENROUTER_API_KEY",
|
||||
aihubmix: "AIHUBMIX_API_KEY",
|
||||
deepseek: "DEEPSEEK_API_KEY",
|
||||
siliconflow: "SILICONFLOW_API_KEY",
|
||||
sglang: "SGLANG_API_KEY",
|
||||
gateway: "AI_GATEWAY_API_KEY",
|
||||
edgeone: null,
|
||||
doubao: "DOUBAO_API_KEY",
|
||||
modelscope: "MODELSCOPE_API_KEY",
|
||||
glm: "GLM_API_KEY",
|
||||
qwen: "QWEN_API_KEY",
|
||||
qiniu: "QINIU_API_KEY",
|
||||
kimi: "KIMI_API_KEY",
|
||||
minimax: "MINIMAX_API_KEY",
|
||||
novita: "NOVITA_API_KEY",
|
||||
mimo: "MIMO_API_KEY",
|
||||
atlascloud: "ATLASCLOUD_API_KEY",
|
||||
} as Record<string, string | null>,
|
||||
sdkKnowsEndpoint: [
|
||||
"openai",
|
||||
"google",
|
||||
"azure",
|
||||
"openrouter",
|
||||
"gateway",
|
||||
"deepseek",
|
||||
],
|
||||
providerGroups: [
|
||||
{
|
||||
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"] },
|
||||
],
|
||||
popularProviders: [
|
||||
"openai",
|
||||
"anthropic",
|
||||
"google",
|
||||
"deepseek",
|
||||
"openrouter",
|
||||
"qwen",
|
||||
],
|
||||
providerNoteKeys: {
|
||||
bedrock: "noteBedrock",
|
||||
vertexai: "noteVertex",
|
||||
azure: "noteAzure",
|
||||
edgeone: "noteEdgeone",
|
||||
ollama: "noteOllama",
|
||||
sglang: "noteSglang",
|
||||
} as Record<string, string>,
|
||||
baseUrlHintKeys: {
|
||||
minimax: "minimaxBaseUrlHint",
|
||||
mimo: "mimoBaseUrlHint",
|
||||
} as Record<string, string>,
|
||||
optionsOpen: ["azure", "ollama", "sglang", "minimax", "mimo"],
|
||||
noKeyNeeded: ["ollama", "openrouter", "aihubmix"],
|
||||
}
|
||||
|
||||
const sorted = (list: Iterable<string>) => [...list].sort()
|
||||
|
||||
describe("the provider record derives the old tables", () => {
|
||||
it("names the same providers", () => {
|
||||
expect(sorted(PROVIDER_NAMES)).toEqual(sorted(fixture.providerNames))
|
||||
expect(PROVIDER_INFO).toBe(PROVIDERS)
|
||||
})
|
||||
|
||||
it("PROVIDER_INFO: label, defaultBaseUrl, apiKeyUrl and modelList", () => {
|
||||
for (const p of PROVIDER_NAMES) {
|
||||
const { label, defaultBaseUrl, apiKeyUrl, modelList } = PROVIDERS[p]
|
||||
expect([
|
||||
p,
|
||||
{ label, defaultBaseUrl, apiKeyUrl, modelList },
|
||||
]).toEqual([p, { ...fixture.providerInfo[p] }])
|
||||
}
|
||||
})
|
||||
|
||||
it("FIXED_CRED_PROVIDERS", () => {
|
||||
expect(FIXED_CRED_PROVIDERS).toEqual(fixture.fixedCredProviders)
|
||||
})
|
||||
|
||||
it("PROVIDER_LOGO_MAP, as its reader resolves it", () => {
|
||||
for (const p of PROVIDER_NAMES) {
|
||||
expect([p, PROVIDER_LOGO_MAP[p] || p]).toEqual([
|
||||
p,
|
||||
fixture.providerLogoMap[p] || p,
|
||||
])
|
||||
expect([p, PROVIDERS[p].logo ?? p]).toEqual([
|
||||
p,
|
||||
fixture.providerLogoMap[p] || p,
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
it("chatRequestUrl's ownPaths", () => {
|
||||
const own = PROVIDER_NAMES.filter(
|
||||
(p) => chatRequestUrl(p, "https://x.example/v1") === null,
|
||||
)
|
||||
expect(sorted(own)).toEqual(sorted(fixture.ownPaths))
|
||||
})
|
||||
|
||||
it("SINGLE_SYSTEM_PROVIDERS", () => {
|
||||
expect(sorted(SINGLE_SYSTEM_PROVIDERS)).toEqual(
|
||||
sorted(fixture.singleSystemProviders),
|
||||
)
|
||||
})
|
||||
|
||||
it("PROVIDER_ENV_VARS: the key variable each provider must have", () => {
|
||||
for (const p of PROVIDER_NAMES) {
|
||||
const { credential, envPrefix, keyEnv } = PROVIDERS[p]
|
||||
const required =
|
||||
credential === "apiKey" || credential === "vertexKey"
|
||||
const derived = required ? (keyEnv ?? `${envPrefix}_API_KEY`) : null
|
||||
expect([p, derived]).toEqual([p, fixture.providerEnvVars[p]])
|
||||
}
|
||||
})
|
||||
|
||||
it("SDK_KNOWS_ENDPOINT", () => {
|
||||
const derived = PROVIDER_NAMES.filter(
|
||||
(p) => PROVIDERS[p].sdkKnowsEndpoint,
|
||||
)
|
||||
expect(sorted(derived)).toEqual(sorted(fixture.sdkKnowsEndpoint))
|
||||
})
|
||||
|
||||
it("PROVIDER_GROUPS, in the picker's order", () => {
|
||||
expect(PROVIDER_GROUPS).toEqual(fixture.providerGroups)
|
||||
})
|
||||
|
||||
it("POPULAR_PROVIDERS", () => {
|
||||
expect(POPULAR_PROVIDERS).toEqual(fixture.popularProviders)
|
||||
})
|
||||
|
||||
it("OPTIONS_OPEN", () => {
|
||||
expect(sorted(OPTIONS_OPEN)).toEqual(sorted(fixture.optionsOpen))
|
||||
})
|
||||
|
||||
it("NO_KEY_NEEDED", () => {
|
||||
const derived = PROVIDER_NAMES.filter(
|
||||
(p) => PROVIDERS[p].listNeedsNoKey,
|
||||
)
|
||||
expect(sorted(derived)).toEqual(sorted(fixture.noKeyNeeded))
|
||||
})
|
||||
|
||||
it("the picker notes and base URL hints, word for word in every language", () => {
|
||||
const dicts: Record<string, Record<string, string>> = {
|
||||
en: enDict.modelConfig,
|
||||
zh: zhDict.modelConfig,
|
||||
ja: jaDict.modelConfig,
|
||||
"zh-Hant": zhHantDict.modelConfig,
|
||||
}
|
||||
const noted = PROVIDER_NAMES.filter((p) => PROVIDERS[p].note)
|
||||
expect(sorted(noted)).toEqual(
|
||||
sorted(Object.keys(fixture.providerNoteKeys)),
|
||||
)
|
||||
for (const p of noted) {
|
||||
for (const [lang, dict] of Object.entries(dicts)) {
|
||||
expect([p, lang, PROVIDERS[p].note?.[lang as never]]).toEqual([
|
||||
p,
|
||||
lang,
|
||||
dict[fixture.providerNoteKeys[p]],
|
||||
])
|
||||
}
|
||||
}
|
||||
const hinted = PROVIDER_NAMES.filter((p) => PROVIDERS[p].baseUrlHint)
|
||||
expect(sorted(hinted)).toEqual(
|
||||
sorted(Object.keys(fixture.baseUrlHintKeys)),
|
||||
)
|
||||
for (const p of hinted) {
|
||||
for (const [lang, dict] of Object.entries(dicts)) {
|
||||
expect([
|
||||
p,
|
||||
lang,
|
||||
PROVIDERS[p].baseUrlHint?.[lang as never],
|
||||
]).toEqual([p, lang, dict[fixture.baseUrlHintKeys[p]]])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("the model lists of 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]).toEqual([p, SUGGESTED_MODELS[p]])
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("the provider record's own rules", () => {
|
||||
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)
|
||||
expect(sorted(detected)).toEqual(
|
||||
sorted(
|
||||
Object.entries(fixture.providerEnvVars)
|
||||
.filter(([, v]) => v)
|
||||
.map(([p]) => p),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
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"])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user