mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-02 01:20:23 +08:00
Compare commits
15 Commits
fix/ai-mod
...
feat-admin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cc64bbe5f | ||
|
|
ee4613c388 | ||
|
|
f96a0c5a18 | ||
|
|
3764b9b620 | ||
|
|
0753518086 | ||
|
|
d208dc70a4 | ||
|
|
b0a96c23a3 | ||
|
|
8ca628a872 | ||
|
|
a90a87355b | ||
|
|
e4a14c3628 | ||
|
|
9feab38a0b | ||
|
|
c09b454105 | ||
|
|
9d958ae9c9 | ||
|
|
d748e94262 | ||
|
|
e2e499e5c7 |
4
.github/workflows/auto-format.yml
vendored
4
.github/workflows/auto-format.yml
vendored
@@ -23,9 +23,7 @@ jobs:
|
|||||||
node-version: '24'
|
node-version: '24'
|
||||||
|
|
||||||
- name: Run Biome format
|
- name: Run Biome format
|
||||||
# Pin to the version in package.json so CI matches local/pre-commit
|
run: npx @biomejs/biome@latest check --write --no-errors-on-unmatched .
|
||||||
# (npx @latest drifts — e.g. 2.5.0 broke this job on unrelated PRs).
|
|
||||||
run: npx @biomejs/biome@2.4.13 check --write --no-errors-on-unmatched .
|
|
||||||
|
|
||||||
- name: Check for changes
|
- name: Check for changes
|
||||||
id: changes
|
id: changes
|
||||||
|
|||||||
@@ -211,7 +211,6 @@ See the [Next.js deployment documentation](https://nextjs.org/docs/app/building-
|
|||||||
- Azure OpenAI
|
- Azure OpenAI
|
||||||
- Ollama
|
- Ollama
|
||||||
- OpenRouter
|
- OpenRouter
|
||||||
- AIHubMix
|
|
||||||
- DeepSeek
|
- DeepSeek
|
||||||
- SiliconFlow
|
- SiliconFlow
|
||||||
- ModelScope
|
- ModelScope
|
||||||
@@ -225,7 +224,7 @@ All providers except AWS Bedrock and OpenRouter support custom endpoints.
|
|||||||
|
|
||||||
### Server-Side Multi-Model Configuration
|
### Server-Side Multi-Model Configuration
|
||||||
|
|
||||||
Administrators can configure multiple server-side models that are available to all users without requiring personal API keys. Configure via `AI_MODELS_CONFIG` environment variable (JSON string) or `ai-models.json` file. For a single-provider quick setup, list comma-separated model IDs in `AI_MODEL`.
|
Administrators can configure multiple server-side models that are available to all users without requiring personal API keys. Configure via `AI_MODELS_CONFIG` environment variable (JSON string) or `ai-models.json` file.
|
||||||
|
|
||||||
### Admin Panel
|
### Admin Panel
|
||||||
|
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import { NextResponse } from "next/server"
|
|
||||||
import {
|
|
||||||
AIHUBMIX_MODELS_ENDPOINT,
|
|
||||||
extractAihubmixModelIds,
|
|
||||||
} from "@/lib/aihubmix-models"
|
|
||||||
import { SUGGESTED_MODELS } from "@/lib/types/model-config"
|
|
||||||
|
|
||||||
const SUCCESS_CACHE_CONTROL =
|
|
||||||
"public, max-age=300, s-maxage=3600, stale-while-revalidate=86400"
|
|
||||||
|
|
||||||
function fallbackResponse() {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
models: SUGGESTED_MODELS.aihubmix || [],
|
|
||||||
source: "fallback",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Cache-Control": "no-store",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(AIHUBMIX_MODELS_ENDPOINT, {
|
|
||||||
next: { revalidate: 3600 },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.warn(
|
|
||||||
`[aihubmix-models] Failed to fetch models: ${response.status}`,
|
|
||||||
)
|
|
||||||
return fallbackResponse()
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = await response.json()
|
|
||||||
const models = extractAihubmixModelIds(payload)
|
|
||||||
|
|
||||||
if (models.length === 0) {
|
|
||||||
console.warn("[aihubmix-models] Model list response was empty")
|
|
||||||
return fallbackResponse()
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
models,
|
|
||||||
source: "aihubmix",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Cache-Control": SUCCESS_CACHE_CONTROL,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("[aihubmix-models] Failed to load models:", error)
|
|
||||||
return fallbackResponse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,16 +5,11 @@ import { createGateway } from "@ai-sdk/gateway"
|
|||||||
import { createGoogleGenerativeAI } from "@ai-sdk/google"
|
import { createGoogleGenerativeAI } from "@ai-sdk/google"
|
||||||
import { createVertex } from "@ai-sdk/google-vertex"
|
import { createVertex } from "@ai-sdk/google-vertex"
|
||||||
import { createOpenAI } from "@ai-sdk/openai"
|
import { createOpenAI } from "@ai-sdk/openai"
|
||||||
import { createAihubmix } from "@aihubmix/ai-sdk-provider"
|
|
||||||
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
||||||
import { generateText } from "ai"
|
import { generateText } from "ai"
|
||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import { createOllama } from "ollama-ai-provider-v2"
|
import { createOllama } from "ollama-ai-provider-v2"
|
||||||
import {
|
import { normalizeMiniMaxBaseURL } from "@/lib/ai-providers"
|
||||||
AIHUBMIX_APP_CODE,
|
|
||||||
isAihubmixStandardBaseURL,
|
|
||||||
normalizeMiniMaxBaseURL,
|
|
||||||
} from "@/lib/ai-providers"
|
|
||||||
import { allowPrivateUrls, isPrivateUrl } from "@/lib/ssrf-protection"
|
import { allowPrivateUrls, isPrivateUrl } from "@/lib/ssrf-protection"
|
||||||
import { PROVIDER_INFO, type ProviderName } from "@/lib/types/model-config"
|
import { PROVIDER_INFO, type ProviderName } from "@/lib/types/model-config"
|
||||||
|
|
||||||
@@ -158,28 +153,6 @@ export async function POST(req: Request) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case "aihubmix": {
|
|
||||||
const defaultBaseURL = PROVIDER_INFO.aihubmix.defaultBaseUrl
|
|
||||||
|
|
||||||
if (
|
|
||||||
isAihubmixStandardBaseURL(baseUrl) ||
|
|
||||||
baseUrl === defaultBaseURL
|
|
||||||
) {
|
|
||||||
const aihubmix = createAihubmix({
|
|
||||||
apiKey,
|
|
||||||
appCode: AIHUBMIX_APP_CODE,
|
|
||||||
})
|
|
||||||
model = aihubmix(modelId)
|
|
||||||
} else {
|
|
||||||
const aihubmixCompatible = createOpenAI({
|
|
||||||
apiKey,
|
|
||||||
baseURL: baseUrl,
|
|
||||||
})
|
|
||||||
model = aihubmixCompatible.chat(modelId)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "deepseek": {
|
case "deepseek": {
|
||||||
if (baseUrl || apiKey) {
|
if (baseUrl || apiKey) {
|
||||||
const ds = createDeepSeek({
|
const ds = createDeepSeek({
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ import {
|
|||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
import { useDictionary } from "@/hooks/use-dictionary"
|
import { useDictionary } from "@/hooks/use-dictionary"
|
||||||
import type { UseModelConfigReturn } from "@/hooks/use-model-config"
|
import type { UseModelConfigReturn } from "@/hooks/use-model-config"
|
||||||
import { getApiEndpoint } from "@/lib/base-path"
|
|
||||||
import { formatMessage } from "@/lib/i18n/utils"
|
import { formatMessage } from "@/lib/i18n/utils"
|
||||||
import type { ProviderConfig, ProviderName } from "@/lib/types/model-config"
|
import type { ProviderConfig, ProviderName } from "@/lib/types/model-config"
|
||||||
import { PROVIDER_INFO, SUGGESTED_MODELS } from "@/lib/types/model-config"
|
import { PROVIDER_INFO, SUGGESTED_MODELS } from "@/lib/types/model-config"
|
||||||
@@ -133,14 +132,6 @@ export function ModelConfigDialog({
|
|||||||
modelId: string
|
modelId: string
|
||||||
message: string
|
message: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const [dynamicSuggestedModels, setDynamicSuggestedModels] = useState<
|
|
||||||
Partial<Record<ProviderName, string[]>>
|
|
||||||
>({})
|
|
||||||
const [loadedSuggestedProviders, setLoadedSuggestedProviders] = useState<
|
|
||||||
Partial<Record<ProviderName, boolean>>
|
|
||||||
>({})
|
|
||||||
const [loadingSuggestedProvider, setLoadingSuggestedProvider] =
|
|
||||||
useState<ProviderName | null>(null)
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
config,
|
config,
|
||||||
@@ -166,68 +157,10 @@ export function ModelConfigDialog({
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
!open ||
|
|
||||||
selectedProvider?.provider !== "aihubmix" ||
|
|
||||||
loadedSuggestedProviders.aihubmix
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let cancelled = false
|
|
||||||
setLoadingSuggestedProvider("aihubmix")
|
|
||||||
|
|
||||||
fetch(getApiEndpoint("/api/aihubmix-models"))
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to load models: ${response.status}`)
|
|
||||||
}
|
|
||||||
return response.json()
|
|
||||||
})
|
|
||||||
.then((data: { models?: unknown }) => {
|
|
||||||
if (cancelled || !Array.isArray(data.models)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const models = data.models.filter(
|
|
||||||
(model): model is string => typeof model === "string",
|
|
||||||
)
|
|
||||||
if (models.length > 0) {
|
|
||||||
setDynamicSuggestedModels((current) => ({
|
|
||||||
...current,
|
|
||||||
aihubmix: models,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.warn("Failed to load AIHubMix models:", error)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (cancelled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadedSuggestedProviders((current) => ({
|
|
||||||
...current,
|
|
||||||
aihubmix: true,
|
|
||||||
}))
|
|
||||||
setLoadingSuggestedProvider(null)
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
}
|
|
||||||
}, [open, selectedProvider?.provider, loadedSuggestedProviders.aihubmix])
|
|
||||||
|
|
||||||
// Get suggested models for current provider
|
// Get suggested models for current provider
|
||||||
const suggestedModels = selectedProvider
|
const suggestedModels = selectedProvider
|
||||||
? dynamicSuggestedModels[selectedProvider.provider] ||
|
? SUGGESTED_MODELS[selectedProvider.provider] || []
|
||||||
SUGGESTED_MODELS[selectedProvider.provider] ||
|
|
||||||
[]
|
|
||||||
: []
|
: []
|
||||||
const isLoadingSuggestedModels =
|
|
||||||
selectedProvider?.provider === loadingSuggestedProvider
|
|
||||||
|
|
||||||
// Filter out already-added models from suggestions
|
// Filter out already-added models from suggestions
|
||||||
const existingModelIds =
|
const existingModelIds =
|
||||||
@@ -235,11 +168,6 @@ export function ModelConfigDialog({
|
|||||||
const availableSuggestions = suggestedModels.filter(
|
const availableSuggestions = suggestedModels.filter(
|
||||||
(modelId) => !existingModelIds.includes(modelId),
|
(modelId) => !existingModelIds.includes(modelId),
|
||||||
)
|
)
|
||||||
const emptyStateSuggestions = selectedProvider
|
|
||||||
? (SUGGESTED_MODELS[selectedProvider.provider] || [])
|
|
||||||
.filter((modelId) => !existingModelIds.includes(modelId))
|
|
||||||
.slice(0, 4)
|
|
||||||
: []
|
|
||||||
|
|
||||||
// Handle adding a new provider
|
// Handle adding a new provider
|
||||||
const handleAddProvider = (providerType: ProviderName) => {
|
const handleAddProvider = (providerType: ProviderName) => {
|
||||||
@@ -845,26 +773,21 @@ export function ModelConfigDialog({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={
|
||||||
isLoadingSuggestedModels ||
|
|
||||||
availableSuggestions.length ===
|
availableSuggestions.length ===
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-28 h-8 rounded-lg hover:bg-interactive-hover">
|
<SelectTrigger className="w-28 h-8 rounded-lg hover:bg-interactive-hover">
|
||||||
{isLoadingSuggestedModels ? (
|
<span className="text-xs">
|
||||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
{availableSuggestions.length ===
|
||||||
) : (
|
0
|
||||||
<span className="text-xs">
|
? dict
|
||||||
{availableSuggestions.length ===
|
.modelConfig
|
||||||
0
|
.allAdded
|
||||||
? dict
|
: dict
|
||||||
.modelConfig
|
.modelConfig
|
||||||
.allAdded
|
.suggested}
|
||||||
: dict
|
</span>
|
||||||
.modelConfig
|
|
||||||
.suggested}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="max-h-72">
|
<SelectContent className="max-h-72">
|
||||||
{availableSuggestions.map(
|
{availableSuggestions.map(
|
||||||
@@ -893,12 +816,7 @@ export function ModelConfigDialog({
|
|||||||
0 ? (
|
0 ? (
|
||||||
<div className="p-6 text-center h-full flex flex-col items-center justify-center">
|
<div className="p-6 text-center h-full flex flex-col items-center justify-center">
|
||||||
<div className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface-2 mb-3">
|
<div className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-surface-2 mb-3">
|
||||||
<ProviderLogo
|
<Sparkles className="h-5 w-5 text-muted-foreground" />
|
||||||
provider={
|
|
||||||
selectedProvider.provider
|
|
||||||
}
|
|
||||||
className="size-5 text-muted-foreground"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{
|
{
|
||||||
@@ -906,36 +824,6 @@ export function ModelConfigDialog({
|
|||||||
.noModelsConfigured
|
.noModelsConfigured
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
{emptyStateSuggestions.length >
|
|
||||||
0 && (
|
|
||||||
<div className="mt-4 flex max-w-full flex-wrap items-center justify-center gap-2">
|
|
||||||
{emptyStateSuggestions.map(
|
|
||||||
(modelId) => (
|
|
||||||
<Button
|
|
||||||
key={
|
|
||||||
modelId
|
|
||||||
}
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="h-7 max-w-[220px] rounded-lg px-2 font-mono text-[11px]"
|
|
||||||
onClick={() =>
|
|
||||||
handleAddModel(
|
|
||||||
modelId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Plus className="h-3 w-3 shrink-0" />
|
|
||||||
<span className="truncate">
|
|
||||||
{
|
|
||||||
modelId
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-border-subtle">
|
<div className="divide-y divide-border-subtle">
|
||||||
|
|||||||
@@ -204,7 +204,6 @@ npm run dev
|
|||||||
- Azure OpenAI
|
- Azure OpenAI
|
||||||
- Ollama
|
- Ollama
|
||||||
- OpenRouter
|
- OpenRouter
|
||||||
- AIHubMix
|
|
||||||
- DeepSeek
|
- DeepSeek
|
||||||
- SiliconFlow
|
- SiliconFlow
|
||||||
- ModelScope
|
- ModelScope
|
||||||
@@ -217,7 +216,7 @@ npm run dev
|
|||||||
|
|
||||||
### 服务端多模型配置
|
### 服务端多模型配置
|
||||||
|
|
||||||
管理员可以配置多个服务端模型,让所有用户无需提供个人 API Key 即可使用。通过 `AI_MODELS_CONFIG` 环境变量(JSON 字符串)或 `ai-models.json` 文件配置。如果只需要单 provider 下的多个模型,也可以直接在 `AI_MODEL` 中用逗号分隔模型 ID。
|
管理员可以配置多个服务端模型,让所有用户无需提供个人 API Key 即可使用。通过 `AI_MODELS_CONFIG` 环境变量(JSON 字符串)或 `ai-models.json` 文件配置。
|
||||||
|
|
||||||
**模型要求**:此任务需要强大的模型能力,因为它涉及生成具有严格格式约束的长文本(draw.io XML)。推荐使用 Claude Sonnet 4.5、GPT-5.1、Gemini 3 Pro 和 DeepSeek V3.2/R1。
|
**模型要求**:此任务需要强大的模型能力,因为它涉及生成具有严格格式约束的长文本(draw.io XML)。推荐使用 Claude Sonnet 4.5、GPT-5.1、Gemini 3 Pro 和 DeepSeek V3.2/R1。
|
||||||
|
|
||||||
|
|||||||
@@ -46,21 +46,6 @@ AI_MODEL=gpt-4o
|
|||||||
OPENAI_BASE_URL=https://your-custom-endpoint/v1
|
OPENAI_BASE_URL=https://your-custom-endpoint/v1
|
||||||
```
|
```
|
||||||
|
|
||||||
### AIHubMix
|
|
||||||
|
|
||||||
AIHubMix 通过单个 API Key 聚合 Claude、GPT、Gemini、DeepSeek 等模型。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
AIHUBMIX_API_KEY=your_api_key
|
|
||||||
AI_MODEL=claude-sonnet-4-5-20250929
|
|
||||||
```
|
|
||||||
|
|
||||||
可选的自定义端点:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
AIHUBMIX_BASE_URL=https://aihubmix.com/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Anthropic
|
### Anthropic
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -315,7 +300,7 @@ QINIU_BASE_URL=https://your-custom-endpoint
|
|||||||
如果您配置了**多个** API 密钥,则必须显式设置 `AI_PROVIDER`:
|
如果您配置了**多个** API 密钥,则必须显式设置 `AI_PROVIDER`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
AI_PROVIDER=google # 或:openai, anthropic, aihubmix, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang, modelscope, minimax, glm, qwen, kimi, qiniu
|
AI_PROVIDER=google # 或:openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang, modelscope, minimax, glm, qwen, kimi, qiniu
|
||||||
```
|
```
|
||||||
|
|
||||||
## 服务端多模型配置
|
## 服务端多模型配置
|
||||||
@@ -336,17 +321,6 @@ AI_MODELS_CONFIG='{"providers":[{"name":"OpenAI","provider":"openai","models":["
|
|||||||
|
|
||||||
在项目根目录创建 `ai-models.json` 文件(或通过 `AI_MODELS_CONFIG_PATH` 指定路径)。
|
在项目根目录创建 `ai-models.json` 文件(或通过 `AI_MODELS_CONFIG_PATH` 指定路径)。
|
||||||
|
|
||||||
**方式三:`AI_MODEL` 用逗号分隔**(单 provider 的快速配置)
|
|
||||||
|
|
||||||
如果只需要暴露同一 provider 下的多个模型,可以直接在 `AI_MODEL` 里用逗号分隔。第一个模型会作为默认值。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
AI_PROVIDER=doubao
|
|
||||||
AI_MODEL=doubao-seed-1-8-251215,doubao-seed-1-6-flash,doubao-seed-1-6-pro
|
|
||||||
```
|
|
||||||
|
|
||||||
这是等价 `ai-models.json` 的简写形式。如果需要配置多个 provider,或自定义 `apiKeyEnv` / `baseUrlEnv`,请使用方式一或方式二。
|
|
||||||
|
|
||||||
### 配置示例
|
### 配置示例
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -61,21 +61,6 @@ Optional custom endpoint (for OpenAI-compatible services):
|
|||||||
OPENAI_BASE_URL=https://your-custom-endpoint/v1
|
OPENAI_BASE_URL=https://your-custom-endpoint/v1
|
||||||
```
|
```
|
||||||
|
|
||||||
### AIHubMix
|
|
||||||
|
|
||||||
AIHubMix provides access to Claude, GPT, Gemini, DeepSeek, and other models through a single API key.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
AIHUBMIX_API_KEY=your_api_key
|
|
||||||
AI_MODEL=claude-sonnet-4-5-20250929
|
|
||||||
```
|
|
||||||
|
|
||||||
Optional custom endpoint:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
AIHUBMIX_BASE_URL=https://aihubmix.com/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Anthropic
|
### Anthropic
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -330,7 +315,7 @@ If you only configure **one** provider's API key, the system will automatically
|
|||||||
If you configure **multiple** API keys, you must explicitly set `AI_PROVIDER`:
|
If you configure **multiple** API keys, you must explicitly set `AI_PROVIDER`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
AI_PROVIDER=google # or: openai, anthropic, aihubmix, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang, modelscope, minimax, glm, qwen, kimi, qiniu
|
AI_PROVIDER=google # or: openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang, modelscope, minimax, glm, qwen, kimi, qiniu
|
||||||
```
|
```
|
||||||
|
|
||||||
## Server-Side Multi-Model Configuration
|
## Server-Side Multi-Model Configuration
|
||||||
@@ -351,17 +336,6 @@ AI_MODELS_CONFIG='{"providers":[{"name":"OpenAI","provider":"openai","models":["
|
|||||||
|
|
||||||
Create an `ai-models.json` file in the project root (or set `AI_MODELS_CONFIG_PATH` to a custom location).
|
Create an `ai-models.json` file in the project root (or set `AI_MODELS_CONFIG_PATH` to a custom location).
|
||||||
|
|
||||||
**Option 3: Comma-separated `AI_MODEL`** (quick setup, single provider)
|
|
||||||
|
|
||||||
If you only need multiple models from one provider, list them in `AI_MODEL` separated by commas. The first model is treated as the default.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
AI_PROVIDER=doubao
|
|
||||||
AI_MODEL=doubao-seed-1-8-251215,doubao-seed-1-6-flash,doubao-seed-1-6-pro
|
|
||||||
```
|
|
||||||
|
|
||||||
This is shorthand for the equivalent `ai-models.json`. For multiple providers or custom `apiKeyEnv` / `baseUrlEnv`, use Option 1 or 2 instead.
|
|
||||||
|
|
||||||
### Example Configuration
|
### Example Configuration
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -203,7 +203,6 @@ Next.jsアプリをデプロイする最も簡単な方法は、Next.jsの作成
|
|||||||
- Azure OpenAI
|
- Azure OpenAI
|
||||||
- Ollama
|
- Ollama
|
||||||
- OpenRouter
|
- OpenRouter
|
||||||
- AIHubMix
|
|
||||||
- DeepSeek
|
- DeepSeek
|
||||||
- SiliconFlow
|
- SiliconFlow
|
||||||
- ModelScope
|
- ModelScope
|
||||||
@@ -216,7 +215,7 @@ AWS BedrockとOpenRouter以外のすべてのプロバイダーはカスタム
|
|||||||
|
|
||||||
### サーバーサイドマルチモデル設定
|
### サーバーサイドマルチモデル設定
|
||||||
|
|
||||||
管理者は、ユーザーが個人のAPIキーを提供することなく利用できる複数のサーバーサイドモデルを設定できます。`AI_MODELS_CONFIG` 環境変数(JSON文字列)または `ai-models.json` ファイルで設定します。同一プロバイダー内の複数モデルだけが必要な場合は、`AI_MODEL` にカンマ区切りでモデルIDを列挙する簡易設定も使えます。
|
管理者は、ユーザーが個人のAPIキーを提供することなく利用できる複数のサーバーサイドモデルを設定できます。`AI_MODELS_CONFIG` 環境変数(JSON文字列)または `ai-models.json` ファイルで設定します。
|
||||||
|
|
||||||
**モデル要件**:このタスクは厳密なフォーマット制約(draw.io XML)を持つ長文テキスト生成を伴うため、強力なモデル機能が必要です。Claude Sonnet 4.5、GPT-5.1、Gemini 3 Pro、DeepSeek V3.2/R1を推奨します。
|
**モデル要件**:このタスクは厳密なフォーマット制約(draw.io XML)を持つ長文テキスト生成を伴うため、強力なモデル機能が必要です。Claude Sonnet 4.5、GPT-5.1、Gemini 3 Pro、DeepSeek V3.2/R1を推奨します。
|
||||||
|
|
||||||
|
|||||||
@@ -46,21 +46,6 @@ AI_MODEL=gpt-4o
|
|||||||
OPENAI_BASE_URL=https://your-custom-endpoint/v1
|
OPENAI_BASE_URL=https://your-custom-endpoint/v1
|
||||||
```
|
```
|
||||||
|
|
||||||
### AIHubMix
|
|
||||||
|
|
||||||
AIHubMix は、単一の API キーで Claude、GPT、Gemini、DeepSeek などのモデルへのアクセスを提供します。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
AIHUBMIX_API_KEY=your_api_key
|
|
||||||
AI_MODEL=claude-sonnet-4-5-20250929
|
|
||||||
```
|
|
||||||
|
|
||||||
任意のカスタムエンドポイント:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
AIHUBMIX_BASE_URL=https://aihubmix.com/v1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Anthropic
|
### Anthropic
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -315,7 +300,7 @@ QINIU_BASE_URL=https://your-custom-endpoint
|
|||||||
**複数**の API キーを設定する場合は、`AI_PROVIDER` を明示的に設定する必要があります:
|
**複数**の API キーを設定する場合は、`AI_PROVIDER` を明示的に設定する必要があります:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
AI_PROVIDER=google # または: openai, anthropic, aihubmix, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang, modelscope, minimax, glm, qwen, kimi, qiniu
|
AI_PROVIDER=google # または: openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang, modelscope, minimax, glm, qwen, kimi, qiniu
|
||||||
```
|
```
|
||||||
|
|
||||||
## サーバーサイドマルチモデル設定
|
## サーバーサイドマルチモデル設定
|
||||||
@@ -336,17 +321,6 @@ AI_MODELS_CONFIG='{"providers":[{"name":"OpenAI","provider":"openai","models":["
|
|||||||
|
|
||||||
プロジェクトルートに `ai-models.json` ファイルを作成します(または `AI_MODELS_CONFIG_PATH` でパスを指定)。
|
プロジェクトルートに `ai-models.json` ファイルを作成します(または `AI_MODELS_CONFIG_PATH` でパスを指定)。
|
||||||
|
|
||||||
**方法3:`AI_MODEL` をカンマ区切りで指定**(単一プロバイダーの簡易設定)
|
|
||||||
|
|
||||||
同一プロバイダー内の複数モデルだけを公開したい場合は、`AI_MODEL` にカンマ区切りで列挙できます。最初のモデルがデフォルトになります。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
AI_PROVIDER=doubao
|
|
||||||
AI_MODEL=doubao-seed-1-8-251215,doubao-seed-1-6-flash,doubao-seed-1-6-pro
|
|
||||||
```
|
|
||||||
|
|
||||||
これは等価な `ai-models.json` の簡易表記です。複数のプロバイダーや、カスタム `apiKeyEnv` / `baseUrlEnv` を使う場合は、方法1または方法2を使ってください。
|
|
||||||
|
|
||||||
### 設定例
|
### 設定例
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
10
env.example
10
env.example
@@ -1,14 +1,10 @@
|
|||||||
# AI Provider Configuration
|
# AI Provider Configuration
|
||||||
# AI_PROVIDER: Which provider to use
|
# AI_PROVIDER: Which provider to use
|
||||||
# Options: bedrock, openai, anthropic, google, vertexai, azure, ollama, openrouter, aihubmix, deepseek, siliconflow, gateway, novita
|
# Options: bedrock, openai, anthropic, google, vertexai, azure, ollama, openrouter, deepseek, siliconflow, gateway, novita
|
||||||
# Default: bedrock
|
# Default: bedrock
|
||||||
AI_PROVIDER=bedrock
|
AI_PROVIDER=bedrock
|
||||||
|
|
||||||
# AI_MODEL: The model ID for your chosen provider (REQUIRED)
|
# AI_MODEL: The model ID for your chosen provider (REQUIRED)
|
||||||
# Tip: For a single-provider quick multi-model setup, list comma-separated model IDs.
|
|
||||||
# The first one becomes the default and the rest appear in the model picker.
|
|
||||||
# For multiple providers or custom apiKeyEnv/baseUrlEnv, use AI_MODELS_CONFIG / ai-models.json instead.
|
|
||||||
# Example: AI_MODEL=doubao-seed-1-8-251215,doubao-seed-1-6-flash,doubao-seed-1-6-pro
|
|
||||||
AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
||||||
|
|
||||||
# AWS Bedrock Configuration
|
# AWS Bedrock Configuration
|
||||||
@@ -73,10 +69,6 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
|||||||
# OPENROUTER_API_KEY=sk-or-v1-...
|
# OPENROUTER_API_KEY=sk-or-v1-...
|
||||||
# OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 # Optional: Custom endpoint
|
# OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 # Optional: Custom endpoint
|
||||||
|
|
||||||
# AIHubMix Configuration
|
|
||||||
# AIHUBMIX_API_KEY=your-aihubmix-api-key
|
|
||||||
# AIHUBMIX_BASE_URL=https://aihubmix.com/v1 # Optional: Custom endpoint
|
|
||||||
|
|
||||||
# DeepSeek Configuration
|
# DeepSeek Configuration
|
||||||
# DEEPSEEK_API_KEY=sk-...
|
# DEEPSEEK_API_KEY=sk-...
|
||||||
# DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 # Optional: Custom endpoint
|
# DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 # Optional: Custom endpoint
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { createGateway, gateway } from "@ai-sdk/gateway"
|
|||||||
import { createGoogleGenerativeAI, google } from "@ai-sdk/google"
|
import { createGoogleGenerativeAI, google } from "@ai-sdk/google"
|
||||||
import { createVertex } from "@ai-sdk/google-vertex"
|
import { createVertex } from "@ai-sdk/google-vertex"
|
||||||
import { createOpenAI, openai } from "@ai-sdk/openai"
|
import { createOpenAI, openai } from "@ai-sdk/openai"
|
||||||
import { aihubmix, createAihubmix } from "@aihubmix/ai-sdk-provider"
|
|
||||||
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
|
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
|
||||||
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
import { createOpenRouter } from "@openrouter/ai-sdk-provider"
|
||||||
import { createOllama, ollama } from "ollama-ai-provider-v2"
|
import { createOllama, ollama } from "ollama-ai-provider-v2"
|
||||||
@@ -14,8 +13,6 @@ import { PROVIDER_INFO, type ProviderName } from "@/lib/types/model-config"
|
|||||||
|
|
||||||
export type { ProviderName }
|
export type { ProviderName }
|
||||||
|
|
||||||
export const AIHUBMIX_APP_CODE = "MSBS9675"
|
|
||||||
|
|
||||||
interface ModelConfig {
|
interface ModelConfig {
|
||||||
model: any
|
model: any
|
||||||
providerOptions?: any
|
providerOptions?: any
|
||||||
@@ -60,18 +57,6 @@ export function normalizeMiniMaxBaseURL(rawUrl: string): {
|
|||||||
return { baseURL, isAnthropicCompatible }
|
return { baseURL, isAnthropicCompatible }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAihubmixStandardBaseURL(
|
|
||||||
rawUrl: string | null | undefined,
|
|
||||||
): boolean {
|
|
||||||
if (!rawUrl) return true
|
|
||||||
|
|
||||||
const baseURL = rawUrl.replace(/\/+$/, "")
|
|
||||||
return (
|
|
||||||
baseURL === "https://aihubmix.com" ||
|
|
||||||
baseURL === "https://aihubmix.com/v1"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ClientOverrides {
|
export interface ClientOverrides {
|
||||||
provider?: string | null
|
provider?: string | null
|
||||||
baseUrl?: string | null
|
baseUrl?: string | null
|
||||||
@@ -101,7 +86,6 @@ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
|
|||||||
"azure",
|
"azure",
|
||||||
"bedrock",
|
"bedrock",
|
||||||
"openrouter",
|
"openrouter",
|
||||||
"aihubmix",
|
|
||||||
"deepseek",
|
"deepseek",
|
||||||
"siliconflow",
|
"siliconflow",
|
||||||
"sglang",
|
"sglang",
|
||||||
@@ -529,7 +513,6 @@ function buildProviderOptions(
|
|||||||
|
|
||||||
case "deepseek":
|
case "deepseek":
|
||||||
case "openrouter":
|
case "openrouter":
|
||||||
case "aihubmix":
|
|
||||||
case "siliconflow":
|
case "siliconflow":
|
||||||
case "sglang":
|
case "sglang":
|
||||||
case "gateway":
|
case "gateway":
|
||||||
@@ -563,7 +546,6 @@ export const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
|
|||||||
azure: "AZURE_API_KEY",
|
azure: "AZURE_API_KEY",
|
||||||
ollama: null, // No credentials needed for local Ollama
|
ollama: null, // No credentials needed for local Ollama
|
||||||
openrouter: "OPENROUTER_API_KEY",
|
openrouter: "OPENROUTER_API_KEY",
|
||||||
aihubmix: "AIHUBMIX_API_KEY",
|
|
||||||
deepseek: "DEEPSEEK_API_KEY",
|
deepseek: "DEEPSEEK_API_KEY",
|
||||||
siliconflow: "SILICONFLOW_API_KEY",
|
siliconflow: "SILICONFLOW_API_KEY",
|
||||||
sglang: "SGLANG_API_KEY",
|
sglang: "SGLANG_API_KEY",
|
||||||
@@ -680,7 +662,7 @@ function validateProviderCredentials(
|
|||||||
* Get the AI model based on environment variables
|
* Get the AI model based on environment variables
|
||||||
*
|
*
|
||||||
* Environment variables:
|
* Environment variables:
|
||||||
* - AI_PROVIDER: The provider to use (bedrock, openai, anthropic, google, azure, ollama, openrouter, aihubmix, deepseek, siliconflow, sglang, gateway, modelscope)
|
* - AI_PROVIDER: The provider to use (bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, modelscope)
|
||||||
* - AI_MODEL: The model ID/name for the selected provider
|
* - AI_MODEL: The model ID/name for the selected provider
|
||||||
*
|
*
|
||||||
* Provider-specific env vars:
|
* Provider-specific env vars:
|
||||||
@@ -692,7 +674,6 @@ function validateProviderCredentials(
|
|||||||
* - AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY: AWS Bedrock credentials
|
* - AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY: AWS Bedrock credentials
|
||||||
* - OLLAMA_BASE_URL: Ollama server URL (optional, defaults to https://ollama.com/api)
|
* - OLLAMA_BASE_URL: Ollama server URL (optional, defaults to https://ollama.com/api)
|
||||||
* - OPENROUTER_API_KEY: OpenRouter API key
|
* - OPENROUTER_API_KEY: OpenRouter API key
|
||||||
* - AIHUBMIX_API_KEY: AIHubMix API key
|
|
||||||
* - DEEPSEEK_API_KEY: DeepSeek API key
|
* - DEEPSEEK_API_KEY: DeepSeek API key
|
||||||
* - DEEPSEEK_BASE_URL: DeepSeek endpoint (optional)
|
* - DEEPSEEK_BASE_URL: DeepSeek endpoint (optional)
|
||||||
* - SILICONFLOW_API_KEY: SiliconFlow API key
|
* - SILICONFLOW_API_KEY: SiliconFlow API key
|
||||||
@@ -729,10 +710,8 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
(overrides?.provider === "vertexai" && overrides?.vertexApiKey))
|
(overrides?.provider === "vertexai" && overrides?.vertexApiKey))
|
||||||
)
|
)
|
||||||
|
|
||||||
// Use client override if provided, otherwise fall back to env vars.
|
// Use client override if provided, otherwise fall back to env vars
|
||||||
// AI_MODEL may be comma-separated (multi-model fallback); pick the first.
|
const modelId = overrides?.modelId || process.env.AI_MODEL
|
||||||
const envModel = process.env.AI_MODEL?.split(",")[0]?.trim() || undefined
|
|
||||||
const modelId = overrides?.modelId || envModel
|
|
||||||
|
|
||||||
if (!modelId) {
|
if (!modelId) {
|
||||||
if (isClientOverride) {
|
if (isClientOverride) {
|
||||||
@@ -782,7 +761,6 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
`- GOOGLE_GENERATIVE_AI_API_KEY for Google\n` +
|
`- GOOGLE_GENERATIVE_AI_API_KEY for Google\n` +
|
||||||
`- AWS_ACCESS_KEY_ID for Bedrock\n` +
|
`- AWS_ACCESS_KEY_ID for Bedrock\n` +
|
||||||
`- OPENROUTER_API_KEY for OpenRouter\n` +
|
`- OPENROUTER_API_KEY for OpenRouter\n` +
|
||||||
`- AIHUBMIX_API_KEY for AIHubMix\n` +
|
|
||||||
`- AZURE_API_KEY for Azure\n` +
|
`- AZURE_API_KEY for Azure\n` +
|
||||||
`- SILICONFLOW_API_KEY for SiliconFlow\n` +
|
`- SILICONFLOW_API_KEY for SiliconFlow\n` +
|
||||||
`- SGLANG_API_KEY for SGLang\n` +
|
`- SGLANG_API_KEY for SGLang\n` +
|
||||||
@@ -1025,42 +1003,6 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case "aihubmix": {
|
|
||||||
const apiKey = resolveApiKey(overrides, "AIHUBMIX_API_KEY")
|
|
||||||
const serverBaseUrl = resolveBaseUrlEnv(
|
|
||||||
overrides,
|
|
||||||
"AIHUBMIX_BASE_URL",
|
|
||||||
)
|
|
||||||
const baseURL = resolveBaseURL(
|
|
||||||
overrides?.apiKey,
|
|
||||||
overrides?.baseUrl,
|
|
||||||
serverBaseUrl,
|
|
||||||
PROVIDER_INFO.aihubmix.defaultBaseUrl,
|
|
||||||
)
|
|
||||||
const defaultBaseURL = PROVIDER_INFO.aihubmix.defaultBaseUrl
|
|
||||||
|
|
||||||
if (
|
|
||||||
isAihubmixStandardBaseURL(baseURL) ||
|
|
||||||
baseURL === defaultBaseURL
|
|
||||||
) {
|
|
||||||
const aihubmixProvider =
|
|
||||||
overrides?.apiKey || apiKey
|
|
||||||
? createAihubmix({
|
|
||||||
apiKey,
|
|
||||||
appCode: AIHUBMIX_APP_CODE,
|
|
||||||
})
|
|
||||||
: aihubmix
|
|
||||||
model = aihubmixProvider(modelId)
|
|
||||||
} else {
|
|
||||||
const aihubmixCompatibleProvider = createOpenAI({
|
|
||||||
apiKey,
|
|
||||||
baseURL,
|
|
||||||
})
|
|
||||||
model = aihubmixCompatibleProvider.chat(modelId)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "deepseek": {
|
case "deepseek": {
|
||||||
const apiKey = resolveApiKey(overrides, "DEEPSEEK_API_KEY")
|
const apiKey = resolveApiKey(overrides, "DEEPSEEK_API_KEY")
|
||||||
const serverBaseUrl = resolveBaseUrlEnv(
|
const serverBaseUrl = resolveBaseUrlEnv(
|
||||||
@@ -1380,7 +1322,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
overrides?.apiKey,
|
overrides?.apiKey,
|
||||||
overrides?.baseUrl,
|
overrides?.baseUrl,
|
||||||
resolveBaseUrlEnv(overrides, "KIMI_BASE_URL"),
|
resolveBaseUrlEnv(overrides, "KIMI_BASE_URL"),
|
||||||
PROVIDER_INFO.kimi?.defaultBaseUrl,
|
PROVIDER_INFO["kimi"]?.defaultBaseUrl,
|
||||||
)
|
)
|
||||||
// Use createDeepSeek to properly handle reasoning_content for Kimi
|
// Use createDeepSeek to properly handle reasoning_content for Kimi
|
||||||
// thinking models (e.g., kimi-k2.6). Kimi's API uses the same
|
// thinking models (e.g., kimi-k2.6). Kimi's API uses the same
|
||||||
@@ -1393,7 +1335,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
|||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, aihubmix, deepseek, siliconflow, sglang, gateway, edgeone, doubao, modelscope, glm, qwen, qiniu, kimi, minimax, novita`,
|
`Unknown AI provider: ${provider}. Supported providers: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, sglang, gateway, edgeone, doubao, modelscope, glm, qwen, qiniu, kimi, minimax, novita`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1492,9 +1434,7 @@ export function supportsImageInput(modelId: string): boolean {
|
|||||||
* Throws if the model doesn't support image input.
|
* Throws if the model doesn't support image input.
|
||||||
*/
|
*/
|
||||||
export function getValidationModel(): ReturnType<typeof getAIModel>["model"] {
|
export function getValidationModel(): ReturnType<typeof getAIModel>["model"] {
|
||||||
// AI_MODEL may be comma-separated (multi-model fallback); pick the first.
|
const modelId = process.env.VALIDATION_MODEL || process.env.AI_MODEL
|
||||||
const envFallback = process.env.AI_MODEL?.split(",")[0]?.trim() || undefined
|
|
||||||
const modelId = process.env.VALIDATION_MODEL || envFallback
|
|
||||||
|
|
||||||
if (!modelId) {
|
if (!modelId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
export const AIHUBMIX_MODELS_ENDPOINT = "https://aihubmix.com/api/v1/models"
|
|
||||||
|
|
||||||
const NON_CHAT_MODEL_TYPES = new Set([
|
|
||||||
"embedding",
|
|
||||||
"image_generation",
|
|
||||||
"rerank",
|
|
||||||
"transcription",
|
|
||||||
"tts",
|
|
||||||
"video",
|
|
||||||
])
|
|
||||||
|
|
||||||
type AihubmixModelListPayload = {
|
|
||||||
data?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
type AihubmixModelRecord = {
|
|
||||||
model_id?: unknown
|
|
||||||
types?: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
function getModelTypes(types: unknown): Set<string> {
|
|
||||||
if (typeof types !== "string") {
|
|
||||||
return new Set()
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Set(
|
|
||||||
types
|
|
||||||
.split(",")
|
|
||||||
.map((type) => type.trim())
|
|
||||||
.filter(Boolean),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isChatModel(record: AihubmixModelRecord): record is {
|
|
||||||
model_id: string
|
|
||||||
types: string
|
|
||||||
} {
|
|
||||||
if (typeof record.model_id !== "string" || !record.model_id.trim()) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const types = getModelTypes(record.types)
|
|
||||||
if (!types.has("llm")) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return !Array.from(NON_CHAT_MODEL_TYPES).some((type) => types.has(type))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extractAihubmixModelIds(payload: unknown): string[] {
|
|
||||||
const data = (payload as AihubmixModelListPayload)?.data
|
|
||||||
if (!Array.isArray(data)) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const seen = new Set<string>()
|
|
||||||
const modelIds: string[] = []
|
|
||||||
|
|
||||||
for (const item of data) {
|
|
||||||
if (!item || typeof item !== "object") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const record = item as AihubmixModelRecord
|
|
||||||
if (!isChatModel(record)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelId = record.model_id.trim()
|
|
||||||
if (seen.has(modelId)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
seen.add(modelId)
|
|
||||||
modelIds.push(modelId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return modelIds
|
|
||||||
}
|
|
||||||
@@ -62,53 +62,6 @@ function getConfigPath(): string {
|
|||||||
return path.join(process.cwd(), "ai-models.json")
|
return path.join(process.cwd(), "ai-models.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Synthesize a config from a comma-separated AI_MODEL value (Priority 3 fallback).
|
|
||||||
* Lets users expose multiple models without authoring AI_MODELS_CONFIG / ai-models.json.
|
|
||||||
* Triggers only when AI_MODEL contains a comma AND AI_PROVIDER is set to a known provider.
|
|
||||||
*/
|
|
||||||
function configFromCommaSeparatedAiModel(): ServerModelsConfig | null {
|
|
||||||
const aiModel = process.env.AI_MODEL
|
|
||||||
if (!aiModel || !aiModel.includes(",")) return null
|
|
||||||
|
|
||||||
const aiProvider = process.env.AI_PROVIDER
|
|
||||||
if (!aiProvider) {
|
|
||||||
console.warn(
|
|
||||||
"[server-model-config] AI_MODEL contains commas but AI_PROVIDER is not set; " +
|
|
||||||
"skipping multi-model fallback. Set AI_PROVIDER, or use AI_MODELS_CONFIG / ai-models.json.",
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (!(aiProvider in PROVIDER_INFO)) {
|
|
||||||
console.warn(
|
|
||||||
`[server-model-config] AI_PROVIDER="${aiProvider}" is not a known provider; skipping multi-model fallback.`,
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const models = Array.from(
|
|
||||||
new Set(
|
|
||||||
aiModel
|
|
||||||
.split(",")
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter((s) => s.length > 0),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if (models.length === 0) return null
|
|
||||||
|
|
||||||
const providerName = aiProvider as ProviderName
|
|
||||||
return {
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
name: PROVIDER_INFO[providerName]?.label || providerName,
|
|
||||||
provider: providerName,
|
|
||||||
models,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadEnvServerModelsConfig(): Promise<ServerModelsConfig | null> {
|
export async function loadEnvServerModelsConfig(): Promise<ServerModelsConfig | null> {
|
||||||
// Priority 1: AI_MODELS_CONFIG env var (JSON string) - for cloud deployments
|
// Priority 1: AI_MODELS_CONFIG env var (JSON string) - for cloud deployments
|
||||||
const envConfig = process.env.AI_MODELS_CONFIG
|
const envConfig = process.env.AI_MODELS_CONFIG
|
||||||
@@ -132,17 +85,15 @@ export async function loadEnvServerModelsConfig(): Promise<ServerModelsConfig |
|
|||||||
const json = JSON.parse(jsonStr)
|
const json = JSON.parse(jsonStr)
|
||||||
return ServerModelsConfigSchema.parse(json)
|
return ServerModelsConfigSchema.parse(json)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err?.code !== "ENOENT") {
|
if (err?.code === "ENOENT") {
|
||||||
console.error(
|
|
||||||
"[server-model-config] Failed to load ai-models.json:",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
console.error(
|
||||||
|
"[server-model-config] Failed to load ai-models.json:",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 3: AI_MODEL with comma-separated values + AI_PROVIDER
|
|
||||||
return configFromCommaSeparatedAiModel()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadRawServerModelsConfig(): Promise<ServerModelsConfig | null> {
|
export async function loadRawServerModelsConfig(): Promise<ServerModelsConfig | null> {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export type ProviderName =
|
|||||||
| "bedrock"
|
| "bedrock"
|
||||||
| "ollama"
|
| "ollama"
|
||||||
| "openrouter"
|
| "openrouter"
|
||||||
| "aihubmix"
|
|
||||||
| "deepseek"
|
| "deepseek"
|
||||||
| "siliconflow"
|
| "siliconflow"
|
||||||
| "sglang"
|
| "sglang"
|
||||||
@@ -103,7 +102,6 @@ export const PROVIDER_LOGO_MAP: Record<string, string> = {
|
|||||||
azure: "azure",
|
azure: "azure",
|
||||||
bedrock: "amazon-bedrock",
|
bedrock: "amazon-bedrock",
|
||||||
openrouter: "openrouter",
|
openrouter: "openrouter",
|
||||||
aihubmix: "aihubmix",
|
|
||||||
deepseek: "deepseek",
|
deepseek: "deepseek",
|
||||||
siliconflow: "siliconflow",
|
siliconflow: "siliconflow",
|
||||||
sglang: "openai", // SGLang is OpenAI-compatible
|
sglang: "openai", // SGLang is OpenAI-compatible
|
||||||
@@ -147,10 +145,6 @@ export const PROVIDER_INFO: Record<
|
|||||||
label: "OpenRouter",
|
label: "OpenRouter",
|
||||||
defaultBaseUrl: "https://openrouter.ai/api/v1",
|
defaultBaseUrl: "https://openrouter.ai/api/v1",
|
||||||
},
|
},
|
||||||
aihubmix: {
|
|
||||||
label: "AIHubMix",
|
|
||||||
defaultBaseUrl: "https://aihubmix.com/v1",
|
|
||||||
},
|
|
||||||
deepseek: {
|
deepseek: {
|
||||||
label: "DeepSeek",
|
label: "DeepSeek",
|
||||||
defaultBaseUrl: "https://api.deepseek.com/v1",
|
defaultBaseUrl: "https://api.deepseek.com/v1",
|
||||||
@@ -323,41 +317,6 @@ export const SUGGESTED_MODELS: Partial<Record<ProviderName, string[]>> = {
|
|||||||
// MiniMax
|
// MiniMax
|
||||||
"minimax/minimax-m3",
|
"minimax/minimax-m3",
|
||||||
],
|
],
|
||||||
aihubmix: [
|
|
||||||
// Fallback list. The settings UI loads the live model list from AIHubMix when available.
|
|
||||||
// Anthropic Claude
|
|
||||||
"claude-fable-5",
|
|
||||||
"claude-opus-4-8",
|
|
||||||
"claude-sonnet-4-6",
|
|
||||||
// OpenAI
|
|
||||||
"gpt-5.5",
|
|
||||||
"gpt-5.5-pro",
|
|
||||||
"gpt-5.4",
|
|
||||||
// Google Gemini
|
|
||||||
"gemini-3.5-flash",
|
|
||||||
"gemini-3.1-pro-preview",
|
|
||||||
"gemini-3-flash-preview",
|
|
||||||
// DeepSeek
|
|
||||||
"deepseek-v4-pro",
|
|
||||||
"deepseek-v4-flash",
|
|
||||||
// Qwen
|
|
||||||
"qwen3.7-max",
|
|
||||||
"qwen3-coder-next",
|
|
||||||
// Z.ai
|
|
||||||
"glm-5.1",
|
|
||||||
// Moonshot AI
|
|
||||||
"kimi-k2.6",
|
|
||||||
// MiniMax
|
|
||||||
"minimax-m3",
|
|
||||||
// xAI
|
|
||||||
"grok-4.3",
|
|
||||||
// Baidu
|
|
||||||
"ernie-5.1",
|
|
||||||
// Mistral
|
|
||||||
"mistral-large-3",
|
|
||||||
// Meta
|
|
||||||
"llama-4-maverick",
|
|
||||||
],
|
|
||||||
deepseek: [
|
deepseek: [
|
||||||
"deepseek-v4-pro",
|
"deepseek-v4-pro",
|
||||||
"deepseek-v4-flash",
|
"deepseek-v4-flash",
|
||||||
|
|||||||
280
package-lock.json
generated
280
package-lock.json
generated
@@ -18,7 +18,6 @@
|
|||||||
"@ai-sdk/google-vertex": "^4.0.16",
|
"@ai-sdk/google-vertex": "^4.0.16",
|
||||||
"@ai-sdk/openai": "^3.0.0",
|
"@ai-sdk/openai": "^3.0.0",
|
||||||
"@ai-sdk/react": "^3.0.1",
|
"@ai-sdk/react": "^3.0.1",
|
||||||
"@aihubmix/ai-sdk-provider": "^2.1.0",
|
|
||||||
"@aws-sdk/client-dynamodb": "^3.957.0",
|
"@aws-sdk/client-dynamodb": "^3.957.0",
|
||||||
"@aws-sdk/credential-providers": "^3.943.0",
|
"@aws-sdk/credential-providers": "^3.943.0",
|
||||||
"@extractus/article-extractor": "^8.0.18",
|
"@extractus/article-extractor": "^8.0.18",
|
||||||
@@ -321,51 +320,6 @@
|
|||||||
"zod": "^3.25.76 || ^4.1.8"
|
"zod": "^3.25.76 || ^4.1.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ai-sdk/openai-compatible": {
|
|
||||||
"version": "2.0.48",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-2.0.48.tgz",
|
|
||||||
"integrity": "sha512-z9MC6M4Oh/yUY/F/eszOtO8wc2nMz99XmZQKd2gWTtyIfe716xTfrKe3aYZKg20NZDtyjqPPKPSR+wqz7q1T7Q==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@ai-sdk/provider": "3.0.10",
|
|
||||||
"@ai-sdk/provider-utils": "4.0.27"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"zod": "^3.25.76 || ^4.1.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ai-sdk/openai-compatible/node_modules/@ai-sdk/provider": {
|
|
||||||
"version": "3.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.10.tgz",
|
|
||||||
"integrity": "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"json-schema": "^0.4.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ai-sdk/openai-compatible/node_modules/@ai-sdk/provider-utils": {
|
|
||||||
"version": "4.0.27",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.27.tgz",
|
|
||||||
"integrity": "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@ai-sdk/provider": "3.0.10",
|
|
||||||
"@standard-schema/spec": "^1.1.0",
|
|
||||||
"eventsource-parser": "^3.0.8"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"zod": "^3.25.76 || ^4.1.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ai-sdk/provider": {
|
"node_modules/@ai-sdk/provider": {
|
||||||
"version": "3.0.8",
|
"version": "3.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz",
|
||||||
@@ -413,26 +367,6 @@
|
|||||||
"react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1"
|
"react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aihubmix/ai-sdk-provider": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@aihubmix/ai-sdk-provider/-/ai-sdk-provider-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-AqK10PV5B4zWFBav5PRUhrWGYTHjC0s6cIbd4v9hBo6D9SqKv5o41B+dkl6IeIPrnZrMGLEE9Mn5R+VaEkhbdg==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@ai-sdk/anthropic": "^3.0.0",
|
|
||||||
"@ai-sdk/google": "^3.0.0",
|
|
||||||
"@ai-sdk/openai": "^3.0.0",
|
|
||||||
"@ai-sdk/openai-compatible": "^2.0.37",
|
|
||||||
"@ai-sdk/provider": "^3.0.0",
|
|
||||||
"@ai-sdk/provider-utils": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"zod": "^3.25.0 || ^4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||||
@@ -3408,9 +3342,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
||||||
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -3425,9 +3359,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
||||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -3442,9 +3376,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm64": {
|
"node_modules/@esbuild/android-arm64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3459,9 +3393,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-x64": {
|
"node_modules/@esbuild/android-x64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3476,9 +3410,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3493,9 +3427,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3510,9 +3444,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3527,9 +3461,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3544,9 +3478,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm": {
|
"node_modules/@esbuild/linux-arm": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
||||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -3561,9 +3495,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3578,9 +3512,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
||||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -3595,9 +3529,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
||||||
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@@ -3612,9 +3546,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
||||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
@@ -3629,9 +3563,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
||||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -3646,9 +3580,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
||||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -3663,9 +3597,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
|
||||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -3680,9 +3614,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3697,9 +3631,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-arm64": {
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3714,9 +3648,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3731,9 +3665,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-arm64": {
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3748,9 +3682,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3765,9 +3699,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openharmony-arm64": {
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3782,9 +3716,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3799,9 +3733,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
|
||||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3816,9 +3750,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
||||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -3833,9 +3767,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
||||||
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -13300,9 +13234,9 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.28.1",
|
"version": "0.28.0",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
||||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -13313,32 +13247,32 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@esbuild/aix-ppc64": "0.28.1",
|
"@esbuild/aix-ppc64": "0.28.0",
|
||||||
"@esbuild/android-arm": "0.28.1",
|
"@esbuild/android-arm": "0.28.0",
|
||||||
"@esbuild/android-arm64": "0.28.1",
|
"@esbuild/android-arm64": "0.28.0",
|
||||||
"@esbuild/android-x64": "0.28.1",
|
"@esbuild/android-x64": "0.28.0",
|
||||||
"@esbuild/darwin-arm64": "0.28.1",
|
"@esbuild/darwin-arm64": "0.28.0",
|
||||||
"@esbuild/darwin-x64": "0.28.1",
|
"@esbuild/darwin-x64": "0.28.0",
|
||||||
"@esbuild/freebsd-arm64": "0.28.1",
|
"@esbuild/freebsd-arm64": "0.28.0",
|
||||||
"@esbuild/freebsd-x64": "0.28.1",
|
"@esbuild/freebsd-x64": "0.28.0",
|
||||||
"@esbuild/linux-arm": "0.28.1",
|
"@esbuild/linux-arm": "0.28.0",
|
||||||
"@esbuild/linux-arm64": "0.28.1",
|
"@esbuild/linux-arm64": "0.28.0",
|
||||||
"@esbuild/linux-ia32": "0.28.1",
|
"@esbuild/linux-ia32": "0.28.0",
|
||||||
"@esbuild/linux-loong64": "0.28.1",
|
"@esbuild/linux-loong64": "0.28.0",
|
||||||
"@esbuild/linux-mips64el": "0.28.1",
|
"@esbuild/linux-mips64el": "0.28.0",
|
||||||
"@esbuild/linux-ppc64": "0.28.1",
|
"@esbuild/linux-ppc64": "0.28.0",
|
||||||
"@esbuild/linux-riscv64": "0.28.1",
|
"@esbuild/linux-riscv64": "0.28.0",
|
||||||
"@esbuild/linux-s390x": "0.28.1",
|
"@esbuild/linux-s390x": "0.28.0",
|
||||||
"@esbuild/linux-x64": "0.28.1",
|
"@esbuild/linux-x64": "0.28.0",
|
||||||
"@esbuild/netbsd-arm64": "0.28.1",
|
"@esbuild/netbsd-arm64": "0.28.0",
|
||||||
"@esbuild/netbsd-x64": "0.28.1",
|
"@esbuild/netbsd-x64": "0.28.0",
|
||||||
"@esbuild/openbsd-arm64": "0.28.1",
|
"@esbuild/openbsd-arm64": "0.28.0",
|
||||||
"@esbuild/openbsd-x64": "0.28.1",
|
"@esbuild/openbsd-x64": "0.28.0",
|
||||||
"@esbuild/openharmony-arm64": "0.28.1",
|
"@esbuild/openharmony-arm64": "0.28.0",
|
||||||
"@esbuild/sunos-x64": "0.28.1",
|
"@esbuild/sunos-x64": "0.28.0",
|
||||||
"@esbuild/win32-arm64": "0.28.1",
|
"@esbuild/win32-arm64": "0.28.0",
|
||||||
"@esbuild/win32-ia32": "0.28.1",
|
"@esbuild/win32-ia32": "0.28.0",
|
||||||
"@esbuild/win32-x64": "0.28.1"
|
"@esbuild/win32-x64": "0.28.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
|
|||||||
@@ -40,7 +40,6 @@
|
|||||||
"@ai-sdk/google-vertex": "^4.0.16",
|
"@ai-sdk/google-vertex": "^4.0.16",
|
||||||
"@ai-sdk/openai": "^3.0.0",
|
"@ai-sdk/openai": "^3.0.0",
|
||||||
"@ai-sdk/react": "^3.0.1",
|
"@ai-sdk/react": "^3.0.1",
|
||||||
"@aihubmix/ai-sdk-provider": "^2.1.0",
|
|
||||||
"@aws-sdk/client-dynamodb": "^3.957.0",
|
"@aws-sdk/client-dynamodb": "^3.957.0",
|
||||||
"@aws-sdk/credential-providers": "^3.943.0",
|
"@aws-sdk/credential-providers": "^3.943.0",
|
||||||
"@extractus/article-extractor": "^8.0.18",
|
"@extractus/article-extractor": "^8.0.18",
|
||||||
|
|||||||
@@ -1,36 +1,10 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||||
import {
|
import {
|
||||||
getAIModel,
|
getAIModel,
|
||||||
isAihubmixStandardBaseURL,
|
|
||||||
resolveBaseURL,
|
resolveBaseURL,
|
||||||
supportsImageInput,
|
supportsImageInput,
|
||||||
supportsPromptCaching,
|
supportsPromptCaching,
|
||||||
} from "@/lib/ai-providers"
|
} from "@/lib/ai-providers"
|
||||||
import { extractAihubmixModelIds } from "@/lib/aihubmix-models"
|
|
||||||
|
|
||||||
describe("extractAihubmixModelIds", () => {
|
|
||||||
it("extracts unique chat model IDs from the AIHubMix model list payload", () => {
|
|
||||||
const models = extractAihubmixModelIds({
|
|
||||||
data: [
|
|
||||||
{ model_id: "claude-sonnet-4-5-20250929", types: "llm" },
|
|
||||||
{ model_id: "gpt-5.1", types: "llm" },
|
|
||||||
{ model_id: "gpt-5.1", types: "llm" },
|
|
||||||
{ model_id: "gpt-image-2", types: "image_generation,llm" },
|
|
||||||
{ model_id: "cohere-rerank-v4.0", types: "rerank" },
|
|
||||||
{ model_id: "", types: "llm" },
|
|
||||||
{ types: "llm" },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(models).toEqual(["claude-sonnet-4-5-20250929", "gpt-5.1"])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("returns an empty list for malformed payloads", () => {
|
|
||||||
expect(extractAihubmixModelIds({ data: null })).toEqual([])
|
|
||||||
expect(extractAihubmixModelIds({})).toEqual([])
|
|
||||||
expect(extractAihubmixModelIds(null)).toEqual([])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("resolveBaseURL", () => {
|
describe("resolveBaseURL", () => {
|
||||||
const SERVER_BASE_URL = "https://server-proxy.example.com"
|
const SERVER_BASE_URL = "https://server-proxy.example.com"
|
||||||
@@ -282,70 +256,6 @@ vi.mock("@ai-sdk/deepseek", () => {
|
|||||||
return { createDeepSeek: mockCreateDeepSeek, deepseek: mockDeepseek }
|
return { createDeepSeek: mockCreateDeepSeek, deepseek: mockDeepseek }
|
||||||
})
|
})
|
||||||
|
|
||||||
vi.mock("@aihubmix/ai-sdk-provider", () => {
|
|
||||||
const mockModel = { modelId: "test-model" }
|
|
||||||
const mockProviderFn = vi.fn(() => mockModel)
|
|
||||||
const mockCreateAihubmix = vi.fn(() => mockProviderFn)
|
|
||||||
const mockAihubmix = vi.fn(() => mockModel)
|
|
||||||
return { aihubmix: mockAihubmix, createAihubmix: mockCreateAihubmix }
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("AIHubMix provider", () => {
|
|
||||||
let createAihubmixMock: ReturnType<typeof vi.fn>
|
|
||||||
const savedEnv: Record<string, string | undefined> = {}
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
savedEnv.AIHUBMIX_API_KEY = process.env.AIHUBMIX_API_KEY
|
|
||||||
savedEnv.AIHUBMIX_BASE_URL = process.env.AIHUBMIX_BASE_URL
|
|
||||||
delete process.env.AIHUBMIX_BASE_URL
|
|
||||||
|
|
||||||
const mod = await import("@aihubmix/ai-sdk-provider")
|
|
||||||
createAihubmixMock = mod.createAihubmix as ReturnType<typeof vi.fn>
|
|
||||||
createAihubmixMock.mockClear()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
process.env.AIHUBMIX_API_KEY = savedEnv.AIHUBMIX_API_KEY
|
|
||||||
process.env.AIHUBMIX_BASE_URL = savedEnv.AIHUBMIX_BASE_URL
|
|
||||||
})
|
|
||||||
|
|
||||||
it("uses AIHUBMIX_API_KEY for server configured AIHubMix", () => {
|
|
||||||
process.env.AIHUBMIX_API_KEY = "server-aihubmix-key"
|
|
||||||
|
|
||||||
getAIModel({
|
|
||||||
provider: "aihubmix",
|
|
||||||
modelId: "claude-sonnet-4-5-20250929",
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(createAihubmixMock).toHaveBeenCalledWith({
|
|
||||||
apiKey: "server-aihubmix-key",
|
|
||||||
appCode: "MSBS9675",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("uses client BYOK API key for AIHubMix", () => {
|
|
||||||
getAIModel({
|
|
||||||
provider: "aihubmix",
|
|
||||||
apiKey: "client-aihubmix-key",
|
|
||||||
modelId: "gpt-5.1",
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(createAihubmixMock).toHaveBeenCalledWith({
|
|
||||||
apiKey: "client-aihubmix-key",
|
|
||||||
appCode: "MSBS9675",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("recognizes AIHubMix standard endpoints", () => {
|
|
||||||
expect(isAihubmixStandardBaseURL(undefined)).toBe(true)
|
|
||||||
expect(isAihubmixStandardBaseURL("https://aihubmix.com")).toBe(true)
|
|
||||||
expect(isAihubmixStandardBaseURL("https://aihubmix.com/v1/")).toBe(true)
|
|
||||||
expect(isAihubmixStandardBaseURL("https://proxy.example.com/v1")).toBe(
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("Kimi provider uses createDeepSeek for reasoning_content support", () => {
|
describe("Kimi provider uses createDeepSeek for reasoning_content support", () => {
|
||||||
let createDeepSeekMock: ReturnType<typeof vi.fn>
|
let createDeepSeekMock: ReturnType<typeof vi.fn>
|
||||||
const savedEnv: Record<string, string | undefined> = {}
|
const savedEnv: Record<string, string | undefined> = {}
|
||||||
|
|||||||
@@ -159,44 +159,6 @@ describe("loadFlattenedServerModels", () => {
|
|||||||
expect(defaultModel.modelId).toBe("gpt-4o") // First model of default provider
|
expect(defaultModel.modelId).toBe("gpt-4o") // First model of default provider
|
||||||
})
|
})
|
||||||
|
|
||||||
it("falls back to comma-separated AI_MODEL when no other config is set", async () => {
|
|
||||||
process.env.AI_MODELS_CONFIG = ""
|
|
||||||
process.env.AI_MODELS_CONFIG_PATH = `non-existent-config-${Date.now()}.json`
|
|
||||||
process.env.AI_PROVIDER = "openai"
|
|
||||||
process.env.AI_MODEL = "gpt-4o, gpt-4o-mini, gpt-4o"
|
|
||||||
|
|
||||||
const models = await loadFlattenedServerModels()
|
|
||||||
|
|
||||||
// Trims, deduplicates, and preserves order
|
|
||||||
expect(models.map((m) => m.modelId)).toEqual(["gpt-4o", "gpt-4o-mini"])
|
|
||||||
expect(models.every((m) => m.provider === "openai")).toBe(true)
|
|
||||||
|
|
||||||
// First model is marked default (provider has default: true)
|
|
||||||
const defaults = models.filter((m) => m.isDefault)
|
|
||||||
expect(defaults.length).toBe(1)
|
|
||||||
expect(defaults[0].modelId).toBe("gpt-4o")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("does not synthesize when AI_MODEL has no comma", async () => {
|
|
||||||
process.env.AI_MODELS_CONFIG = ""
|
|
||||||
process.env.AI_MODELS_CONFIG_PATH = `non-existent-config-${Date.now()}.json`
|
|
||||||
process.env.AI_PROVIDER = "openai"
|
|
||||||
process.env.AI_MODEL = "gpt-4o"
|
|
||||||
|
|
||||||
const models = await loadFlattenedServerModels()
|
|
||||||
expect(models).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("does not synthesize when AI_PROVIDER is unset", async () => {
|
|
||||||
process.env.AI_MODELS_CONFIG = ""
|
|
||||||
process.env.AI_MODELS_CONFIG_PATH = `non-existent-config-${Date.now()}.json`
|
|
||||||
delete process.env.AI_PROVIDER
|
|
||||||
process.env.AI_MODEL = "gpt-4o, gpt-4o-mini"
|
|
||||||
|
|
||||||
const models = await loadFlattenedServerModels()
|
|
||||||
expect(models).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("preserves apiKeyEnv array in flattened models for load balancing", async () => {
|
it("preserves apiKeyEnv array in flattened models for load balancing", async () => {
|
||||||
const config: ServerModelsConfig = {
|
const config: ServerModelsConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
Reference in New Issue
Block a user