Compare commits

..
Author SHA1 Message Date
dayuan.jiang ab33dae69d fix(mcp-server): read npm 12's pack output in the package check
The publish workflow installs the latest npm, and npm 12 prints
`npm pack --json` as an object keyed by package name instead of a list,
so check-package failed and 0.3.0 was not published. Accept both.
2026-10-05 22:43:05 +09:00
14 changed files with 1349 additions and 573 deletions
+22 -25
View File
@@ -77,7 +77,6 @@ export default function ExamplePanel({
minimal?: boolean
}) {
const dict = useDictionary()
const isSelfHosted = process.env.NEXT_PUBLIC_SELFHOSTED === "true"
const handleReplicateFlowchart = async () => {
setInput("Replicate this flowchart.")
@@ -126,31 +125,29 @@ export default function ExamplePanel({
<div className={minimal ? "" : "py-6 px-2 animate-fade-in"}>
{!minimal && (
<>
{/* MCP Server Notice, hidden on self-hosted deployments */}
{!isSelfHosted && (
<a
href="https://github.com/DayuanJiang/next-ai-draw-io/tree/main/packages/mcp-server"
target="_blank"
rel="noopener noreferrer"
className="block mb-4 p-3 rounded-xl bg-gradient-to-r from-purple-500/10 to-blue-500/10 border border-purple-500/20 hover:border-purple-500/40 transition-colors group"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-purple-500/20 flex items-center justify-center shrink-0">
<Terminal className="w-4 h-4 text-purple-500" />
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-foreground group-hover:text-purple-500 transition-colors">
{dict.examples.mcpServer}
</span>
</div>
<p className="text-xs text-muted-foreground">
{dict.examples.mcpDescription}
</p>
</div>
{/* MCP Server Notice */}
<a
href="https://github.com/DayuanJiang/next-ai-draw-io/tree/main/packages/mcp-server"
target="_blank"
rel="noopener noreferrer"
className="block mb-4 p-3 rounded-xl bg-gradient-to-r from-purple-500/10 to-blue-500/10 border border-purple-500/20 hover:border-purple-500/40 transition-colors group"
>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-purple-500/20 flex items-center justify-center shrink-0">
<Terminal className="w-4 h-4 text-purple-500" />
</div>
</a>
)}
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-foreground group-hover:text-purple-500 transition-colors">
{dict.examples.mcpServer}
</span>
</div>
<p className="text-xs text-muted-foreground">
{dict.examples.mcpDescription}
</p>
</div>
</div>
</a>
{/* Welcome section */}
<div className="text-center mb-6">
+1 -1
View File
@@ -192,7 +192,7 @@ MODELSCOPE_BASE_URL=https://your-custom-endpoint
可选的自定义 URL:
```bash
OLLAMA_BASE_URL=http://localhost:11434/api
OLLAMA_BASE_URL=http://localhost:11434
```
### Vercel AI Gateway
+1 -1
View File
@@ -194,7 +194,7 @@ AI_MODEL=llama3.2
Optional custom URL:
```bash
OLLAMA_BASE_URL=http://localhost:11434/api
OLLAMA_BASE_URL=http://localhost:11434
```
### ModelScope
+1 -1
View File
@@ -179,7 +179,7 @@ AI_MODEL=llama3.2
任意のカスタム URL:
```bash
OLLAMA_BASE_URL=http://localhost:11434/api
OLLAMA_BASE_URL=http://localhost:11434
```
### ModelScope
+2 -3
View File
@@ -1,6 +1,6 @@
# AI Provider Configuration
# AI_PROVIDER: Which provider to use
# Options: bedrock, openai, anthropic, google, vertexai, azure, ollama, openrouter, aihubmix, deepseek, siliconflow, sglang, gateway, edgeone, doubao, modelscope, glm, qwen, qiniu, kimi, minimax, novita, mimo, atlascloud
# Options: bedrock, openai, anthropic, google, vertexai, azure, ollama, openrouter, aihubmix, deepseek, siliconflow, gateway, novita
# Default: bedrock
AI_PROVIDER=bedrock
@@ -70,7 +70,7 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
# AZURE_REASONING_SUMMARY=detailed
# Ollama Configuration (Local or Cloud)
# OLLAMA_BASE_URL=https://ollama.com/api # Optional: Ollama Cloud; defaults to local Ollama (http://127.0.0.1:11434/api)
# OLLAMA_BASE_URL=https://ollama.com/api # Optional: Ollama Cloud; defaults to local Ollama (http://127.0.0.1:11434)
# OLLAMA_API_KEY=your-ollama-cloud-api-key # Optional: For Ollama Cloud or authenticated remote instances
# OLLAMA_ENABLE_THINKING=true # Optional: Enable thinking for models that support it (e.g., qwen3)
@@ -168,7 +168,6 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
# which triggers the client UI to display messages suggesting self-hosting or sponsorship.
# This switch allows self-hosted users to provide custom messages in response to a 429 code,
# in messageTokenSelfHosted, messageApiSelfHosted, and tipSelfHosted translation strings.
# It also turns off the MCP server advertisement in the chat examples.
# NEXT_PUBLIC_SELFHOSTED=true
# Minimax Configuration (Optional)
+1 -6
View File
@@ -125,12 +125,7 @@ let writableCache: boolean | null = null
export function isSettingsWritable(): boolean {
if (writableCache !== null) return writableCache
try {
const filePath = getSettingsPath()
if (fs.existsSync(filePath) && !fs.statSync(filePath).isFile()) {
writableCache = false
return writableCache
}
const dir = path.dirname(filePath)
const dir = path.dirname(getSettingsPath())
fs.mkdirSync(dir, { recursive: true })
fs.accessSync(dir, fs.constants.W_OK)
writableCache = true
+1 -2
View File
@@ -25,7 +25,6 @@ import { getEnvFallback } from "@/lib/admin/settings"
import { isPrivateUrl, redirectGuardedFetch } from "@/lib/ssrf-protection"
import {
normalizeBaseUrl,
ollamaApiUrl,
PROVIDER_INFO,
type ProviderName,
} from "@/lib/types/model-config"
@@ -1026,7 +1025,7 @@ export function getAIModel(clientOverrides?: ClientOverrides): ModelConfig {
? PROVIDER_INFO.ollama.defaultBaseUrl
: resolveBaseUrlEnv(overrides, "OLLAMA_BASE_URL"))
model = createOllama({
...(baseURL && { baseURL: ollamaApiUrl(baseURL) }),
...(baseURL && { baseURL }),
...(apiKey && {
headers: { Authorization: `Bearer ${apiKey}` },
}),
+2 -6
View File
@@ -3,7 +3,6 @@ import { getModelInfo } from "@/lib/model-catalog"
import { readLimitedBody } from "@/lib/read-limited-body"
import {
normalizeBaseUrl,
ollamaApiUrl,
PROVIDER_INFO,
type ProviderName,
} from "@/lib/types/model-config"
@@ -201,11 +200,8 @@ export async function listProviderModels(
break
}
case "ollama": {
const data = await getJson(
`${ollamaApiUrl(base)}/tags`,
bearer,
fetchFn,
)
const api = base.endsWith("/api") ? base : `${base}/api`
const data = await getJson(`${api}/tags`, bearer, fetchFn)
models = (data.models ?? []).map((m: { name: string }) => ({
id: m.name,
}))
+1 -10
View File
@@ -622,15 +622,6 @@ export function normalizeBaseUrl(url: string): string {
.replace(/\/(?:chat\/completions|completions|messages|responses)$/, "")
}
/**
* Ollama's native API root, which the SDK appends /chat to and the model
* list /tags. Users often enter the server address ("http://localhost:11434")
* or its OpenAI-compatible one (".../v1"); both get /api.
*/
export function ollamaApiUrl(baseUrl: string): string {
return `${normalizeBaseUrl(baseUrl).replace(/\/(?:api|v1)$/, "")}/api`
}
/** Where a chat request goes for a base URL, or null when the SDK decides */
export function chatRequestUrl(
provider: ProviderName,
@@ -639,7 +630,6 @@ export function chatRequestUrl(
const url = normalizeBaseUrl(baseUrl)
if (!url) return null
if (provider === "anthropic") return `${url}/messages`
if (provider === "ollama") return `${ollamaApiUrl(url)}/chat`
// These SDKs build their own paths (or, for MiniMax, pick the protocol
// from the URL)
const ownPaths: ProviderName[] = [
@@ -647,6 +637,7 @@ export function chatRequestUrl(
"vertexai",
"azure",
"bedrock",
"ollama",
"gateway",
"minimax",
"edgeone",
+1308 -466
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -67,6 +67,7 @@
"@radix-ui/react-use-controllable-state": "^1.2.2",
"@xmldom/xmldom": "^0.9.8",
"ai": "^6.0.300",
"base-64": "^1.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
+4 -6
View File
@@ -154,9 +154,9 @@ describe("isSettingsWritable", () => {
expect(isSettingsWritable()).toBe(true)
})
it("returns false when the settings path is a directory", () => {
it("returns false for an unwritable path", () => {
_resetForTests()
process.env.SETTINGS_FILE = tmpDir
process.env.SETTINGS_FILE = "/nonexistent-root-dir/settings.json"
expect(isSettingsWritable()).toBe(false)
})
})
@@ -170,9 +170,7 @@ describe("settings file on disk", () => {
version: 1,
values: { TEST_ADMIN_VAR: "secret" },
})
if (process.platform !== "win32") {
const mode = fs.statSync(filePath).mode & 0o777
expect(mode).toBe(0o600)
}
const mode = fs.statSync(filePath).mode & 0o777
expect(mode).toBe(0o600)
})
})
+3 -20
View File
@@ -434,7 +434,7 @@ describe("Ollama API key security", () => {
expect(createOllamaMock).toHaveBeenCalledWith(
expect.objectContaining({
baseURL: "https://my-ollama.com/api",
baseURL: "https://my-ollama.com",
headers: { Authorization: "Bearer client-key" },
}),
)
@@ -448,7 +448,7 @@ describe("Ollama API key security", () => {
expect(createOllamaMock).toHaveBeenCalledWith(
expect.objectContaining({
baseURL: "https://cloud.ollama.com/api",
baseURL: "https://cloud.ollama.com",
headers: { Authorization: "Bearer server-key" },
}),
)
@@ -483,24 +483,7 @@ describe("Ollama API key security", () => {
expect(createOllamaMock).toHaveBeenCalledTimes(1)
const callArgs = createOllamaMock.mock.calls[0][0]
expect(callArgs.baseURL).toBe("https://my-ollama.com/api")
expect(callArgs.baseURL).toBe("https://my-ollama.com")
expect(callArgs).not.toHaveProperty("headers")
})
it("sends chat to Ollama's /api for a server address or a /v1 URL", () => {
delete process.env.OLLAMA_API_KEY
for (const baseUrl of [
"http://localhost:11434",
"http://localhost:11434/",
"http://localhost:11434/v1",
"http://localhost:11434/api",
]) {
createOllamaMock.mockClear()
getAIModel({ provider: "ollama", baseUrl, modelId: "llama3.2" })
expect(createOllamaMock.mock.calls[0][0].baseURL).toBe(
"http://localhost:11434/api",
)
}
})
})
+1 -26
View File
@@ -1,9 +1,5 @@
import { describe, expect, it } from "vitest"
import {
chatRequestUrl,
normalizeBaseUrl,
ollamaApiUrl,
} from "@/lib/types/model-config"
import { chatRequestUrl, normalizeBaseUrl } from "@/lib/types/model-config"
describe("normalizeBaseUrl", () => {
it("drops spaces, trailing slashes and a pasted endpoint path", () => {
@@ -36,9 +32,6 @@ describe("chatRequestUrl", () => {
expect(
chatRequestUrl("anthropic", "https://proxy.example.com/v1"),
).toBe("https://proxy.example.com/v1/messages")
expect(chatRequestUrl("ollama", "http://localhost:11434")).toBe(
"http://localhost:11434/api/chat",
)
})
it("stays out of the way for SDKs that build their own paths", () => {
@@ -47,21 +40,3 @@ describe("chatRequestUrl", () => {
expect(chatRequestUrl("glm", " ")).toBeNull()
})
})
describe("ollamaApiUrl", () => {
it("points at Ollama's /api whatever form the address takes", () => {
for (const url of [
"http://localhost:11434",
"http://localhost:11434/",
"http://localhost:11434/api",
"http://localhost:11434/api/",
"http://localhost:11434/v1",
"http://localhost:11434/v1/chat/completions",
]) {
expect(ollamaApiUrl(url)).toBe("http://localhost:11434/api")
}
expect(ollamaApiUrl("https://ollama.com/api")).toBe(
"https://ollama.com/api",
)
})
})