Compare commits

..

1 Commits

Author SHA1 Message Date
dayuan.jiang
e598891322 fix: detect models that don't support image input and return clear error
Some models (Kimi K2, DeepSeek, Qwen text models) don't support image/vision
input. The AI SDK silently drops unsupported image parts, causing confusing
responses where the model acts as if no image was uploaded.

Added supportsImageInput() function to detect unsupported models by name,
and return a 400 error with clear guidance when users try to upload images
to these models.

Closes #469
2025-12-31 12:14:37 +09:00
10 changed files with 843 additions and 839 deletions

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",

View File

@@ -283,7 +283,7 @@ export function ChatMessageDisplay({
try {
await navigator.clipboard.writeText(text)
setCopyState(messageId, isToolCall, true)
} catch (_err) {
} catch (err) {
// Fallback for non-secure contexts (HTTP) or permission denied
const textarea = document.createElement("textarea")
textarea.value = text

View File

@@ -21,6 +21,7 @@ import {
Zap,
} from "lucide-react"
import { useCallback, useEffect, useRef, useState } from "react"
import { toast } from "sonner"
import {
AlertDialog,
AlertDialogAction,
@@ -516,6 +517,7 @@ export function ModelConfigDialog({
{/* Provider Details (Right Panel) */}
<div className="flex-1 min-w-0 flex flex-col overflow-auto [&::-webkit-scrollbar]:hidden ">
{selectedProvider ? (
<>
<ScrollArea className="flex-1" ref={scrollRef}>
<div className="p-6 space-y-8">
{/* Provider Header */}
@@ -557,7 +559,10 @@ export function ModelConfigDialog({
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-success-muted text-success">
<Check className="h-3.5 w-3.5 animate-check-pop" />
<span className="text-xs font-medium">
{dict.modelConfig.verified}
{
dict.modelConfig
.verified
}
</span>
</div>
)}
@@ -570,13 +575,18 @@ export function ModelConfigDialog({
className="text-destructive hover:text-destructive hover:bg-destructive/10"
>
<Trash2 className="h-4 w-4 mr-1.5" />
{dict.modelConfig.deleteProvider}
{
dict.modelConfig
.deleteProvider
}
</Button>
</div>
{/* Configuration Section */}
<ConfigSection
title={dict.modelConfig.configuration}
title={
dict.modelConfig.configuration
}
icon={Settings2}
>
<ConfigCard>
@@ -626,7 +636,8 @@ export function ModelConfigDialog({
>
<Key className="h-3.5 w-3.5 text-muted-foreground" />
{
dict.modelConfig
dict
.modelConfig
.awsAccessKeyId
}
</Label>
@@ -661,7 +672,8 @@ export function ModelConfigDialog({
>
<Key className="h-3.5 w-3.5 text-muted-foreground" />
{
dict.modelConfig
dict
.modelConfig
.awsSecretAccessKey
}
</Label>
@@ -677,10 +689,13 @@ export function ModelConfigDialog({
selectedProvider.awsSecretAccessKey ||
""
}
onChange={(e) =>
onChange={(
e,
) =>
handleProviderUpdate(
"awsSecretAccessKey",
e.target
e
.target
.value,
)
}
@@ -722,7 +737,8 @@ export function ModelConfigDialog({
>
<Link2 className="h-3.5 w-3.5 text-muted-foreground" />
{
dict.modelConfig
dict
.modelConfig
.awsRegion
}
</Label>
@@ -801,7 +817,8 @@ export function ModelConfigDialog({
</SelectItem>
<SelectItem value="sa-east-1">
sa-east-1
(São Paulo)
(São
Paulo)
</SelectItem>
</SelectContent>
</Select>
@@ -848,7 +865,8 @@ export function ModelConfigDialog({
}
</>
) : (
dict.modelConfig
dict
.modelConfig
.test
)}
</Button>
@@ -904,7 +922,8 @@ export function ModelConfigDialog({
}
</>
) : (
dict.modelConfig
dict
.modelConfig
.test
)}
</Button>
@@ -930,7 +949,8 @@ export function ModelConfigDialog({
>
<Key className="h-3.5 w-3.5 text-muted-foreground" />
{
dict.modelConfig
dict
.modelConfig
.apiKey
}
</Label>
@@ -1047,7 +1067,8 @@ export function ModelConfigDialog({
>
<Link2 className="h-3.5 w-3.5 text-muted-foreground" />
{
dict.modelConfig
dict
.modelConfig
.baseUrl
}
<span className="text-muted-foreground font-normal">
@@ -1077,7 +1098,8 @@ export function ModelConfigDialog({
.provider
]
.defaultBaseUrl ||
dict.modelConfig
dict
.modelConfig
.customEndpoint
}
className="h-9 rounded-xl font-mono text-xs"
@@ -1100,10 +1122,13 @@ export function ModelConfigDialog({
dict.modelConfig
.customModelId
}
value={customModelInput}
value={
customModelInput
}
onChange={(e) => {
setCustomModelInput(
e.target.value,
e.target
.value,
)
if (
duplicateError
@@ -1123,7 +1148,9 @@ export function ModelConfigDialog({
handleAddModel(
customModelInput.trim(),
)
if (success) {
if (
success
) {
setCustomModelInput(
"",
)
@@ -1168,7 +1195,9 @@ export function ModelConfigDialog({
<Plus className="h-3.5 w-3.5" />
</Button>
<Select
onValueChange={(value) => {
onValueChange={(
value,
) => {
if (value) {
handleAddModel(
value,
@@ -1204,7 +1233,9 @@ export function ModelConfigDialog({
}
className="font-mono text-xs"
>
{modelId}
{
modelId
}
</SelectItem>
),
)}
@@ -1215,8 +1246,8 @@ export function ModelConfigDialog({
>
{/* Model List */}
<div className="rounded-2xl border border-border-subtle bg-surface-2/30 overflow-hidden min-h-[120px]">
{selectedProvider.models.length ===
0 ? (
{selectedProvider.models
.length === 0 ? (
<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">
<Sparkles className="h-5 w-5 text-muted-foreground" />
@@ -1233,7 +1264,9 @@ export function ModelConfigDialog({
{selectedProvider.models.map(
(model, index) => (
<div
key={model.id}
key={
model.id
}
className={cn(
"transition-colors duration-150 hover:bg-interactive-hover/50",
)}
@@ -1467,6 +1500,7 @@ export function ModelConfigDialog({
</ConfigSection>
</div>
</ScrollArea>
</>
) : (
<div className="h-full flex flex-col items-center justify-center p-8 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-surface-2 mb-4">

View File

@@ -11,6 +11,7 @@ import {
flattenModels,
type ModelConfig,
type MultiModelConfig,
PROVIDER_INFO,
type ProviderConfig,
type ProviderName,
} from "@/lib/types/model-config"

View File

@@ -786,7 +786,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
`data: ${JSON.stringify(data)}\n\n`,
),
)
} catch (_e) {
} catch (e) {
// If parsing fails, forward the original message to avoid breaking the stream.
controller.enqueue(
new TextEncoder().encode(

View File

@@ -1,18 +1,18 @@
{
"name": "@next-ai-drawio/mcp-server",
"version": "0.1.11",
"version": "0.1.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@next-ai-drawio/mcp-server",
"version": "0.1.11",
"version": "0.1.6",
"license": "Apache-2.0",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4",
"linkedom": "^0.18.0",
"open": "^11.0.0",
"zod": "^4.0.0"
"zod": "^3.24.0"
},
"bin": {
"next-ai-drawio-mcp": "dist/index.js"
@@ -2051,9 +2051,9 @@
}
},
"node_modules/zod": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.4.tgz",
"integrity": "sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A==",
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {

View File

@@ -1,6 +1,6 @@
{
"name": "@next-ai-drawio/mcp-server",
"version": "0.1.11",
"version": "0.1.10",
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
"type": "module",
"main": "dist/index.js",
@@ -39,7 +39,7 @@
"@modelcontextprotocol/sdk": "^1.0.4",
"linkedom": "^0.18.0",
"open": "^11.0.0",
"zod": "^4.0.0"
"zod": "^3.24.0"
},
"devDependencies": {
"@types/node": "^24.0.0",

View File

@@ -127,12 +127,7 @@ function cleanupExpiredSessions(): void {
}
}
const cleanupIntervalId = setInterval(cleanupExpiredSessions, 5 * 60 * 1000)
export function shutdown(): void {
clearInterval(cleanupIntervalId)
stopHttpServer()
}
setInterval(cleanupExpiredSessions, 5 * 60 * 1000)
export function getServerPort(): number {
return serverPort

View File

@@ -39,7 +39,6 @@ import {
getState,
requestSync,
setState,
shutdown,
startHttpServer,
waitForSync,
} from "./http-server.js"
@@ -48,7 +47,7 @@ import { validateAndFixXml } from "./xml-validation.js"
// Server configuration
const config = {
port: parseInt(process.env.PORT || "6002", 10),
port: parseInt(process.env.PORT || "6002"),
}
// Session state (single session for simplicity)
@@ -619,31 +618,6 @@ server.registerTool(
},
)
// Graceful shutdown handler
let isShuttingDown = false
function gracefulShutdown(reason: string) {
if (isShuttingDown) return
isShuttingDown = true
log.info(`Shutting down: ${reason}`)
shutdown()
process.exit(0)
}
// Handle stdin close (primary method - works on all platforms including Windows)
process.stdin.on("close", () => gracefulShutdown("stdin closed"))
process.stdin.on("end", () => gracefulShutdown("stdin ended"))
// Handle signals (may not work reliably on Windows)
process.on("SIGINT", () => gracefulShutdown("SIGINT"))
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"))
// Handle broken pipe (writing to closed stdout)
process.stdout.on("error", (err) => {
if (err.code === "EPIPE" || err.code === "ERR_STREAM_DESTROYED") {
gracefulShutdown("stdout error")
}
})
// Start the MCP server
async function main() {
log.info("Starting MCP server for Next AI Draw.io (embedded mode)...")

View File

@@ -253,7 +253,7 @@ async function main() {
},
)
console.log("👀 Watching for preset configuration changes...")
} catch (_err) {
} catch (err) {
// File might not exist yet, that's ok
setTimeout(setupConfigWatcher, 5000)
}