Compare commits

..

6 Commits

Author SHA1 Message Date
dayuan.jiang
0956684b8e chore: refresh SUGGESTED_MODELS for all providers
Updates the SUGGESTED_MODELS quick-add list in lib/types/model-config.ts
against each provider's official model docs as of 2026-06-09.

- openai: GPT-5.5 / 5.4 frontier; drop deprecated 5.0-5.2 family
- anthropic: add Opus 4.8/4.7/4.6, Sonnet 4.6, Haiku 4.5 (new dateless
  pinned IDs); fix wrong date suffixes on Opus/Sonnet 4.5
- google / vertexai: adopt Gemini 3 family; drop Gemini 2.0 (shut down)
  and 1.5
- azure: GPT-5.x line + o3 / o4-mini; drop gpt-4-turbo / gpt-35-turbo
- bedrock: Opus 4.8/4.7/4.6, Sonnet 4.6, Haiku 4.5, Nova Premier /
  Nova 2 Lite, Llama 4 Maverick / Scout, Mistral Large 3, Pixtral
- openrouter: refreshed against live /api/v1/models
- deepseek: V4 Pro / Flash
- siliconflow / modelscope: DeepSeek V4, Qwen 3.x, drop bogus
  qwen3.5-plus
- gateway: verified against live Vercel AI Gateway endpoint
- doubao: Seed 2.0 / 1.8 / 1.6 in official dash-form IDs
- minimax: + M2.5
- novita: M3, GLM-5.1, Kimi-K2.6, DeepSeek V4
2026-06-09 21:08:02 +09:00
chaochaoweb3
410993a3bf fix: block private IPv6 URLs (#858)
* fix: block private IPv6 URLs

* fix: cover full fe80::/10 link-local range and :: unspecified

- Replace startsWith("fe80:") with a check covering the full fe80::/10
  range (fe80 through febf) per RFC 4291.
- Add :: (unspecified) to the localhost block.
- Drop the dead 0:0:0:0:0:0:0:1 branch (URL parser normalizes it to ::1).
- Add tests for fe9f::1, febf::1, and ::.

---------

Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-06 00:30:20 +09:00
Dayuan Jiang
77e7766f9a fix(deps): bump @ai-sdk/amazon-bedrock to 4.0.113 to fix tool streaming under Zod v4 (#860)
The installed 4.0.64 declared `input: z.unknown()` on BedrockToolUseSchema,
which Zod v4 treats as non-optional. With the
`fine-grained-tool-streaming-2025-05-14` beta enabled in lib/ai-providers.ts,
Bedrock's contentBlockStart event arrives without an `input` field, causing
type validation to fail with "expected nonoptional, received undefined".

4.0.101 fixed this upstream by marking input optional on the streaming
tool-use schema. The semver range `^4.0.1` already permitted this; only the
lockfile needed refreshing.

Closes #859
2026-06-06 00:15:45 +09:00
Octopus
a9ffd6a1de feat: upgrade MiniMax default model to M3 (#857)
- Add MiniMax-M3 to the model selection list (set as new default at top)
- Retain MiniMax-M2.7 and MiniMax-M2.7-highspeed
- Remove deprecated MiniMax-M2.5 / M2.5-highspeed
- Update supportsImageInput: M3 supports image input (M2.x stay text-only)
- Update unit tests to reflect new model lineup
- Update example AI_MODEL in CN/EN/JA docs to MiniMax-M3

Co-authored-by: octo-patch <octo-patch@github.com>
2026-06-02 19:38:49 +09:00
waterystone
277ad83552 fix(anthropic): support ANTHROPIC_AUTH_TOKEN as alternative to ANTHROPIC_API_KEY (#853)
* fix(anthropic): support ANTHROPIC_AUTH_TOKEN as alternative to ANTHROPIC_API_KEY

Anthropic SDK supports two mutually exclusive auth methods: apiKey (sent as
x-api-key header) and authToken (sent as Authorization: Bearer header). Detect
either env var during provider detection and credential validation, and pass
authToken to createAnthropic when only ANTHROPIC_AUTH_TOKEN is set.

* docs(anthropic): document ANTHROPIC_AUTH_TOKEN and refine error message

- Add ANTHROPIC_AUTH_TOKEN to env.example and the en/cn/ja provider docs
- Reword the missing-credential error to "Either ... or ..." for readability

---------

Co-authored-by: duyunjie <duyunjie@zhuanzhuan.com>
Co-authored-by: dayuan.jiang <jdy.toh@gmail.com>
2026-06-02 19:26:27 +09:00
Dayuan Jiang
7b6eb39fa5 fix(parse-url): block SSRF via private/internal URLs (#845)
/api/parse-url accepted any URL the user submitted, fetched it via
@extractus/article-extractor, and returned the body as Markdown. With
ALLOW_PRIVATE_URLS unset (the default after #600) the SSRF guard
short-circuited entirely, so an unauthenticated POST could probe
container ports, read AWS IMDS / GCP metadata, and reach same-VPC
internal services.

- parse-url now always rejects private URLs regardless of
  ALLOW_PRIVATE_URLS. The flag's only legitimate use case is local
  LLM provider baseUrl overrides (validate-model, chat); article
  extraction has no business fetching internal hosts. Local LLM
  setups (Ollama, LM Studio, etc.) are unaffected.
- Strip a trailing dot from the hostname before equality checks so
  the FQDN form "localhost." (which still resolves to 127.0.0.1) is
  caught by the existing string match.

Known follow-ups (not addressed here):
- DNS rebinding: hostnames are matched as strings; a public domain
  resolving to 127.0.0.1 (e.g. localtest.me) is not caught.
- HTTP redirects: @extractus/article-extractor uses cross-fetch with
  default redirect: "follow" and exposes no hook, so a public URL
  302-ing to an internal host still leaks.
2026-05-21 23:54:23 +09:00
11 changed files with 323 additions and 171 deletions

View File

@@ -1,7 +1,7 @@
import { extract } from "@extractus/article-extractor" import { extract } from "@extractus/article-extractor"
import { NextResponse } from "next/server" import { NextResponse } from "next/server"
import TurndownService from "turndown" import TurndownService from "turndown"
import { allowPrivateUrls, isPrivateUrl } from "@/lib/ssrf-protection" import { isPrivateUrl } from "@/lib/ssrf-protection"
const MAX_CONTENT_LENGTH = 150000 // Match PDF limit const MAX_CONTENT_LENGTH = 150000 // Match PDF limit
const EXTRACT_TIMEOUT_MS = 15000 const EXTRACT_TIMEOUT_MS = 15000
@@ -28,8 +28,10 @@ export async function POST(req: Request) {
) )
} }
// SSRF protection // SSRF protection: parse-url has no use case for fetching internal
if (!allowPrivateUrls && isPrivateUrl(url)) { // hosts, so private URLs are always rejected. ALLOW_PRIVATE_URLS only
// governs LLM provider baseUrl overrides (validate-model, chat).
if (isPrivateUrl(url)) {
return NextResponse.json( return NextResponse.json(
{ error: "Cannot access private/internal URLs" }, { error: "Cannot access private/internal URLs" },
{ status: 400 }, { status: 400 },

View File

@@ -53,6 +53,13 @@ ANTHROPIC_API_KEY=your_api_key
AI_MODEL=claude-sonnet-4-5-20250514 AI_MODEL=claude-sonnet-4-5-20250514
``` ```
或者使用 Bearer 认证令牌(例如通过会下发 OAuth 风格 token 的网关时)。`ANTHROPIC_AUTH_TOKEN` 会作为 `Authorization: Bearer <token>` 头发送,而 `ANTHROPIC_API_KEY` 会作为 `x-api-key` 头发送。两者互斥,只能设置其中之一:
```bash
ANTHROPIC_AUTH_TOKEN=your_auth_token
AI_MODEL=claude-sonnet-4-5-20250514
```
可选的自定义端点: 可选的自定义端点:
```bash ```bash
@@ -215,7 +222,7 @@ MiniMax 支持两种 API 格式:
```bash ```bash
MINIMAX_API_KEY=your_api_key MINIMAX_API_KEY=your_api_key
AI_MODEL=MiniMax-M2.7 AI_MODEL=MiniMax-M3
``` ```
可选配置: 可选配置:

View File

@@ -68,6 +68,13 @@ ANTHROPIC_API_KEY=your_api_key
AI_MODEL=claude-sonnet-4-5-20250514 AI_MODEL=claude-sonnet-4-5-20250514
``` ```
Or use a Bearer auth token instead of an API key (e.g. when going through a gateway that issues OAuth-style tokens). `ANTHROPIC_AUTH_TOKEN` is sent as `Authorization: Bearer <token>`, while `ANTHROPIC_API_KEY` is sent as `x-api-key`. The two are mutually exclusive — set only one:
```bash
ANTHROPIC_AUTH_TOKEN=your_auth_token
AI_MODEL=claude-sonnet-4-5-20250514
```
Optional custom endpoint: Optional custom endpoint:
```bash ```bash
@@ -230,7 +237,7 @@ MiniMax supports two API formats:
```bash ```bash
MINIMAX_API_KEY=your_api_key MINIMAX_API_KEY=your_api_key
AI_MODEL=MiniMax-M2.7 AI_MODEL=MiniMax-M3
``` ```
Optional configuration: Optional configuration:

View File

@@ -53,6 +53,13 @@ ANTHROPIC_API_KEY=your_api_key
AI_MODEL=claude-sonnet-4-5-20250514 AI_MODEL=claude-sonnet-4-5-20250514
``` ```
または、Bearer 認証トークンを使用することもできますOAuth スタイルのトークンを発行するゲートウェイ経由で利用する場合など)。`ANTHROPIC_AUTH_TOKEN``Authorization: Bearer <token>` ヘッダーで送信され、`ANTHROPIC_API_KEY``x-api-key` ヘッダーで送信されます。両者は排他的なので、いずれか一方のみを設定してください:
```bash
ANTHROPIC_AUTH_TOKEN=your_auth_token
AI_MODEL=claude-sonnet-4-5-20250514
```
任意のカスタムエンドポイント: 任意のカスタムエンドポイント:
```bash ```bash
@@ -215,7 +222,7 @@ MiniMax は 2 つの API 形式をサポートしています:
```bash ```bash
MINIMAX_API_KEY=your_api_key MINIMAX_API_KEY=your_api_key
AI_MODEL=MiniMax-M2.7 AI_MODEL=MiniMax-M3
``` ```
オプション設定: オプション設定:

View File

@@ -25,7 +25,8 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
# OPENAI_REASONING_SUMMARY=detailed # Optional: Override reasoning summary (none/brief/detailed) # OPENAI_REASONING_SUMMARY=detailed # Optional: Override reasoning summary (none/brief/detailed)
# Anthropic (Direct) Configuration # Anthropic (Direct) Configuration
# ANTHROPIC_API_KEY=sk-ant-... # ANTHROPIC_API_KEY=sk-ant-... # Sent as `x-api-key` header
# ANTHROPIC_AUTH_TOKEN= # Alternative to ANTHROPIC_API_KEY; sent as `Authorization: Bearer` header (mutually exclusive)
# ANTHROPIC_BASE_URL=https://your-custom-anthropic/v1 # ANTHROPIC_BASE_URL=https://your-custom-anthropic/v1
# ANTHROPIC_THINKING_TYPE=enabled # Optional: Anthropic extended thinking (enabled) # ANTHROPIC_THINKING_TYPE=enabled # Optional: Anthropic extended thinking (enabled)
# ANTHROPIC_THINKING_BUDGET_TOKENS=12000 # Optional: Budget for extended thinking in tokens # ANTHROPIC_THINKING_BUDGET_TOKENS=12000 # Optional: Budget for extended thinking in tokens

View File

@@ -573,7 +573,15 @@ function detectProvider(): ProviderName | null {
// Skip ollama - it doesn't require credentials // Skip ollama - it doesn't require credentials
continue continue
} }
if (process.env[envVar]) { // Anthropic accepts ANTHROPIC_AUTH_TOKEN (Bearer auth) as alternative to ANTHROPIC_API_KEY
const hasCredential =
provider === "anthropic"
? !!(
process.env.ANTHROPIC_API_KEY ||
process.env.ANTHROPIC_AUTH_TOKEN
)
: !!process.env[envVar]
if (hasCredential) {
// Azure requires additional config (baseURL or resourceName) // Azure requires additional config (baseURL or resourceName)
if (provider === "azure") { if (provider === "azure") {
const hasBaseUrl = !!process.env.AZURE_BASE_URL const hasBaseUrl = !!process.env.AZURE_BASE_URL
@@ -615,13 +623,26 @@ function validateProviderCredentials(
return return
} }
// Use custom env var name if provided, otherwise use default // Anthropic accepts ANTHROPIC_AUTH_TOKEN (Bearer auth) as alternative to ANTHROPIC_API_KEY
const requiredVar = customApiKeyEnv || PROVIDER_ENV_VARS[provider] if (provider === "anthropic" && !customApiKeyEnv) {
if (requiredVar && !process.env[requiredVar]) { const hasCredential = !!(
throw new Error( process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
`${requiredVar} environment variable is required for ${provider} provider. ` +
`Please set it in your .env.local file.`,
) )
if (!hasCredential) {
throw new Error(
`Either ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN environment variable is required for anthropic provider. ` +
`Please set one in your .env.local file.`,
)
}
} else {
// Use custom env var name if provided, otherwise use default
const requiredVar = customApiKeyEnv || PROVIDER_ENV_VARS[provider]
if (requiredVar && !process.env[requiredVar]) {
throw new Error(
`${requiredVar} environment variable is required for ${provider} provider. ` +
`Please set it in your .env.local file.`,
)
}
} }
// Azure requires either AZURE_BASE_URL or AZURE_RESOURCE_NAME in addition to API key // Azure requires either AZURE_BASE_URL or AZURE_RESOURCE_NAME in addition to API key
@@ -845,8 +866,16 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
serverBaseUrl, serverBaseUrl,
"https://api.anthropic.com/v1", "https://api.anthropic.com/v1",
) )
// Anthropic supports two auth methods (mutually exclusive):
// - apiKey: sends as `x-api-key` header
// - authToken: sends as `Authorization: Bearer <token>` header
// Prefer apiKey if present (including client overrides); fall back
// to ANTHROPIC_AUTH_TOKEN env var only when no apiKey is available.
const authToken = !apiKey
? process.env.ANTHROPIC_AUTH_TOKEN
: undefined
const customProvider = createAnthropic({ const customProvider = createAnthropic({
apiKey, ...(authToken ? { authToken } : { apiKey }),
baseURL, baseURL,
headers: ANTHROPIC_BETA_HEADERS, headers: ANTHROPIC_BETA_HEADERS,
}) })
@@ -1361,8 +1390,12 @@ export function supportsImageInput(modelId: string): boolean {
return false return false
} }
// MiniMax text models (MiniMax-M2.x series are text-only) // MiniMax text models (MiniMax-M2.x series are text-only; M3 supports image input)
if (lowerModelId.includes("minimax") && !hasVisionIndicator) { if (
lowerModelId.includes("minimax") &&
!hasVisionIndicator &&
!lowerModelId.includes("m3")
) {
return false return false
} }

View File

@@ -9,17 +9,40 @@
export function isPrivateUrl(urlString: string): boolean { export function isPrivateUrl(urlString: string): boolean {
try { try {
const url = new URL(urlString) const url = new URL(urlString)
const hostname = url.hostname.toLowerCase() // Strip a trailing dot so FQDN forms like "localhost." (which still
// resolve to 127.0.0.1) cannot bypass the equality checks below.
const hostname = url.hostname
.toLowerCase()
.replace(/^\[|\]$/g, "")
.replace(/\.$/, "")
// Block localhost // Block localhost
if ( if (
hostname === "localhost" || hostname === "localhost" ||
hostname === "127.0.0.1" || hostname === "127.0.0.1" ||
hostname === "::1" hostname === "::1" ||
hostname === "::"
) { ) {
return true return true
} }
// Block IPv6 unique-local (fc00::/7), link-local (fe80::/10),
// and IPv4-mapped (::ffff:0:0/96) hosts.
if (hostname.includes(":")) {
if (
hostname.startsWith("fc") ||
hostname.startsWith("fd") ||
hostname.startsWith("::ffff:")
) {
return true
}
const linkLocal = hostname.match(/^fe([0-9a-f]{2}):/)
if (linkLocal) {
const high = parseInt(linkLocal[1], 16)
if (high >= 0x80 && high <= 0xbf) return true
}
}
// Block AWS/cloud metadata endpoints // Block AWS/cloud metadata endpoints
if ( if (
hostname === "169.254.169.254" || hostname === "169.254.169.254" ||

View File

@@ -190,177 +190,202 @@ export const PROVIDER_INFO: Record<
// Suggested models per provider for quick add // Suggested models per provider for quick add
export const SUGGESTED_MODELS: Partial<Record<ProviderName, string[]>> = { export const SUGGESTED_MODELS: Partial<Record<ProviderName, string[]>> = {
openai: [ openai: [
"gpt-5.2-pro", "gpt-5.5-pro",
"gpt-5.2-chat-latest", "gpt-5.5",
"gpt-5.2", "gpt-5.4-pro",
"gpt-5.1-codex-mini", "gpt-5.4",
"gpt-5.1-codex", "gpt-5.4-mini",
"gpt-5.1-chat-latest", "gpt-5.4-nano",
"gpt-5.1", "gpt-5-codex-mini",
"gpt-5-pro",
"gpt-5",
"gpt-5-mini",
"gpt-5-nano",
"gpt-5-codex",
"gpt-5-chat-latest",
"gpt-4.1", "gpt-4.1",
"gpt-4.1-mini", "gpt-4.1-mini",
"gpt-4.1-nano",
"gpt-4o", "gpt-4o",
"gpt-4o-mini", "gpt-4o-mini",
], ],
anthropic: [ anthropic: [
// Claude 4.5 series (latest) // Claude 4.8 / 4.7 / 4.6 series (latest, dateless pinned IDs)
"claude-opus-4-5-20250514", "claude-opus-4-8",
"claude-sonnet-4-5-20250514", "claude-sonnet-4-6",
// Claude 4 series "claude-haiku-4-5",
"claude-opus-4-20250514", "claude-opus-4-7",
"claude-sonnet-4-20250514", "claude-opus-4-6",
// Claude 4.5 series
"claude-sonnet-4-5-20250929",
"claude-opus-4-5-20251101",
// Claude 3.7 series // Claude 3.7 series
"claude-3-7-sonnet-20250219", "claude-3-7-sonnet-20250219",
// Claude 3.5 series // Claude 3.5 series
"claude-3-5-sonnet-20241022", "claude-3-5-sonnet-20241022",
"claude-3-5-haiku-20241022", "claude-3-5-haiku-20241022",
// Claude 3 series
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-haiku-20240307",
], ],
google: [ google: [
// Gemini 3 series
"gemini-3.1-pro",
"gemini-3.5-flash",
"gemini-3-flash",
"gemini-3.1-flash-lite",
// Gemini 2.5 series // Gemini 2.5 series
"gemini-2.5-pro", "gemini-2.5-pro",
"gemini-2.5-flash", "gemini-2.5-flash",
"gemini-2.5-flash-preview-05-20", "gemini-2.5-flash-lite",
// Gemini 2.0 series
"gemini-2.0-flash",
"gemini-2.0-flash-exp",
"gemini-2.0-flash-lite",
// Gemini 1.5 series
"gemini-1.5-pro",
"gemini-1.5-flash",
// Legacy
"gemini-pro",
], ],
vertexai: [ vertexai: [
// Gemini 3 series
"gemini-3.1-pro-preview",
"gemini-3.5-flash",
"gemini-3-flash-preview",
"gemini-3.1-flash-lite",
// Gemini 2.5 series // Gemini 2.5 series
"gemini-2.5-pro", "gemini-2.5-pro",
"gemini-2.5-flash", "gemini-2.5-flash",
// Gemini 2.0 series "gemini-2.5-flash-lite",
"gemini-2.0-flash", ],
"gemini-2.0-flash-exp", azure: [
// Gemini 1.5 series "gpt-5.5",
"gemini-1.5-pro", "gpt-5.4",
"gemini-1.5-flash", "gpt-5.1",
"gpt-5",
"gpt-5-mini",
"gpt-4.1",
"gpt-4o",
"gpt-4o-mini",
"o3",
"o4-mini",
], ],
azure: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4", "gpt-35-turbo"],
bedrock: [ bedrock: [
// Anthropic Claude // Anthropic Claude
"anthropic.claude-opus-4-5-20250514-v1:0", "anthropic.claude-opus-4-8",
"anthropic.claude-sonnet-4-5-20250514-v1:0", "anthropic.claude-opus-4-7",
"anthropic.claude-sonnet-4-6",
"anthropic.claude-opus-4-6-v1",
"anthropic.claude-opus-4-5-20251101-v1:0",
"anthropic.claude-sonnet-4-5-20250929-v1:0",
"anthropic.claude-haiku-4-5-20251001-v1:0",
"anthropic.claude-opus-4-1-20250805-v1:0",
"anthropic.claude-opus-4-20250514-v1:0", "anthropic.claude-opus-4-20250514-v1:0",
"anthropic.claude-sonnet-4-20250514-v1:0", "anthropic.claude-sonnet-4-20250514-v1:0",
"anthropic.claude-3-7-sonnet-20250219-v1:0",
"anthropic.claude-3-5-sonnet-20241022-v2:0",
"anthropic.claude-3-5-haiku-20241022-v1:0", "anthropic.claude-3-5-haiku-20241022-v1:0",
"anthropic.claude-3-opus-20240229-v1:0",
"anthropic.claude-3-sonnet-20240229-v1:0",
"anthropic.claude-3-haiku-20240307-v1:0",
// Amazon Nova // Amazon Nova
"amazon.nova-2-lite-v1:0",
"amazon.nova-premier-v1:0",
"amazon.nova-pro-v1:0", "amazon.nova-pro-v1:0",
"amazon.nova-lite-v1:0", "amazon.nova-lite-v1:0",
"amazon.nova-micro-v1:0", "amazon.nova-micro-v1:0",
// Meta Llama // Meta Llama
"meta.llama4-maverick-17b-instruct-v1:0",
"meta.llama4-scout-17b-instruct-v1:0",
"meta.llama3-3-70b-instruct-v1:0", "meta.llama3-3-70b-instruct-v1:0",
"meta.llama3-1-405b-instruct-v1:0",
"meta.llama3-1-70b-instruct-v1:0",
// Mistral // Mistral
"mistral.mistral-large-2411-v1:0", "mistral.mistral-large-3-675b-instruct",
"mistral.mistral-small-2503-v1:0", "mistral.pixtral-large-2502-v1:0",
], ],
openrouter: [ openrouter: [
// Anthropic // Anthropic
"anthropic/claude-sonnet-4", "anthropic/claude-opus-4.8",
"anthropic/claude-opus-4", "anthropic/claude-sonnet-4.6",
"anthropic/claude-3.5-sonnet", "anthropic/claude-haiku-4.5",
"anthropic/claude-3.5-haiku",
// OpenAI // OpenAI
"openai/gpt-4o", "openai/gpt-5.5",
"openai/gpt-5.4",
"openai/gpt-5.4-mini",
"openai/gpt-4o-mini", "openai/gpt-4o-mini",
"openai/o1",
"openai/o3-mini",
// Google // Google
"google/gemini-2.5-pro", "google/gemini-3.1-pro-preview",
"google/gemini-2.5-flash", "google/gemini-3.5-flash",
"google/gemini-2.0-flash-exp:free", "google/gemini-2.5-flash-lite",
// xAI
"x-ai/grok-4.3",
// Meta Llama // Meta Llama
"meta-llama/llama-4-maverick",
"meta-llama/llama-4-scout",
"meta-llama/llama-3.3-70b-instruct", "meta-llama/llama-3.3-70b-instruct",
"meta-llama/llama-3.1-405b-instruct",
"meta-llama/llama-3.1-70b-instruct",
// DeepSeek // DeepSeek
"deepseek/deepseek-chat", "deepseek/deepseek-v4-pro",
"deepseek/deepseek-r1", "deepseek/deepseek-v3.2",
// Qwen // Qwen
"qwen/qwen-2.5-72b-instruct", "qwen/qwen3.7-max",
"qwen/qwen3-coder",
// MiniMax
"minimax/minimax-m3",
],
deepseek: [
"deepseek-v4-pro",
"deepseek-v4-flash",
"deepseek-chat",
"deepseek-reasoner",
], ],
deepseek: ["deepseek-chat", "deepseek-reasoner", "deepseek-coder"],
siliconflow: [ siliconflow: [
// DeepSeek // DeepSeek
"deepseek-ai/DeepSeek-V3", "deepseek-ai/DeepSeek-V4-Pro",
"deepseek-ai/DeepSeek-R1", "deepseek-ai/DeepSeek-V4-Flash",
"deepseek-ai/DeepSeek-V2.5", "deepseek-ai/DeepSeek-V3.2",
// MiniMax
"MiniMaxAI/MiniMax-M3",
// Moonshot
"moonshotai/Kimi-K2.6",
// Z.ai
"zai-org/GLM-5",
// Qwen // Qwen
"Qwen/Qwen2.5-72B-Instruct", "Qwen/Qwen3.6-35B-A3B",
"Qwen/Qwen2.5-32B-Instruct", "Qwen/Qwen3-Coder-480B-A35B-Instruct",
"Qwen/Qwen2.5-Coder-32B-Instruct", "Qwen/Qwen3-30B-A3B-Instruct-2507",
"Qwen/Qwen2.5-7B-Instruct", "Qwen/Qwen3-VL-32B-Instruct",
"Qwen/Qwen2-VL-72B-Instruct", // OpenAI open-weights
"qwen3.5-plus", "openai/gpt-oss-120b",
], ],
sglang: [ sglang: [
// SGLang is OpenAI-compatible, models depend on deployment // SGLang is OpenAI-compatible, models depend on deployment
"default", "default",
], ],
gateway: [ gateway: [
"openai/gpt-4o", "openai/gpt-5.5",
"openai/gpt-4o-mini", "anthropic/claude-opus-4.7",
"anthropic/claude-sonnet-4-5", "google/gemini-3.1-pro-preview",
"anthropic/claude-3-5-sonnet", "xai/grok-4.3",
"google/gemini-2.0-flash", "anthropic/claude-sonnet-4.6",
"anthropic/claude-haiku-4.5",
"openai/gpt-5.4-mini",
], ],
edgeone: ["@tx/deepseek-ai/deepseek-v32"], edgeone: ["@tx/deepseek-ai/deepseek-v32"],
doubao: [ doubao: [
// ByteDance Doubao models // ByteDance Doubao models (Volcengine Ark IDs use dash form)
"doubao-1.5-thinking-pro-250415", "doubao-seed-2-0-pro-260215",
"doubao-1.5-thinking-pro-m-250428", "doubao-seed-2-0-lite-260428",
"doubao-1.5-pro-32k-250115", "doubao-seed-2-0-mini-260428",
"doubao-1.5-pro-256k-250115", "doubao-seed-1-8-251228",
"doubao-pro-32k-241215", "doubao-seed-1-6-251015",
"doubao-pro-256k-241215", "doubao-seed-1-6-flash-250828",
"doubao-seed-1-6-vision-250815",
"doubao-1-5-pro-32k-250115",
"doubao-1-5-lite-32k-250115",
], ],
modelscope: [ modelscope: [
// DeepSeek
"deepseek-ai/DeepSeek-V4-Pro",
"deepseek-ai/DeepSeek-V3.2",
"deepseek-ai/DeepSeek-R1-0528",
"deepseek-ai/DeepSeek-R1",
// Qwen // Qwen
"Qwen/Qwen2.5-72B-Instruct",
"Qwen/Qwen2.5-32B-Instruct",
"Qwen/Qwen3-235B-A22B-Instruct-2507", "Qwen/Qwen3-235B-A22B-Instruct-2507",
"Qwen/Qwen3-VL-235B-A22B-Instruct", "Qwen/Qwen3-VL-235B-A22B-Instruct",
"Qwen/Qwen3-Coder-30B-A3B-Instruct",
"Qwen/Qwen3-32B", "Qwen/Qwen3-32B",
"qwen3.5-plus", "Qwen/Qwen2.5-72B-Instruct",
// DeepSeek
"deepseek-ai/DeepSeek-R1-0528",
"deepseek-ai/DeepSeek-V3.2",
], ],
minimax: [ minimax: [
// MiniMax models (Anthropic-compatible API) // MiniMax models (Anthropic-compatible API)
"MiniMax-M3",
"MiniMax-M2.7", "MiniMax-M2.7",
"MiniMax-M2.7-highspeed", "MiniMax-M2.7-highspeed",
"MiniMax-M2.5", "MiniMax-M2.5",
"MiniMax-M2.5-highspeed",
], ],
novita: [ novita: [
// Novita AI models (OpenAI-compatible API) // Novita AI models (OpenAI-compatible API)
"moonshotai/kimi-k2.5", "minimax/minimax-m3",
"zai-org/glm-5", "deepseek/deepseek-v4-pro",
"minimax/minimax-m2.5", "zai-org/glm-5.1",
"moonshotai/kimi-k2.6",
"deepseek/deepseek-v4-flash",
], ],
} }

123
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "next-ai-draw-io", "name": "next-ai-draw-io",
"version": "0.4.15", "version": "0.4.16",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "next-ai-draw-io", "name": "next-ai-draw-io",
"version": "0.4.15", "version": "0.4.16",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@ai-sdk/amazon-bedrock": "^4.0.1", "@ai-sdk/amazon-bedrock": "^4.0.1",
@@ -122,14 +122,15 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@ai-sdk/amazon-bedrock": { "node_modules/@ai-sdk/amazon-bedrock": {
"version": "4.0.64", "version": "4.0.113",
"resolved": "https://registry.npmjs.org/@ai-sdk/amazon-bedrock/-/amazon-bedrock-4.0.64.tgz", "resolved": "https://registry.npmjs.org/@ai-sdk/amazon-bedrock/-/amazon-bedrock-4.0.113.tgz",
"integrity": "sha512-foZskBdIHPGPZLfEKqkOP18JgfMy5Duik1TTduvEQ97mX3yfEzr4go1V3oU7ktry0dAL5BiNY38WwRhl+xhy9g==", "integrity": "sha512-qoeF2ghkYqHY4u68rasZopYsRuGnRwgqSfe6rhC/jM6W73Z7TU5v9QcfDYKt9dgp19YHY9EqiX3SXVC+uPGkRQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "3.0.47", "@ai-sdk/anthropic": "3.0.81",
"@ai-sdk/provider": "3.0.8", "@ai-sdk/openai": "3.0.68",
"@ai-sdk/provider-utils": "4.0.15", "@ai-sdk/provider": "3.0.10",
"@ai-sdk/provider-utils": "4.0.27",
"@smithy/eventstream-codec": "^4.0.1", "@smithy/eventstream-codec": "^4.0.1",
"@smithy/util-utf8": "^4.0.0", "@smithy/util-utf8": "^4.0.0",
"aws4fetch": "^1.0.20" "aws4fetch": "^1.0.20"
@@ -141,6 +142,67 @@
"zod": "^3.25.76 || ^4.1.8" "zod": "^3.25.76 || ^4.1.8"
} }
}, },
"node_modules/@ai-sdk/amazon-bedrock/node_modules/@ai-sdk/anthropic": {
"version": "3.0.81",
"resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-3.0.81.tgz",
"integrity": "sha512-B1JDd9Ugq9R5AgIaW3674lhGCMMYJcPUxnrZh8fzbGojgg4QvHFRv6eZahGQAUsmGHbcf74G9bdSBDLWQGY2GA==",
"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/amazon-bedrock/node_modules/@ai-sdk/openai": {
"version": "3.0.68",
"resolved": "https://registry.npmjs.org/@ai-sdk/openai/-/openai-3.0.68.tgz",
"integrity": "sha512-FCs/DPr4M95UyZ/ABHJmTmCEYRCka/4J0Bna0nsd78QCdGIS0X/zhn+fVzB7mZJo7464uOWYUjROx9PGNGOb0w==",
"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/amazon-bedrock/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/amazon-bedrock/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/anthropic": { "node_modules/@ai-sdk/anthropic": {
"version": "3.0.47", "version": "3.0.47",
"resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-3.0.47.tgz", "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-3.0.47.tgz",
@@ -2243,9 +2305,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2263,9 +2322,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2283,9 +2339,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2303,9 +2356,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -4898,9 +4948,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -4917,9 +4964,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -4936,9 +4980,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -4955,9 +4996,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -8304,9 +8342,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -13936,9 +13971,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/eventsource-parser": { "node_modules/eventsource-parser": {
"version": "3.0.6", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz",
"integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
@@ -16613,9 +16648,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -16799,9 +16831,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -16822,9 +16851,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [
@@ -16845,9 +16871,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "MPL-2.0", "license": "MPL-2.0",
"optional": true, "optional": true,
"os": [ "os": [

View File

@@ -180,11 +180,14 @@ describe("supportsImageInput", () => {
expect(supportsImageInput("moonshot-v1-128k")).toBe(false) expect(supportsImageInput("moonshot-v1-128k")).toBe(false)
}) })
it("returns false for MiniMax text models", () => { it("returns false for MiniMax M2 text models", () => {
expect(supportsImageInput("MiniMax-M2.7")).toBe(false) expect(supportsImageInput("MiniMax-M2.7")).toBe(false)
expect(supportsImageInput("MiniMax-M2.5")).toBe(false) expect(supportsImageInput("MiniMax-M2.7-highspeed")).toBe(false)
expect(supportsImageInput("MiniMax-M2")).toBe(false) expect(supportsImageInput("MiniMax-M2")).toBe(false)
expect(supportsImageInput("MiniMax-M2.5-highspeed")).toBe(false) })
it("returns true for MiniMax M3 (supports image input)", () => {
expect(supportsImageInput("MiniMax-M3")).toBe(true)
}) })
it("returns false for DeepSeek text models", () => { it("returns false for DeepSeek text models", () => {

View File

@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest"
import { isPrivateUrl } from "@/lib/ssrf-protection"
describe("isPrivateUrl", () => {
it("blocks private IPv6 URLs", () => {
expect(isPrivateUrl("http://[::1]/")).toBe(true)
expect(isPrivateUrl("http://[0:0:0:0:0:0:0:1]/")).toBe(true)
expect(isPrivateUrl("http://[::]/")).toBe(true)
expect(isPrivateUrl("http://[::ffff:127.0.0.1]/")).toBe(true)
expect(isPrivateUrl("http://[fc00::1]/")).toBe(true)
expect(isPrivateUrl("http://[fd12:3456:789a::1]/")).toBe(true)
expect(isPrivateUrl("http://[fe80::1]/")).toBe(true)
expect(isPrivateUrl("http://[fe9f::1]/")).toBe(true)
expect(isPrivateUrl("http://[febf::1]/")).toBe(true)
})
it("allows public URLs", () => {
expect(isPrivateUrl("https://example.com/article")).toBe(false)
expect(isPrivateUrl("https://fc00.example.com/article")).toBe(false)
})
})