Compare commits

..

4 Commits

Author SHA1 Message Date
dayuan.jiang
a0616c1ad8 fix: save session when diagram is modified via autosave
Add chartXML to dependency array of auto-save effect so that user
modifications in draw.io (captured via autosave) trigger session save.
2026-01-13 23:16:45 +09:00
dayuan.jiang
e271df43f8 fix: disable beforeunload warning from draw.io iframe
Set modified=false in urlParameters to prevent draw.io from triggering
the browser's 'Changes you made may not be saved' dialog on page refresh.
2026-01-13 23:15:21 +09:00
dayuan.jiang
38a247c55d refactor: use EventAutoSave type for type safety 2026-01-13 23:02:19 +09:00
dayuan.jiang
a0e3130ac2 fix: enable autosave to sync user modifications in draw.io
Previously, user modifications made directly in draw.io (moving nodes,
editing text, etc.) were not captured by React state until an explicit
export was triggered.

This adds the autosave feature from react-drawio:
- Enable autosave={true} on DrawIoEmbed component
- Add onAutoSave callback to update chartXML on every change
- Keep React state in sync with draw.io editor state
2026-01-13 22:54:02 +09:00
34 changed files with 151 additions and 1641 deletions

3
.gitignore vendored
View File

@@ -68,5 +68,4 @@ CLAUDE.md
# edgeone # edgeone
.edgeone .edgeone
opencode.json opencode.json
ai-models.json

View File

@@ -207,7 +207,6 @@ See the [Next.js deployment documentation](https://nextjs.org/docs/app/building-
- OpenAI - OpenAI
- Anthropic - Anthropic
- Google AI - Google AI
- Google Vertex AI
- Azure OpenAI - Azure OpenAI
- Ollama - Ollama
- OpenRouter - OpenRouter
@@ -222,10 +221,6 @@ All providers except AWS Bedrock and OpenRouter support custom endpoints.
📖 **[Detailed Provider Configuration Guide](./docs/en/ai-providers.md)** - See setup instructions for each provider. 📖 **[Detailed Provider Configuration Guide](./docs/en/ai-providers.md)** - See setup instructions for each provider.
### 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.
**Model Requirements**: This task requires strong model capabilities for generating long-form text with strict formatting constraints (draw.io XML). Recommended models include Claude Sonnet 4.5, GPT-5.1, Gemini 3 Pro, and DeepSeek V3.2/R1. **Model Requirements**: This task requires strong model capabilities for generating long-form text with strict formatting constraints (draw.io XML). Recommended models include Claude Sonnet 4.5, GPT-5.1, Gemini 3 Pro, and DeepSeek V3.2/R1.
Note that the `claude` series has been trained on draw.io diagrams with cloud architecture logos like AWS, Azure, GCP. So if you want to create cloud architecture diagrams, this is the best choice. Note that the `claude` series has been trained on draw.io diagrams with cloud architecture logos like AWS, Azure, GCP. So if you want to create cloud architecture diagrams, this is the best choice.

View File

@@ -292,7 +292,6 @@ export default function AboutCN() {
</li> </li>
<li>Anthropic</li> <li>Anthropic</li>
<li>Google AI</li> <li>Google AI</li>
<li>Google Vertex AI</li>
<li>Azure OpenAI</li> <li>Azure OpenAI</li>
<li>Ollama</li> <li>Ollama</li>
<li>OpenRouter</li> <li>OpenRouter</li>

View File

@@ -307,7 +307,6 @@ export default function AboutJA() {
</li> </li>
<li>Anthropic</li> <li>Anthropic</li>
<li>Google AI</li> <li>Google AI</li>
<li>Google Vertex AI</li>
<li>Azure OpenAI</li> <li>Azure OpenAI</li>
<li>Ollama</li> <li>Ollama</li>
<li>OpenRouter</li> <li>OpenRouter</li>

View File

@@ -326,7 +326,6 @@ export default function About() {
</li> </li>
<li>Anthropic</li> <li>Anthropic</li>
<li>Google AI</li> <li>Google AI</li>
<li>Google Vertex AI</li>
<li>Azure OpenAI</li> <li>Azure OpenAI</li>
<li>Ollama</li> <li>Ollama</li>
<li>OpenRouter</li> <li>OpenRouter</li>

View File

@@ -16,8 +16,13 @@ const drawioBaseUrl =
process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net" process.env.NEXT_PUBLIC_DRAWIO_BASE_URL || "https://embed.diagrams.net"
export default function Home() { export default function Home() {
const { drawioRef, handleDiagramExport, onDrawioLoad, resetDrawioReady } = const {
useDiagram() drawioRef,
handleDiagramExport,
handleAutoSave,
onDrawioLoad,
resetDrawioReady,
} = useDiagram()
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
// Extract current language from pathname (e.g., "/zh/about" → "zh") // Extract current language from pathname (e.g., "/zh/about" → "zh")
@@ -164,6 +169,8 @@ export default function Home() {
ref={drawioRef} ref={drawioRef}
onExport={handleDiagramExport} onExport={handleDiagramExport}
onLoad={handleDrawioLoad} onLoad={handleDrawioLoad}
onAutoSave={handleAutoSave}
autosave={true}
baseUrl={drawioBaseUrl} baseUrl={drawioBaseUrl}
urlParameters={{ urlParameters={{
ui: drawioUi, ui: drawioUi,
@@ -172,6 +179,7 @@ export default function Home() {
saveAndExit: false, saveAndExit: false,
noSaveBtn: true, noSaveBtn: true,
noExitBtn: true, noExitBtn: true,
modified: false,
dark: darkMode, dark: darkMode,
lang: currentLang, lang: currentLang,
}} }}

View File

@@ -34,7 +34,6 @@ import {
setTraceOutput, setTraceOutput,
wrapWithObserve, wrapWithObserve,
} from "@/lib/langfuse" } from "@/lib/langfuse"
import { findServerModelById } from "@/lib/server-model-config"
import { getSystemPrompt } from "@/lib/system-prompts" import { getSystemPrompt } from "@/lib/system-prompts"
import { getUserIdFromRequest } from "@/lib/user-id" import { getUserIdFromRequest } from "@/lib/user-id"
@@ -169,7 +168,6 @@ async function handleChatRequest(req: Request): Promise<Response> {
// Read client AI provider overrides from headers // Read client AI provider overrides from headers
const provider = req.headers.get("x-ai-provider") const provider = req.headers.get("x-ai-provider")
let baseUrl = req.headers.get("x-ai-base-url") let baseUrl = req.headers.get("x-ai-base-url")
const selectedModelId = req.headers.get("x-selected-model-id")
// For EdgeOne provider, construct full URL from request origin // For EdgeOne provider, construct full URL from request origin
// because createOpenAI needs absolute URL, not relative path // because createOpenAI needs absolute URL, not relative path
@@ -181,30 +179,8 @@ async function handleChatRequest(req: Request): Promise<Response> {
// Get cookie header for EdgeOne authentication (eo_token, eo_time) // Get cookie header for EdgeOne authentication (eo_token, eo_time)
const cookieHeader = req.headers.get("cookie") const cookieHeader = req.headers.get("cookie")
// Check if this is a server model with custom env var names
let serverModelConfig: {
apiKeyEnv?: string
baseUrlEnv?: string
provider?: string
} = {}
if (selectedModelId?.startsWith("server:")) {
const serverModel = await findServerModelById(selectedModelId)
console.log(
`[Server Model Lookup] ID: ${selectedModelId}, Found: ${!!serverModel}, Provider: ${serverModel?.provider}`,
)
if (serverModel) {
serverModelConfig = {
apiKeyEnv: serverModel.apiKeyEnv,
baseUrlEnv: serverModel.baseUrlEnv,
// Use actual provider from config (client header may have incorrect value due to ID format change)
provider: serverModel.provider,
}
}
}
const clientOverrides = { const clientOverrides = {
// Server model provider takes precedence over client header provider,
provider: serverModelConfig.provider || provider,
baseUrl, baseUrl,
apiKey: req.headers.get("x-ai-api-key"), apiKey: req.headers.get("x-ai-api-key"),
modelId: req.headers.get("x-ai-model"), modelId: req.headers.get("x-ai-model"),
@@ -213,10 +189,6 @@ async function handleChatRequest(req: Request): Promise<Response> {
awsSecretAccessKey: req.headers.get("x-aws-secret-access-key"), awsSecretAccessKey: req.headers.get("x-aws-secret-access-key"),
awsRegion: req.headers.get("x-aws-region"), awsRegion: req.headers.get("x-aws-region"),
awsSessionToken: req.headers.get("x-aws-session-token"), awsSessionToken: req.headers.get("x-aws-session-token"),
// Server model custom env var names
...serverModelConfig,
// Vertex AI credentials (Express Mode)
vertexApiKey: req.headers.get("x-vertex-api-key"),
// Pass cookies for EdgeOne Pages authentication // Pass cookies for EdgeOne Pages authentication
...(provider === "edgeone" && ...(provider === "edgeone" &&
cookieHeader && { cookieHeader && {
@@ -227,10 +199,6 @@ async function handleChatRequest(req: Request): Promise<Response> {
// Read minimal style preference from header // Read minimal style preference from header
const minimalStyle = req.headers.get("x-minimal-style") === "true" const minimalStyle = req.headers.get("x-minimal-style") === "true"
console.log(
`[Client Overrides] provider: ${clientOverrides.provider}, modelId: ${clientOverrides.modelId}`,
)
// Get AI model with optional client overrides // Get AI model with optional client overrides
const { model, providerOptions, headers, modelId } = const { model, providerOptions, headers, modelId } =
getAIModel(clientOverrides) getAIModel(clientOverrides)

View File

@@ -1,14 +0,0 @@
import { NextResponse } from "next/server"
import { loadFlattenedServerModels } from "@/lib/server-model-config"
// Use dynamic rendering to read AI_MODEL/AI_PROVIDER env vars at runtime
// This ensures Docker users can set these values when starting containers
export const dynamic = "force-dynamic"
export async function GET() {
const models = await loadFlattenedServerModels()
return NextResponse.json({
models,
hasConfig: models.length > 0,
})
}

View File

@@ -3,7 +3,6 @@ import { createAnthropic } from "@ai-sdk/anthropic"
import { createDeepSeek, deepseek } from "@ai-sdk/deepseek" import { createDeepSeek, deepseek } from "@ai-sdk/deepseek"
import { createGateway } from "@ai-sdk/gateway" 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 { createOpenAI } from "@ai-sdk/openai" import { createOpenAI } from "@ai-sdk/openai"
import { createOpenRouter } from "@openrouter/ai-sdk-provider" import { createOpenRouter } from "@openrouter/ai-sdk-provider"
import { generateText } from "ai" import { generateText } from "ai"
@@ -81,8 +80,6 @@ interface ValidateRequest {
awsAccessKeyId?: string awsAccessKeyId?: string
awsSecretAccessKey?: string awsSecretAccessKey?: string
awsRegion?: string awsRegion?: string
// Vertex AI specific
vertexApiKey?: string // Express Mode API key
} }
export async function POST(req: Request) { export async function POST(req: Request) {
@@ -96,8 +93,6 @@ export async function POST(req: Request) {
awsAccessKeyId, awsAccessKeyId,
awsSecretAccessKey, awsSecretAccessKey,
awsRegion, awsRegion,
// Note: Express Mode only needs vertexApiKey
vertexApiKey,
} = body } = body
if (!provider || !modelId) { if (!provider || !modelId) {
@@ -126,16 +121,6 @@ export async function POST(req: Request) {
{ status: 400 }, { status: 400 },
) )
} }
} else if (provider === "vertexai") {
if (!vertexApiKey) {
return NextResponse.json(
{
valid: false,
error: "Vertex AI API key is required for Express Mode",
},
{ status: 400 },
)
}
} else if (provider !== "ollama" && provider !== "edgeone" && !apiKey) { } else if (provider !== "ollama" && provider !== "edgeone" && !apiKey) {
return NextResponse.json( return NextResponse.json(
{ valid: false, error: "API key is required" }, { valid: false, error: "API key is required" },
@@ -173,15 +158,6 @@ export async function POST(req: Request) {
break break
} }
case "vertexai": {
const vertex = createVertex({
apiKey: vertexApiKey,
...(baseUrl && { baseURL: baseUrl }),
})
model = vertex(modelId)
break
}
case "azure": { case "azure": {
const azure = createOpenAI({ const azure = createOpenAI({
apiKey, apiKey,

View File

@@ -169,27 +169,3 @@ export const ModelSelectorName = ({
}: ModelSelectorNameProps) => ( }: ModelSelectorNameProps) => (
<span className={cn("flex-1 truncate text-left", className)} {...props} /> <span className={cn("flex-1 truncate text-left", className)} {...props} />
) )
export type ModelSelectorSectionHeaderProps = {
icon: ReactNode
label: string
className?: string
}
export const ModelSelectorSectionHeader = ({
icon,
label,
className,
}: ModelSelectorSectionHeaderProps) => (
<div
className={cn(
"flex items-center gap-2 px-2 py-1.5 text-xs font-semibold text-muted-foreground bg-muted/40 rounded-sm mx-1 mt-1",
className,
)}
>
<span className="[&>svg]:size-3.5" aria-hidden="true">
{icon}
</span>
<span>{label}</span>
</div>
)

View File

@@ -27,7 +27,6 @@ import { isPdfFile, isTextFile } from "@/lib/pdf-utils"
import { STORAGE_KEYS } from "@/lib/storage" import { STORAGE_KEYS } from "@/lib/storage"
import type { FlattenedModel } from "@/lib/types/model-config" import type { FlattenedModel } from "@/lib/types/model-config"
import { extractUrlContent, type UrlData } from "@/lib/url-utils" import { extractUrlContent, type UrlData } from "@/lib/url-utils"
import { isRealDiagram } from "@/lib/utils"
import { FilePreviewList } from "./file-preview-list" import { FilePreviewList } from "./file-preview-list"
const MAX_IMAGE_SIZE = 2 * 1024 * 1024 // 2MB const MAX_IMAGE_SIZE = 2 * 1024 * 1024 // 2MB
@@ -160,9 +159,6 @@ interface ChatInputProps {
onModelSelect?: (modelId: string | undefined) => void onModelSelect?: (modelId: string | undefined) => void
showUnvalidatedModels?: boolean showUnvalidatedModels?: boolean
onConfigureModels?: () => void onConfigureModels?: () => void
// Focus control props
shouldFocus?: boolean
onFocused?: () => void
} }
export function ChatInput({ export function ChatInput({
@@ -182,12 +178,9 @@ export function ChatInput({
onModelSelect = () => {}, onModelSelect = () => {},
showUnvalidatedModels = false, showUnvalidatedModels = false,
onConfigureModels = () => {}, onConfigureModels = () => {},
shouldFocus = false,
onFocused,
}: ChatInputProps) { }: ChatInputProps) {
const dict = useDictionary() const dict = useDictionary()
const { const {
chartXML,
diagramHistory, diagramHistory,
saveDiagramToFile, saveDiagramToFile,
showSaveDialog, showSaveDialog,
@@ -197,19 +190,6 @@ export function ChatInput({
const textareaRef = useRef<HTMLTextAreaElement>(null) const textareaRef = useRef<HTMLTextAreaElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const [isDragging, setIsDragging] = useState(false) const [isDragging, setIsDragging] = useState(false)
// Focus the textarea when shouldFocus becomes true
// Use setTimeout to ensure focus happens after drawio iframe settles
useEffect(() => {
if (shouldFocus) {
const timer = setTimeout(() => {
textareaRef.current?.focus()
onFocused?.()
}, 150)
return () => clearTimeout(timer)
}
}, [shouldFocus, onFocused])
const [showHistory, setShowHistory] = useState(false) const [showHistory, setShowHistory] = useState(false)
const [showUrlDialog, setShowUrlDialog] = useState(false) const [showUrlDialog, setShowUrlDialog] = useState(false)
const [isExtractingUrl, setIsExtractingUrl] = useState(false) const [isExtractingUrl, setIsExtractingUrl] = useState(false)
@@ -474,7 +454,7 @@ export function ChatInput({
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => setShowSaveDialog(true)} onClick={() => setShowSaveDialog(true)}
disabled={isDisabled || !isRealDiagram(chartXML)} disabled={isDisabled}
tooltipContent={dict.chat.saveDiagram} tooltipContent={dict.chat.saveDiagram}
className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground" className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground"
> >

View File

@@ -173,7 +173,6 @@ export default function ChatPanel({
const [dailyTokenLimit, setDailyTokenLimit] = useState(0) const [dailyTokenLimit, setDailyTokenLimit] = useState(0)
const [tpmLimit, setTpmLimit] = useState(0) const [tpmLimit, setTpmLimit] = useState(0)
const [minimalStyle, setMinimalStyle] = useState(false) const [minimalStyle, setMinimalStyle] = useState(false)
const [shouldFocusInput, setShouldFocusInput] = useState(false)
// Restore input from sessionStorage on mount (when ChatPanel remounts due to key change) // Restore input from sessionStorage on mount (when ChatPanel remounts due to key change)
useEffect(() => { useEffect(() => {
@@ -640,6 +639,7 @@ export default function ChatPanel({
sessionIsAvailable, sessionIsAvailable,
currentSessionId, currentSessionId,
buildSessionData, buildSessionData,
chartXML,
]) ])
// Update URL when a new session is created (first message sent) // Update URL when a new session is created (first message sent)
@@ -857,7 +857,6 @@ export default function ChatPanel({
// Clear UI state (can't use syncUIWithSession here because we also need to clear files) // Clear UI state (can't use syncUIWithSession here because we also need to clear files)
setMessages([]) setMessages([])
setInput("")
clearDiagram() clearDiagram()
setDiagramHistory([]) setDiagramHistory([])
handleFileChange([]) // Use handleFileChange to also clear pdfData handleFileChange([]) // Use handleFileChange to also clear pdfData
@@ -872,9 +871,6 @@ export default function ChatPanel({
// Clear URL param to show blank state // Clear URL param to show blank state
router.replace(window.location.pathname, { scroll: false }) router.replace(window.location.pathname, { scroll: false })
// After starting a fresh chat, move focus back to the chat input
setShouldFocusInput(true)
}, [ }, [
clearDiagram, clearDiagram,
handleFileChange, handleFileChange,
@@ -968,14 +964,6 @@ export default function ChatPanel({
...(config.awsSessionToken && { ...(config.awsSessionToken && {
"x-aws-session-token": config.awsSessionToken, "x-aws-session-token": config.awsSessionToken,
}), }),
// Vertex AI credentials (Express Mode)
...(config.vertexApiKey && {
"x-vertex-api-key": config.vertexApiKey,
}),
}),
// Send selected model ID for server model lookup (apiKeyEnv/baseUrlEnv)
...(config.selectedModelId && {
"x-selected-model-id": config.selectedModelId,
}), }),
...(minimalStyle && { ...(minimalStyle && {
"x-minimal-style": "true", "x-minimal-style": "true",
@@ -1301,8 +1289,6 @@ export default function ChatPanel({
onModelSelect={modelConfig.setSelectedModelId} onModelSelect={modelConfig.setSelectedModelId}
showUnvalidatedModels={modelConfig.showUnvalidatedModels} showUnvalidatedModels={modelConfig.showUnvalidatedModels}
onConfigureModels={() => setShowModelConfigDialog(true)} onConfigureModels={() => setShowModelConfigDialog(true)}
shouldFocus={shouldFocusInput}
onFocused={() => setShouldFocusInput(false)}
/> />
</footer> </footer>

View File

@@ -78,7 +78,6 @@ const PROVIDER_LOGO_MAP: Record<string, string> = {
sglang: "openai", // SGLang is OpenAI-compatible sglang: "openai", // SGLang is OpenAI-compatible
gateway: "vercel", gateway: "vercel",
edgeone: "tencent-cloud", edgeone: "tencent-cloud",
vertexai: "google",
doubao: "bytedance", doubao: "bytedance",
modelscope: "modelscope", modelscope: "modelscope",
} }
@@ -238,7 +237,6 @@ export function ModelConfigDialog({
"awsAccessKeyId", "awsAccessKeyId",
"awsSecretAccessKey", "awsSecretAccessKey",
"awsRegion", "awsRegion",
"vertexApiKey",
] ]
if (credentialFields.includes(field)) { if (credentialFields.includes(field)) {
setValidationStatus("idle") setValidationStatus("idle")
@@ -282,7 +280,6 @@ export function ModelConfigDialog({
// Check credentials based on provider type // Check credentials based on provider type
const isBedrock = selectedProvider.provider === "bedrock" const isBedrock = selectedProvider.provider === "bedrock"
const isEdgeOne = selectedProvider.provider === "edgeone" const isEdgeOne = selectedProvider.provider === "edgeone"
const isVertexAI = selectedProvider.provider === "vertexai"
if (isBedrock) { if (isBedrock) {
if ( if (
!selectedProvider.awsAccessKeyId || !selectedProvider.awsAccessKeyId ||
@@ -291,11 +288,6 @@ export function ModelConfigDialog({
) { ) {
return return
} }
} else if (isVertexAI) {
// Vertex AI requires vertexApiKey for Express Mode
if (!selectedProvider.vertexApiKey) {
return
}
} else if (!isEdgeOne && !selectedProvider.apiKey) { } else if (!isEdgeOne && !selectedProvider.apiKey) {
return return
} }
@@ -336,8 +328,6 @@ export function ModelConfigDialog({
awsAccessKeyId: selectedProvider.awsAccessKeyId, awsAccessKeyId: selectedProvider.awsAccessKeyId,
awsSecretAccessKey: selectedProvider.awsSecretAccessKey, awsSecretAccessKey: selectedProvider.awsSecretAccessKey,
awsRegion: selectedProvider.awsRegion, awsRegion: selectedProvider.awsRegion,
// Vertex AI credentials (Express Mode)
vertexApiKey: selectedProvider.vertexApiKey,
}), }),
}) })
const data = await response.json() const data = await response.json()
@@ -877,153 +867,7 @@ export function ModelConfigDialog({
</div> </div>
</> </>
) : selectedProvider.provider === ) : selectedProvider.provider ===
"vertexai" ? ( "edgeone" ? (
<>
{/* Vertex AI API Key */}
<div className="space-y-2">
<Label
htmlFor="vertex-api-key"
className="text-xs font-medium flex items-center gap-1.5"
>
<Key className="h-3.5 w-3.5 text-muted-foreground" />
API Key
</Label>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="vertex-api-key"
type={
showApiKey
? "text"
: "password"
}
value={
selectedProvider.vertexApiKey ||
""
}
onChange={(
e,
) =>
handleProviderUpdate(
"vertexApiKey",
e
.target
.value,
)
}
placeholder="Enter your Vertex AI API key"
className="h-9 pr-10 font-mono text-xs"
/>
<button
type="button"
onClick={() =>
setShowApiKey(
!showApiKey,
)
}
aria-label={
showApiKey
? "Hide API key"
: "Show API key"
}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded"
>
{showApiKey ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
</div>
<Button
variant={
validationStatus ===
"success"
? "outline"
: "default"
}
size="sm"
onClick={
handleValidate
}
disabled={
!selectedProvider.vertexApiKey ||
validationStatus ===
"validating"
}
className={cn(
"h-9 px-4",
validationStatus ===
"success" &&
"text-success border-success/30 bg-success-muted hover:bg-success-muted",
)}
>
{validationStatus ===
"validating" ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : validationStatus ===
"success" ? (
<>
<Check className="h-4 w-4 mr-1.5 animate-check-pop" />
{
dict
.modelConfig
.verified
}
</>
) : (
dict
.modelConfig
.test
)}
</Button>
</div>
{validationStatus ===
"error" &&
validationError && (
<p className="text-xs text-destructive flex items-center gap-1">
<X className="h-3 w-3" />
{
validationError
}
</p>
)}
</div>
{/* Base URL (optional) */}
<div className="space-y-2">
<Label
htmlFor="vertex-base-url"
className="text-xs font-medium flex items-center gap-1.5"
>
<Link2 className="h-3.5 w-3.5 text-muted-foreground" />
Base URL{" "}
<span className="text-muted-foreground font-normal">
(optional)
</span>
</Label>
<Input
id="vertex-base-url"
value={
selectedProvider.baseUrl ||
""
}
onChange={(e) =>
handleProviderUpdate(
"baseUrl",
e.target
.value,
)
}
placeholder="Custom endpoint URL"
className="h-9 font-mono text-xs"
/>
</div>
</>
) : selectedProvider.provider ===
"ollama" ||
selectedProvider.provider ===
"edgeone" ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button

View File

@@ -5,10 +5,8 @@ import {
Bot, Bot,
Check, Check,
ChevronDown, ChevronDown,
Monitor,
Server, Server,
Settings2, Settings2,
User,
} from "lucide-react" } from "lucide-react"
import { useEffect, useMemo, useRef, useState } from "react" import { useEffect, useMemo, useRef, useState } from "react"
import { import {
@@ -21,7 +19,6 @@ import {
ModelSelectorLogo, ModelSelectorLogo,
ModelSelectorName, ModelSelectorName,
ModelSelector as ModelSelectorRoot, ModelSelector as ModelSelectorRoot,
ModelSelectorSectionHeader,
ModelSelectorSeparator, ModelSelectorSeparator,
ModelSelectorTrigger, ModelSelectorTrigger,
} from "@/components/ai-elements/model-selector" } from "@/components/ai-elements/model-selector"
@@ -52,7 +49,6 @@ const PROVIDER_LOGO_MAP: Record<string, string> = {
sglang: "openai", // SGLang is OpenAI-compatible, use OpenAI logo sglang: "openai", // SGLang is OpenAI-compatible, use OpenAI logo
gateway: "vercel", gateway: "vercel",
edgeone: "tencent-cloud", edgeone: "tencent-cloud",
vertexai: "google",
doubao: "bytedance", doubao: "bytedance",
modelscope: "modelscope", modelscope: "modelscope",
} }
@@ -66,11 +62,7 @@ function groupModelsByProvider(
{ provider: string; models: FlattenedModel[] } { provider: string; models: FlattenedModel[] }
>() >()
for (const model of models) { for (const model of models) {
// For server models, strip "Server · " prefix for cleaner grouping const key = model.providerLabel
const key =
model.source === "server"
? model.providerLabel.replace(/^Server · /, "")
: model.providerLabel
const existing = groups.get(key) const existing = groups.get(key)
if (existing) { if (existing) {
existing.models.push(model) existing.models.push(model)
@@ -98,26 +90,10 @@ export function ModelSelector({
} }
return models.filter((m) => m.validated === true) return models.filter((m) => m.validated === true)
}, [models, showUnvalidatedModels]) }, [models, showUnvalidatedModels])
const groupedModels = useMemo(
// Separate server and user models () => groupModelsByProvider(displayModels),
const serverModels = useMemo(
() => displayModels.filter((m) => m.source === "server"),
[displayModels], [displayModels],
) )
const userModels = useMemo(
() => displayModels.filter((m) => m.source !== "server"),
[displayModels],
)
// Group each category separately
const groupedServerModels = useMemo(
() => groupModelsByProvider(serverModels),
[serverModels],
)
const groupedUserModels = useMemo(
() => groupModelsByProvider(userModels),
[userModels],
)
// Find selected model for display // Find selected model for display
const selectedModel = useMemo( const selectedModel = useMemo(
@@ -184,7 +160,7 @@ export function ModelSelector({
size="sm" size="sm"
disabled={disabled} disabled={disabled}
className={cn( className={cn(
"hover:bg-accent gap-1.5 h-8 px-2 transition-[padding,background-color] duration-150 ease-in-out", "hover:bg-accent gap-1.5 h-8 px-2 transition-all duration-150 ease-in-out",
!showLabel && "px-1.5 justify-center", !showLabel && "px-1.5 justify-center",
)} )}
// accessibility: expose label to screen readers // accessibility: expose label to screen readers
@@ -221,169 +197,83 @@ export function ModelSelector({
: dict.modelConfig.noModelsFound} : dict.modelConfig.noModelsFound}
</ModelSelectorEmpty> </ModelSelectorEmpty>
{/* Server Default Option - only show when no server models are configured */} {/* Server Default Option */}
{serverModels.length === 0 && ( <ModelSelectorGroup heading={dict.modelConfig.default}>
<ModelSelectorGroup <ModelSelectorItem
heading={dict.modelConfig.default} value="__server_default__"
onSelect={handleSelect}
className={cn(
"cursor-pointer",
!selectedModelId && "bg-accent",
)}
> >
<ModelSelectorItem <Check
value="__server_default__"
onSelect={handleSelect}
className={cn( className={cn(
"cursor-pointer", "mr-2 h-4 w-4",
!selectedModelId && "bg-accent", !selectedModelId
? "opacity-100"
: "opacity-0",
)} )}
/>
<Server className="mr-2 h-4 w-4 text-muted-foreground" />
<ModelSelectorName>
{dict.modelConfig.serverDefault}
</ModelSelectorName>
</ModelSelectorItem>
</ModelSelectorGroup>
{/* Configured Models by Provider */}
{Array.from(groupedModels.entries()).map(
([
providerLabel,
{ provider, models: providerModels },
]) => (
<ModelSelectorGroup
key={providerLabel}
heading={providerLabel}
> >
<Check {providerModels.map((model) => (
className={cn( <ModelSelectorItem
"mr-2 h-4 w-4", key={model.id}
!selectedModelId value={model.modelId}
? "opacity-100" onSelect={() =>
: "opacity-0", handleSelect(model.id)
)} }
/> className="cursor-pointer"
<Server className="mr-2 h-4 w-4 text-muted-foreground" />
<ModelSelectorName>
{dict.modelConfig.serverDefault}
</ModelSelectorName>
</ModelSelectorItem>
</ModelSelectorGroup>
)}
{/* Server Models Section */}
{serverModels.length > 0 && (
<>
<ModelSelectorSectionHeader
icon={<Monitor />}
label={dict.modelConfig.serverModels}
/>
{Array.from(groupedServerModels.entries()).map(
([
providerLabel,
{ provider, models: providerModels },
]) => (
<ModelSelectorGroup
key={`server-${providerLabel}`}
heading={providerLabel}
className="[&>[cmdk-group-heading]]:pl-4"
> >
{providerModels.map((model) => ( <Check
<ModelSelectorItem className={cn(
key={model.id} "mr-2 h-4 w-4",
value={model.modelId} selectedModelId === model.id
onSelect={() => ? "opacity-100"
handleSelect(model.id) : "opacity-0",
)}
/>
<ModelSelectorLogo
provider={
PROVIDER_LOGO_MAP[
provider
] || provider
}
className="mr-2"
/>
<ModelSelectorName>
{model.modelId}
</ModelSelectorName>
{model.validated !== true && (
<span
title={
dict.modelConfig
.unvalidatedModelWarning
} }
className="cursor-pointer"
> >
<Check <AlertTriangle className="ml-auto h-3 w-3 text-warning" />
className={cn( </span>
"mr-2 h-4 w-4", )}
selectedModelId === </ModelSelectorItem>
model.id ))}
? "opacity-100" </ModelSelectorGroup>
: "opacity-0", ),
)}
/>
<ModelSelectorLogo
provider={
PROVIDER_LOGO_MAP[
provider
] || provider
}
className="mr-2"
/>
<ModelSelectorName>
{model.modelId}
</ModelSelectorName>
{model.isDefault && (
<span
title={
dict.modelConfig
.serverDefaultModel
}
className="ml-auto text-xs text-muted-foreground"
>
{
dict.modelConfig
.default
}
</span>
)}
</ModelSelectorItem>
))}
</ModelSelectorGroup>
),
)}
</>
)}
{/* User Models Section */}
{userModels.length > 0 && (
<>
{serverModels.length > 0 && (
<ModelSelectorSeparator />
)}
<ModelSelectorSectionHeader
icon={<User />}
label={dict.modelConfig.userModels}
/>
{Array.from(groupedUserModels.entries()).map(
([
providerLabel,
{ provider, models: providerModels },
]) => (
<ModelSelectorGroup
key={`user-${providerLabel}`}
heading={providerLabel}
className="[&>[cmdk-group-heading]]:pl-4"
>
{providerModels.map((model) => (
<ModelSelectorItem
key={model.id}
value={model.modelId}
onSelect={() =>
handleSelect(model.id)
}
className="cursor-pointer"
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedModelId ===
model.id
? "opacity-100"
: "opacity-0",
)}
/>
<ModelSelectorLogo
provider={
PROVIDER_LOGO_MAP[
provider
] || provider
}
className="mr-2"
/>
<ModelSelectorName>
{model.modelId}
</ModelSelectorName>
{model.validated !==
true && (
<span
title={
dict.modelConfig
.unvalidatedModelWarning
}
>
<AlertTriangle className="ml-auto h-3 w-3 text-warning" />
</span>
)}
</ModelSelectorItem>
))}
</ModelSelectorGroup>
),
)}
</>
)} )}
{/* Configure Option */} {/* Configure Option */}
@@ -392,7 +282,7 @@ export function ModelSelector({
<ModelSelectorItem <ModelSelectorItem
value="__configure__" value="__configure__"
onSelect={handleSelect} onSelect={handleSelect}
className="cursor-pointer text-muted-foreground hover:text-foreground" className="cursor-pointer"
> >
<Settings2 className="mr-2 h-4 w-4" /> <Settings2 className="mr-2 h-4 w-4" />
<ModelSelectorName> <ModelSelectorName>

View File

@@ -2,7 +2,7 @@
import type React from "react" import type React from "react"
import { createContext, useContext, useEffect, useRef, useState } from "react" import { createContext, useContext, useEffect, useRef, useState } from "react"
import type { DrawIoEmbedRef } from "react-drawio" import type { DrawIoEmbedRef, EventAutoSave } from "react-drawio"
import { toast } from "sonner" import { toast } from "sonner"
import type { ExportFormat } from "@/components/save-dialog" import type { ExportFormat } from "@/components/save-dialog"
import { getApiEndpoint } from "@/lib/base-path" import { getApiEndpoint } from "@/lib/base-path"
@@ -23,6 +23,7 @@ interface DiagramContextType {
resolverRef: React.Ref<((value: string) => void) | null> resolverRef: React.Ref<((value: string) => void) | null>
drawioRef: React.Ref<DrawIoEmbedRef | null> drawioRef: React.Ref<DrawIoEmbedRef | null>
handleDiagramExport: (data: any) => void handleDiagramExport: (data: any) => void
handleAutoSave: (data: EventAutoSave) => void
clearDiagram: () => void clearDiagram: () => void
saveDiagramToFile: ( saveDiagramToFile: (
filename: string, filename: string,
@@ -226,6 +227,13 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
} }
} }
// Handle autosave events from draw.io - keeps chartXML in sync with user modifications
const handleAutoSave = (data: EventAutoSave) => {
if (data.xml) {
setChartXML(data.xml)
}
}
const clearDiagram = () => { const clearDiagram = () => {
const emptyDiagram = `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>` const emptyDiagram = `<mxfile><diagram name="Page-1" id="page-1"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
// Skip validation for trusted internal template (loadDiagram also sets chartXML) // Skip validation for trusted internal template (loadDiagram also sets chartXML)
@@ -350,6 +358,7 @@ export function DiagramProvider({ children }: { children: React.ReactNode }) {
resolverRef, resolverRef,
drawioRef, drawioRef,
handleDiagramExport, handleDiagramExport,
handleAutoSave,
clearDiagram, clearDiagram,
saveDiagramToFile, saveDiagramToFile,
getThumbnailSvg, getThumbnailSvg,

View File

@@ -200,7 +200,6 @@ npm run dev
- OpenAI - OpenAI
- Anthropic - Anthropic
- Google AI - Google AI
- Google Vertex AI
- Azure OpenAI - Azure OpenAI
- Ollama - Ollama
- OpenRouter - OpenRouter
@@ -214,10 +213,6 @@ npm run dev
📖 **[详细的提供商配置指南](./ai-providers.md)** - 查看各提供商的设置说明。 📖 **[详细的提供商配置指南](./ai-providers.md)** - 查看各提供商的设置说明。
### 服务端多模型配置
管理员可以配置多个服务端模型,让所有用户无需提供个人 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。
注意:`claude` 系列已在带有 AWS、Azure、GCP 等云架构 Logo 的 draw.io 图表上进行训练,因此如果您想创建云架构图,这是最佳选择。 注意:`claude` 系列已在带有 AWS、Azure、GCP 等云架构 Logo 的 draw.io 图表上进行训练,因此如果您想创建云架构图,这是最佳选择。

View File

@@ -217,63 +217,6 @@ AI_MODEL=openai/gpt-4o
AI_PROVIDER=google # 或openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang AI_PROVIDER=google # 或openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang
``` ```
## 服务端多模型配置
管理员可以配置多个服务端模型,让所有用户无需提供个人 API Key 即可使用。
### 配置方式
**方式一:环境变量**(推荐用于云部署)
设置 `AI_MODELS_CONFIG` 为 JSON 字符串:
```bash
AI_MODELS_CONFIG='{"providers":[{"name":"OpenAI","provider":"openai","models":["gpt-4o"],"default":true}]}'
```
**方式二:配置文件**
在项目根目录创建 `ai-models.json` 文件(或通过 `AI_MODELS_CONFIG_PATH` 指定路径)。
### 配置示例
```json
{
"providers": [
{
"name": "OpenAI Production",
"provider": "openai",
"models": ["gpt-4o", "gpt-4o-mini"],
"default": true
},
{
"name": "Custom DeepSeek",
"provider": "deepseek",
"models": ["deepseek-chat"],
"apiKeyEnv": "MY_DEEPSEEK_KEY",
"baseUrlEnv": "MY_DEEPSEEK_URL"
}
]
}
```
### 字段说明
| 字段 | 必填 | 说明 |
|------|------|------|
| `name` | 是 | 显示名称(支持同一提供商多个配置) |
| `provider` | 是 | 提供商类型(`openai`, `anthropic`, `google`, `bedrock` 等) |
| `models` | 是 | 模型 ID 列表 |
| `default` | 否 | 设为 `true` 表示默认选中该提供商的第一个模型 |
| `apiKeyEnv` | 否 | 自定义 API Key 环境变量名(默认使用提供商标准变量如 `OPENAI_API_KEY` |
| `baseUrlEnv` | 否 | 自定义 Base URL 环境变量名 |
### 说明
- API Key 和凭证通过环境变量提供。默认使用标准变量名(如 `OPENAI_API_KEY`),也可通过 `apiKeyEnv` 指定自定义变量名。
- `name` 字段允许同一提供商多个配置(例如 "OpenAI Production" 和 "OpenAI Staging" 都使用 `provider: "openai"``apiKeyEnv` 不同)。
- 如果配置不存在,应用会回退到 `AI_PROVIDER`/`AI_MODEL` 环境变量配置。
## 模型能力要求 ## 模型能力要求
此任务对模型能力要求极高因为它涉及生成具有严格格式约束draw.io XML的长文本。 此任务对模型能力要求极高因为它涉及生成具有严格格式约束draw.io XML的长文本。

View File

@@ -33,21 +33,6 @@ Optional custom endpoint:
GOOGLE_BASE_URL=https://your-custom-endpoint GOOGLE_BASE_URL=https://your-custom-endpoint
``` ```
### Google Vertex AI (Enterprise GCP)
Google Vertex AI offers enterprise-grade features and data residency. **Express Mode** allows for simple API key authentication, making it compatible with edge runtimes like Vercel and Cloudflare.
```bash
GOOGLE_VERTEX_API_KEY=your_api_key
AI_MODEL=gemini-2.0-flash
```
Optional custom endpoint:
```bash
GOOGLE_VERTEX_BASE_URL=https://your-custom-endpoint
```
### OpenAI ### OpenAI
```bash ```bash
@@ -232,63 +217,6 @@ If you configure **multiple** API keys, you must explicitly set `AI_PROVIDER`:
AI_PROVIDER=google # or: openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang, modelscope AI_PROVIDER=google # or: openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang, modelscope
``` ```
## Server-Side Multi-Model Configuration
Administrators can configure multiple server-side models that are available to all users without requiring personal API keys.
### Configuration Methods
**Option 1: Environment Variable** (recommended for cloud deployments)
Set `AI_MODELS_CONFIG` as a JSON string:
```bash
AI_MODELS_CONFIG='{"providers":[{"name":"OpenAI","provider":"openai","models":["gpt-4o"],"default":true}]}'
```
**Option 2: Config File**
Create an `ai-models.json` file in the project root (or set `AI_MODELS_CONFIG_PATH` to a custom location).
### Example Configuration
```json
{
"providers": [
{
"name": "OpenAI Production",
"provider": "openai",
"models": ["gpt-4o", "gpt-4o-mini"],
"default": true
},
{
"name": "Custom DeepSeek",
"provider": "deepseek",
"models": ["deepseek-chat"],
"apiKeyEnv": "MY_DEEPSEEK_KEY",
"baseUrlEnv": "MY_DEEPSEEK_URL"
}
]
}
```
### Field Reference
| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Display name (supports multiple configs for same provider) |
| `provider` | Yes | Provider type (`openai`, `anthropic`, `google`, `bedrock`, etc.) |
| `models` | Yes | List of model IDs |
| `default` | No | Set to `true` to auto-select this provider's first model as default |
| `apiKeyEnv` | No | Custom API key env var name (defaults to provider's standard var like `OPENAI_API_KEY`) |
| `baseUrlEnv` | No | Custom base URL env var name |
### Notes
- API keys and credentials are provided via environment variables. By default, standard var names are used (e.g., `OPENAI_API_KEY`), but you can specify custom var names with `apiKeyEnv`.
- The `name` field allows multiple configurations for the same provider (e.g., "OpenAI Production" and "OpenAI Staging" both using `provider: "openai"` but with different `apiKeyEnv` values).
- If config is not present, the app falls back to `AI_PROVIDER`/`AI_MODEL` environment variable configuration.
## Model Capability Requirements ## Model Capability Requirements
This task requires exceptionally strong model capabilities, as it involves generating long-form text with strict formatting constraints (draw.io XML). This task requires exceptionally strong model capabilities, as it involves generating long-form text with strict formatting constraints (draw.io XML).

View File

@@ -22,27 +22,6 @@ cp env.example .env
docker run -d -p 3000:3000 --env-file .env ghcr.io/dayuanjiang/next-ai-draw-io:latest docker run -d -p 3000:3000 --env-file .env ghcr.io/dayuanjiang/next-ai-draw-io:latest
``` ```
### Using server-side model configuration
You can mount an `ai-models.json` file into the container to provide multiple server-side models without exposing user API keys:
```bash
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY=your_api_key \
-v $(pwd)/ai-models.json:/app/ai-models.json:ro \
ghcr.io/dayuanjiang/next-ai-draw-io:latest
```
If you prefer to keep the config in a different path inside the container, set `AI_MODELS_CONFIG_PATH`:
```bash
docker run -d -p 3000:3000 \
-e OPENAI_API_KEY=your_api_key \
-e AI_MODELS_CONFIG_PATH=/config/ai-models.json \
-v $(pwd)/ai-models.json:/config/ai-models.json:ro \
ghcr.io/dayuanjiang/next-ai-draw-io:latest
```
Open [http://localhost:3000](http://localhost:3000) in your browser. Open [http://localhost:3000](http://localhost:3000) in your browser.
Replace the environment variables with your preferred AI provider configuration. See [AI Providers](./ai-providers.md) for available options. Replace the environment variables with your preferred AI provider configuration. See [AI Providers](./ai-providers.md) for available options.

View File

@@ -201,7 +201,6 @@ Next.jsアプリをデプロイする最も簡単な方法は、Next.jsの作成
- OpenAI - OpenAI
- Anthropic - Anthropic
- Google AI - Google AI
- Google Vertex AI
- Azure OpenAI - Azure OpenAI
- Ollama - Ollama
- OpenRouter - OpenRouter
@@ -215,10 +214,6 @@ AWS BedrockとOpenRouter以外のすべてのプロバイダーはカスタム
📖 **[詳細なプロバイダー設定ガイド](./ai-providers.md)** - 各プロバイダーの設定手順をご覧ください。 📖 **[詳細なプロバイダー設定ガイド](./ai-providers.md)** - 各プロバイダーの設定手順をご覧ください。
### サーバーサイドマルチモデル設定
管理者は、ユーザーが個人の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を推奨します。
注:`claude`シリーズはAWS、Azure、GCPなどのクラウドアーキテクチャロゴ付きのdraw.ioダイアグラムで学習されているため、クラウドアーキテクチャダイアグラムを作成したい場合は最適な選択です。 注:`claude`シリーズはAWS、Azure、GCPなどのクラウドアーキテクチャロゴ付きのdraw.ioダイアグラムで学習されているため、クラウドアーキテクチャダイアグラムを作成したい場合は最適な選択です。

View File

@@ -217,63 +217,6 @@ AI_MODEL=openai/gpt-4o
AI_PROVIDER=google # または: openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang AI_PROVIDER=google # または: openai, anthropic, deepseek, siliconflow, doubao, azure, bedrock, openrouter, ollama, gateway, sglang
``` ```
## サーバーサイドマルチモデル設定
管理者は、ユーザーが個人のAPIキーを提供することなく利用できる複数のサーバーサイドモデルを設定できます。
### 設定方法
**方法1環境変数**(クラウドデプロイ推奨)
`AI_MODELS_CONFIG` をJSON文字列として設定
```bash
AI_MODELS_CONFIG='{"providers":[{"name":"OpenAI","provider":"openai","models":["gpt-4o"],"default":true}]}'
```
**方法2設定ファイル**
プロジェクトルートに `ai-models.json` ファイルを作成します(または `AI_MODELS_CONFIG_PATH` でパスを指定)。
### 設定例
```json
{
"providers": [
{
"name": "OpenAI Production",
"provider": "openai",
"models": ["gpt-4o", "gpt-4o-mini"],
"default": true
},
{
"name": "Custom DeepSeek",
"provider": "deepseek",
"models": ["deepseek-chat"],
"apiKeyEnv": "MY_DEEPSEEK_KEY",
"baseUrlEnv": "MY_DEEPSEEK_URL"
}
]
}
```
### フィールド説明
| フィールド | 必須 | 説明 |
|------------|------|------|
| `name` | はい | 表示名(同一プロバイダーの複数設定をサポート) |
| `provider` | はい | プロバイダータイプ(`openai`, `anthropic`, `google`, `bedrock` など) |
| `models` | はい | モデルIDのリスト |
| `default` | いいえ | `true` に設定すると、そのプロバイダーの最初のモデルがデフォルトで選択されます |
| `apiKeyEnv` | いいえ | カスタムAPIキー環境変数名デフォルトは `OPENAI_API_KEY` などの標準変数) |
| `baseUrlEnv` | いいえ | カスタムBase URL環境変数名 |
### 備考
- APIキーと認証情報は環境変数で提供します。デフォルトは標準変数名`OPENAI_API_KEY`)を使用しますが、`apiKeyEnv` でカスタム変数名を指定できます。
- `name` フィールドにより同一プロバイダーの複数設定が可能です「OpenAI Production」と「OpenAI Staging」が両方とも `provider: "openai"` を使用しつつ、異なる `apiKeyEnv` を持つ)。
- 設定が存在しない場合、アプリは `AI_PROVIDER`/`AI_MODEL` 環境変数設定にフォールバックします。
## モデル性能要件 ## モデル性能要件
このタスクは、厳密なフォーマット制約draw.io XMLを伴う長文テキストの生成を含むため、非常に強力なモデル性能が必要です。 このタスクは、厳密なフォーマット制約draw.io XMLを伴う長文テキストの生成を含むため、非常に強力なモデル性能が必要です。

View File

@@ -1,6 +1,6 @@
# 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, deepseek, siliconflow, gateway # Options: bedrock, openai, anthropic, google, azure, ollama, openrouter, deepseek, siliconflow, gateway
# Default: bedrock # Default: bedrock
AI_PROVIDER=bedrock AI_PROVIDER=bedrock
@@ -40,14 +40,6 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
# GOOGLE_THINKING_BUDGET=8192 # Optional: Gemini 2.5 thinking budget in tokens (for more/less thinking) # GOOGLE_THINKING_BUDGET=8192 # Optional: Gemini 2.5 thinking budget in tokens (for more/less thinking)
# GOOGLE_THINKING_LEVEL=high # Optional: Gemini 3 thinking level (low/high) # GOOGLE_THINKING_LEVEL=high # Optional: Gemini 3 thinking level (low/high)
# Google Vertex AI Configuration (Enterprise GCP)
# For enterprise users needing data residency, VPC Service Controls, or GCP integration
# GOOGLE_VERTEX_API_KEY= # Required: Express Mode API key
# GOOGLE_VERTEX_BASE_URL=https://... # Optional: Custom endpoint URL
# Note: Gemini 2.5/3 models automatically enable reasoning display (includeThoughts: true)
# GOOGLE_VERTEX_THINKING_BUDGET=8192 # Optional: Gemini 2.5 thinking budget in tokens (1024-100000)
# GOOGLE_VERTEX_THINKING_LEVEL=high # Optional: Gemini 3 thinking level (minimal/low/medium/high)
# Azure OpenAI Configuration # Azure OpenAI Configuration
# Configure endpoint using ONE of these methods: # Configure endpoint using ONE of these methods:
# 1. AZURE_RESOURCE_NAME - SDK constructs: https://{name}.openai.azure.com/openai/v1{path} # 1. AZURE_RESOURCE_NAME - SDK constructs: https://{name}.openai.azure.com/openai/v1{path}
@@ -101,11 +93,6 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
# LANGFUSE_SECRET_KEY=sk-lf-... # LANGFUSE_SECRET_KEY=sk-lf-...
# LANGFUSE_BASEURL=https://cloud.langfuse.com # EU region, use https://us.cloud.langfuse.com for US # LANGFUSE_BASEURL=https://cloud.langfuse.com # EU region, use https://us.cloud.langfuse.com for US
# Optional server-side multi-model configuration
# If set, points to a JSON file with server-provided models (see README for schema).
# Default: ./ai-models.json in project root
# AI_MODELS_CONFIG_PATH=/path/to/ai-models.json
# Temperature (Optional) # Temperature (Optional)
# Controls randomness in AI responses. Lower = more deterministic. # Controls randomness in AI responses. Lower = more deterministic.
# Leave unset for models that don't support temperature (e.g., GPT-5.1 reasoning models) # Leave unset for models that don't support temperature (e.g., GPT-5.1 reasoning models)

View File

@@ -1,7 +1,6 @@
"use client" "use client"
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"
import type { FlattenedServerModel } from "@/lib/server-model-config"
import { STORAGE_KEYS } from "@/lib/storage" import { STORAGE_KEYS } from "@/lib/storage"
import { import {
createEmptyConfig, createEmptyConfig,
@@ -133,56 +132,14 @@ export interface UseModelConfigReturn {
export function useModelConfig(): UseModelConfigReturn { export function useModelConfig(): UseModelConfigReturn {
const [config, setConfig] = useState<MultiModelConfig>(createEmptyConfig) const [config, setConfig] = useState<MultiModelConfig>(createEmptyConfig)
const [isLoaded, setIsLoaded] = useState(false) const [isLoaded, setIsLoaded] = useState(false)
const [serverModels, setServerModels] = useState<FlattenedServerModel[]>([])
const [serverLoaded, setServerLoaded] = useState(false)
// Load client config on mount // Load config on mount
useEffect(() => { useEffect(() => {
const loaded = loadConfig() const loaded = loadConfig()
setConfig(loaded) setConfig(loaded)
setIsLoaded(true) setIsLoaded(true)
}, []) }, [])
// Load server models on mount (if any)
useEffect(() => {
if (typeof window === "undefined") return
fetch("/api/server-models")
.then((res) => {
if (!res.ok) {
console.error(
"Failed to load server models:",
res.status,
res.statusText,
)
throw new Error(`Request failed with status ${res.status}`)
}
return res.json()
})
.then((data) => {
const raw: FlattenedServerModel[] = data?.models || []
setServerModels(raw)
setServerLoaded(true)
// Auto-select default server model if no model is currently selected
setConfig((prev) => {
if (!prev.selectedModelId && raw.length > 0) {
const defaultModel = raw.find((m) => m.isDefault)
if (defaultModel) {
return { ...prev, selectedModelId: defaultModel.id }
}
// If no default marked, use first server model
return { ...prev, selectedModelId: raw[0].id }
}
return prev
})
})
.catch((error) => {
console.error("Error while loading server models:", error)
setServerLoaded(true)
})
}, [])
// Save config whenever it changes (after initial load) // Save config whenever it changes (after initial load)
useEffect(() => { useEffect(() => {
if (isLoaded) { if (isLoaded) {
@@ -191,33 +148,9 @@ export function useModelConfig(): UseModelConfigReturn {
}, [config, isLoaded]) }, [config, isLoaded])
// Derived state // Derived state
const userModels = flattenModels(config) const models = flattenModels(config)
const models: FlattenedModel[] = [
// Server models (read-only, credentials from env)
...serverModels.map((m) => ({
id: m.id,
modelId: m.modelId,
provider: m.provider,
providerLabel: `Server · ${m.providerLabel}`,
apiKey: "",
baseUrl: undefined,
awsAccessKeyId: undefined,
awsSecretAccessKey: undefined,
awsRegion: undefined,
awsSessionToken: undefined,
validated: true,
source: "server" as const,
isDefault: m.isDefault,
apiKeyEnv: m.apiKeyEnv,
baseUrlEnv: m.baseUrlEnv,
})),
// User models from local configuration
...userModels,
]
const selectedModel = config.selectedModelId const selectedModel = config.selectedModelId
? models.find((m) => m.id === config.selectedModelId) ? findModelById(config, config.selectedModelId)
: undefined : undefined
// Actions // Actions
@@ -349,7 +282,7 @@ export function useModelConfig(): UseModelConfigReturn {
return { return {
config, config,
isLoaded: isLoaded && serverLoaded, isLoaded,
models, models,
selectedModel, selectedModel,
selectedModelId: config.selectedModelId, selectedModelId: config.selectedModelId,
@@ -381,10 +314,6 @@ export function getSelectedAIConfig(): {
awsSecretAccessKey: string awsSecretAccessKey: string
awsRegion: string awsRegion: string
awsSessionToken: string awsSessionToken: string
// Selected model ID (for server model lookup)
selectedModelId: string
// Vertex AI credentials (Express Mode)
vertexApiKey: string
} { } {
const empty = { const empty = {
accessCode: "", accessCode: "",
@@ -396,8 +325,6 @@ export function getSelectedAIConfig(): {
awsSecretAccessKey: "", awsSecretAccessKey: "",
awsRegion: "", awsRegion: "",
awsSessionToken: "", awsSessionToken: "",
selectedModelId: "",
vertexApiKey: "",
} }
if (typeof window === "undefined") return empty if (typeof window === "undefined") return empty
@@ -420,8 +347,6 @@ export function getSelectedAIConfig(): {
awsSecretAccessKey: "", awsSecretAccessKey: "",
awsRegion: "", awsRegion: "",
awsSessionToken: "", awsSessionToken: "",
selectedModelId: "",
vertexApiKey: "",
} }
} }
@@ -432,32 +357,12 @@ export function getSelectedAIConfig(): {
return { ...empty, accessCode } return { ...empty, accessCode }
} }
// No selected model = use server default (AI_PROVIDER/AI_MODEL/env auto-detect) // No selected model = use server default
if (!config.selectedModelId) { if (!config.selectedModelId) {
return { ...empty, accessCode } return { ...empty, accessCode }
} }
// Server-side model selection (id = "server:<name-slug>:<modelId>") // Find selected model
// Provider is resolved server-side via findServerModelById()
if (config.selectedModelId.startsWith("server:")) {
const parts = config.selectedModelId.split(":")
const nameSlug = parts[1] || ""
const modelId = parts.slice(2).join(":") // Preserve Bedrock-style IDs
return {
...empty,
accessCode,
// Note: nameSlug is NOT the provider, but we send it for backwards compat
// Server uses selectedModelId to lookup the actual provider
aiProvider: nameSlug,
aiBaseUrl: "",
aiApiKey: "",
aiModel: modelId,
selectedModelId: config.selectedModelId,
}
}
// Find selected user-defined model
const model = findModelById(config, config.selectedModelId) const model = findModelById(config, config.selectedModelId)
if (!model) { if (!model) {
return { ...empty, accessCode } return { ...empty, accessCode }
@@ -474,8 +379,5 @@ export function getSelectedAIConfig(): {
awsSecretAccessKey: model.awsSecretAccessKey || "", awsSecretAccessKey: model.awsSecretAccessKey || "",
awsRegion: model.awsRegion || "", awsRegion: model.awsRegion || "",
awsSessionToken: model.awsSessionToken || "", awsSessionToken: model.awsSessionToken || "",
selectedModelId: config.selectedModelId || "",
// Vertex AI credentials (Express Mode)
vertexApiKey: model.vertexApiKey || "",
} }
} }

View File

@@ -4,7 +4,6 @@ import { azure, createAzure } from "@ai-sdk/azure"
import { createDeepSeek, deepseek } from "@ai-sdk/deepseek" import { createDeepSeek, deepseek } from "@ai-sdk/deepseek"
import { createGateway, gateway } from "@ai-sdk/gateway" 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 { createOpenAI, openai } from "@ai-sdk/openai" import { createOpenAI, openai } from "@ai-sdk/openai"
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"
@@ -30,13 +29,8 @@ export interface ClientOverrides {
awsSecretAccessKey?: string | null awsSecretAccessKey?: string | null
awsRegion?: string | null awsRegion?: string | null
awsSessionToken?: string | null awsSessionToken?: string | null
// Vertex AI config
vertexApiKey?: string | null // Express Mode API key
// Custom headers (e.g., for EdgeOne cookie auth) // Custom headers (e.g., for EdgeOne cookie auth)
headers?: Record<string, string> headers?: Record<string, string>
// Custom env var names for server models (allows multiple API keys per provider)
apiKeyEnv?: string
baseUrlEnv?: string
} }
// Providers that can be used with client-provided API keys // Providers that can be used with client-provided API keys
@@ -44,7 +38,6 @@ const ALLOWED_CLIENT_PROVIDERS: ProviderName[] = [
"openai", "openai",
"anthropic", "anthropic",
"google", "google",
"vertexai",
"azure", "azure",
"bedrock", "bedrock",
"openrouter", "openrouter",
@@ -95,36 +88,6 @@ export function resolveBaseURL(
return userBaseUrl || serverBaseUrl || defaultBaseUrl || undefined return userBaseUrl || serverBaseUrl || defaultBaseUrl || undefined
} }
/**
* Resolve API key from custom env var name or default env var.
* Supports multiple API keys per provider via ai-models.json apiKeyEnv config.
*
* Priority:
* 1. User-provided API key (overrides.apiKey)
* 2. Custom env var from ai-models.json (overrides.apiKeyEnv)
* 3. Default provider env var (defaultEnvVar)
*/
function resolveApiKey(
overrides: ClientOverrides | undefined,
defaultEnvVar: string,
): string | undefined {
if (overrides?.apiKey) return overrides.apiKey
if (overrides?.apiKeyEnv) return process.env[overrides.apiKeyEnv]
return process.env[defaultEnvVar]
}
/**
* Resolve base URL from custom env var name or default env var.
* Supports multiple base URLs per provider via ai-models.json baseUrlEnv config.
*/
function resolveBaseUrlEnv(
overrides: ClientOverrides | undefined,
defaultEnvVar: string,
): string | undefined {
if (overrides?.baseUrlEnv) return process.env[overrides.baseUrlEnv]
return process.env[defaultEnvVar]
}
/** /**
* Safely parse integer from environment variable with validation * Safely parse integer from environment variable with validation
*/ */
@@ -159,8 +122,6 @@ function parseIntSafe(
* - ANTHROPIC_THINKING_TYPE: Anthropic thinking type (enabled) * - ANTHROPIC_THINKING_TYPE: Anthropic thinking type (enabled)
* - GOOGLE_THINKING_BUDGET: Google Gemini 2.5 thinking budget in tokens (1024-100000) * - GOOGLE_THINKING_BUDGET: Google Gemini 2.5 thinking budget in tokens (1024-100000)
* - GOOGLE_THINKING_LEVEL: Google Gemini 3 thinking level (low/high) * - GOOGLE_THINKING_LEVEL: Google Gemini 3 thinking level (low/high)
* - GOOGLE_VERTEX_THINKING_BUDGET: Vertex AI Gemini 2.5 thinking budget in tokens (1024-100000)
* - GOOGLE_VERTEX_THINKING_LEVEL: Vertex AI Gemini 3 thinking level (low/high)
* - AZURE_REASONING_EFFORT: Azure/OpenAI reasoning effort (low/medium/high) * - AZURE_REASONING_EFFORT: Azure/OpenAI reasoning effort (low/medium/high)
* - AZURE_REASONING_SUMMARY: Azure reasoning summary (none/brief/detailed) * - AZURE_REASONING_SUMMARY: Azure reasoning summary (none/brief/detailed)
* - BEDROCK_REASONING_BUDGET_TOKENS: Bedrock Claude reasoning budget in tokens (1024-64000) * - BEDROCK_REASONING_BUDGET_TOKENS: Bedrock Claude reasoning budget in tokens (1024-64000)
@@ -325,46 +286,7 @@ function buildProviderOptions(
} }
break break
} }
case "vertexai": {
const thinkingBudget = parseIntSafe(
process.env.GOOGLE_VERTEX_THINKING_BUDGET,
"GOOGLE_VERTEX_THINKING_BUDGET",
1024,
100000,
)
const thinkingLevel = process.env.GOOGLE_VERTEX_THINKING_LEVEL
if (
modelId &&
(modelId.includes("gemini-2") ||
modelId.includes("gemini-3") ||
modelId.includes("gemini2") ||
modelId.includes("gemini3"))
) {
const thinkingConfig: Record<string, any> = {
includeThoughts: true,
}
const isGemini3 =
modelId?.includes("gemini-3") ||
modelId?.includes("gemini3")
const isGemini25 =
modelId?.includes("2.5") || modelId?.includes("2-5")
if (isGemini3 && thinkingLevel) {
// Vertex AI provider in AI SDK supports more granular levels (minimal/low/medium/high)
thinkingConfig.thinkingLevel = thinkingLevel as
| "minimal"
| "low"
| "medium"
| "high"
} else if (isGemini25 && thinkingBudget) {
thinkingConfig.thinkingBudget = thinkingBudget
}
options.google = { thinkingConfig }
}
break
}
case "azure": { case "azure": {
const reasoningEffort = process.env.AZURE_REASONING_EFFORT const reasoningEffort = process.env.AZURE_REASONING_EFFORT
const reasoningSummary = process.env.AZURE_REASONING_SUMMARY const reasoningSummary = process.env.AZURE_REASONING_SUMMARY
@@ -466,7 +388,6 @@ const PROVIDER_ENV_VARS: Record<ProviderName, string | null> = {
openai: "OPENAI_API_KEY", openai: "OPENAI_API_KEY",
anthropic: "ANTHROPIC_API_KEY", anthropic: "ANTHROPIC_API_KEY",
google: "GOOGLE_GENERATIVE_AI_API_KEY", google: "GOOGLE_GENERATIVE_AI_API_KEY",
vertexai: "GOOGLE_VERTEX_API_KEY",
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",
@@ -514,15 +435,9 @@ function detectProvider(): ProviderName | null {
/** /**
* Validate that required API keys are present for the selected provider * Validate that required API keys are present for the selected provider
* @param provider - The provider to validate
* @param customApiKeyEnv - Optional custom env var name (from ai-models.json apiKeyEnv)
*/ */
function validateProviderCredentials( function validateProviderCredentials(provider: ProviderName): void {
provider: ProviderName, const requiredVar = PROVIDER_ENV_VARS[provider]
customApiKeyEnv?: string,
): void {
// Use custom env var name if provided, otherwise use default
const requiredVar = customApiKeyEnv || PROVIDER_ENV_VARS[provider]
if (requiredVar && !process.env[requiredVar]) { if (requiredVar && !process.env[requiredVar]) {
throw new Error( throw new Error(
`${requiredVar} environment variable is required for ${provider} provider. ` + `${requiredVar} environment variable is required for ${provider} provider. ` +
@@ -576,7 +491,6 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
if ( if (
overrides?.baseUrl && overrides?.baseUrl &&
!overrides?.apiKey && !overrides?.apiKey &&
!(overrides?.provider === "vertexai" && overrides?.vertexApiKey) &&
overrides?.provider !== "edgeone" overrides?.provider !== "edgeone"
) { ) {
throw new Error( throw new Error(
@@ -586,11 +500,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
} }
// Check if client is providing their own provider override // Check if client is providing their own provider override
const isClientOverride = !!( const isClientOverride = !!(overrides?.provider && overrides?.apiKey)
overrides?.provider &&
(overrides?.apiKey ||
(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
const modelId = overrides?.modelId || process.env.AI_MODEL const modelId = overrides?.modelId || process.env.AI_MODEL
@@ -660,7 +570,7 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
// Only validate server credentials if client isn't providing their own API key // Only validate server credentials if client isn't providing their own API key
if (!isClientOverride) { if (!isClientOverride) {
validateProviderCredentials(provider, overrides?.apiKeyEnv) validateProviderCredentials(provider)
} }
console.log(`[AI Provider] Initializing ${provider} with model: ${modelId}`) console.log(`[AI Provider] Initializing ${provider} with model: ${modelId}`)
@@ -710,15 +620,11 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
} }
case "openai": { case "openai": {
const apiKey = resolveApiKey(overrides, "OPENAI_API_KEY") const apiKey = overrides?.apiKey || process.env.OPENAI_API_KEY
const serverBaseUrl = resolveBaseUrlEnv(
overrides,
"OPENAI_BASE_URL",
)
const baseURL = resolveBaseURL( const baseURL = resolveBaseURL(
overrides?.apiKey, overrides?.apiKey,
overrides?.baseUrl, overrides?.baseUrl,
serverBaseUrl, process.env.OPENAI_BASE_URL,
) )
if (baseURL) { if (baseURL) {
// Custom base URL = third-party proxy, use Chat Completions API // Custom base URL = third-party proxy, use Chat Completions API
@@ -737,15 +643,11 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
} }
case "anthropic": { case "anthropic": {
const apiKey = resolveApiKey(overrides, "ANTHROPIC_API_KEY") const apiKey = overrides?.apiKey || process.env.ANTHROPIC_API_KEY
const serverBaseUrl = resolveBaseUrlEnv(
overrides,
"ANTHROPIC_BASE_URL",
)
const baseURL = resolveBaseURL( const baseURL = resolveBaseURL(
overrides?.apiKey, overrides?.apiKey,
overrides?.baseUrl, overrides?.baseUrl,
serverBaseUrl, process.env.ANTHROPIC_BASE_URL,
"https://api.anthropic.com/v1", "https://api.anthropic.com/v1",
) )
const customProvider = createAnthropic({ const customProvider = createAnthropic({
@@ -760,18 +662,12 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
} }
case "google": { case "google": {
const apiKey = resolveApiKey( const apiKey =
overrides, overrides?.apiKey || process.env.GOOGLE_GENERATIVE_AI_API_KEY
"GOOGLE_GENERATIVE_AI_API_KEY",
)
const serverBaseUrl = resolveBaseUrlEnv(
overrides,
"GOOGLE_BASE_URL",
)
const baseURL = resolveBaseURL( const baseURL = resolveBaseURL(
overrides?.apiKey, overrides?.apiKey,
overrides?.baseUrl, overrides?.baseUrl,
serverBaseUrl, process.env.GOOGLE_BASE_URL,
) )
if (baseURL || overrides?.apiKey) { if (baseURL || overrides?.apiKey) {
const customGoogle = createGoogleGenerativeAI({ const customGoogle = createGoogleGenerativeAI({
@@ -784,37 +680,13 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
} }
break break
} }
case "vertexai": {
// Express Mode: Use API key for authentication
const vertexApiKey =
overrides?.vertexApiKey || process.env.GOOGLE_VERTEX_API_KEY
if (!vertexApiKey) {
throw new Error(
"Vertex AI requires an API key for Express Mode. " +
"Get one from Google Cloud Console or set GOOGLE_VERTEX_API_KEY environment variable.",
)
}
// Support custom base URL from env or client override
const baseURL =
overrides?.baseUrl || process.env.GOOGLE_VERTEX_BASE_URL
const vertexProvider = createVertex({
apiKey: vertexApiKey,
...(baseURL && { baseURL }),
})
model = vertexProvider(modelId)
break
}
case "azure": { case "azure": {
const apiKey = resolveApiKey(overrides, "AZURE_API_KEY") const apiKey = overrides?.apiKey || process.env.AZURE_API_KEY
const serverBaseUrl = resolveBaseUrlEnv(overrides, "AZURE_BASE_URL")
const baseURL = resolveBaseURL( const baseURL = resolveBaseURL(
overrides?.apiKey, overrides?.apiKey,
overrides?.baseUrl, overrides?.baseUrl,
serverBaseUrl, process.env.AZURE_BASE_URL,
) )
// Only use server's resourceName if user is NOT providing their own API key // Only use server's resourceName if user is NOT providing their own API key
const resourceName = overrides?.apiKey const resourceName = overrides?.apiKey
@@ -848,15 +720,11 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
break break
case "openrouter": { case "openrouter": {
const apiKey = resolveApiKey(overrides, "OPENROUTER_API_KEY") const apiKey = overrides?.apiKey || process.env.OPENROUTER_API_KEY
const serverBaseUrl = resolveBaseUrlEnv(
overrides,
"OPENROUTER_BASE_URL",
)
const baseURL = resolveBaseURL( const baseURL = resolveBaseURL(
overrides?.apiKey, overrides?.apiKey,
overrides?.baseUrl, overrides?.baseUrl,
serverBaseUrl, process.env.OPENROUTER_BASE_URL,
) )
const openrouter = createOpenRouter({ const openrouter = createOpenRouter({
apiKey, apiKey,
@@ -867,15 +735,11 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
} }
case "deepseek": { case "deepseek": {
const apiKey = resolveApiKey(overrides, "DEEPSEEK_API_KEY") const apiKey = overrides?.apiKey || process.env.DEEPSEEK_API_KEY
const serverBaseUrl = resolveBaseUrlEnv(
overrides,
"DEEPSEEK_BASE_URL",
)
const baseURL = resolveBaseURL( const baseURL = resolveBaseURL(
overrides?.apiKey, overrides?.apiKey,
overrides?.baseUrl, overrides?.baseUrl,
serverBaseUrl, process.env.DEEPSEEK_BASE_URL,
) )
if (baseURL || overrides?.apiKey) { if (baseURL || overrides?.apiKey) {
const customDeepSeek = createDeepSeek({ const customDeepSeek = createDeepSeek({
@@ -890,15 +754,11 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
} }
case "siliconflow": { case "siliconflow": {
const apiKey = resolveApiKey(overrides, "SILICONFLOW_API_KEY") const apiKey = overrides?.apiKey || process.env.SILICONFLOW_API_KEY
const serverBaseUrl = resolveBaseUrlEnv(
overrides,
"SILICONFLOW_BASE_URL",
)
const baseURL = resolveBaseURL( const baseURL = resolveBaseURL(
overrides?.apiKey, overrides?.apiKey,
overrides?.baseUrl, overrides?.baseUrl,
serverBaseUrl, process.env.SILICONFLOW_BASE_URL,
"https://api.siliconflow.cn/v1", "https://api.siliconflow.cn/v1",
) )
const siliconflowProvider = createOpenAI({ const siliconflowProvider = createOpenAI({
@@ -910,15 +770,11 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
} }
case "sglang": { case "sglang": {
const apiKey = resolveApiKey(overrides, "SGLANG_API_KEY") const apiKey = overrides?.apiKey || process.env.SGLANG_API_KEY
const serverBaseUrl = resolveBaseUrlEnv(
overrides,
"SGLANG_BASE_URL",
)
const baseURL = resolveBaseURL( const baseURL = resolveBaseURL(
overrides?.apiKey, overrides?.apiKey,
overrides?.baseUrl, overrides?.baseUrl,
serverBaseUrl, process.env.SGLANG_BASE_URL,
) )
const sglangProvider = createOpenAI({ const sglangProvider = createOpenAI({
@@ -1027,15 +883,11 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
// Vercel AI Gateway - unified access to multiple AI providers // Vercel AI Gateway - unified access to multiple AI providers
// Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5" // Model format: "provider/model" e.g., "openai/gpt-4o", "anthropic/claude-sonnet-4-5"
// See: https://vercel.com/ai-gateway // See: https://vercel.com/ai-gateway
const apiKey = resolveApiKey(overrides, "AI_GATEWAY_API_KEY") const apiKey = overrides?.apiKey || process.env.AI_GATEWAY_API_KEY
const serverBaseUrl = resolveBaseUrlEnv(
overrides,
"AI_GATEWAY_BASE_URL",
)
const baseURL = resolveBaseURL( const baseURL = resolveBaseURL(
overrides?.apiKey, overrides?.apiKey,
overrides?.baseUrl, overrides?.baseUrl,
serverBaseUrl, process.env.AI_GATEWAY_BASE_URL,
) )
// Only use custom configuration if explicitly set (local dev or custom Gateway) // Only use custom configuration if explicitly set (local dev or custom Gateway)
// Otherwise undefined → AI SDK uses Vercel default (https://ai-gateway.vercel.sh/v1/ai) + OIDC // Otherwise undefined → AI SDK uses Vercel default (https://ai-gateway.vercel.sh/v1/ai) + OIDC
@@ -1067,15 +919,11 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
} }
case "doubao": { case "doubao": {
const apiKey = resolveApiKey(overrides, "DOUBAO_API_KEY") const apiKey = overrides?.apiKey || process.env.DOUBAO_API_KEY
const serverBaseUrl = resolveBaseUrlEnv(
overrides,
"DOUBAO_BASE_URL",
)
const baseURL = resolveBaseURL( const baseURL = resolveBaseURL(
overrides?.apiKey, overrides?.apiKey,
overrides?.baseUrl, overrides?.baseUrl,
serverBaseUrl, process.env.DOUBAO_BASE_URL,
"https://ark.cn-beijing.volces.com/api/v3", "https://ark.cn-beijing.volces.com/api/v3",
) )
const lowerModelId = modelId.toLowerCase() const lowerModelId = modelId.toLowerCase()
@@ -1100,15 +948,11 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
} }
case "modelscope": { case "modelscope": {
const apiKey = resolveApiKey(overrides, "MODELSCOPE_API_KEY") const apiKey = overrides?.apiKey || process.env.MODELSCOPE_API_KEY
const serverBaseUrl = resolveBaseUrlEnv(
overrides,
"MODELSCOPE_BASE_URL",
)
const baseURL = resolveBaseURL( const baseURL = resolveBaseURL(
overrides?.apiKey, overrides?.apiKey,
overrides?.baseUrl, overrides?.baseUrl,
serverBaseUrl, process.env.MODELSCOPE_BASE_URL,
"https://api-inference.modelscope.cn/v1", "https://api-inference.modelscope.cn/v1",
) )
const modelscopeProvider = createOpenAI({ const modelscopeProvider = createOpenAI({

View File

@@ -305,13 +305,10 @@
"noModelsFound": "No models found.", "noModelsFound": "No models found.",
"default": "Default", "default": "Default",
"serverDefault": "Server Default", "serverDefault": "Server Default",
"serverModels": "Server Models",
"userModels": "User Models",
"configureModels": "Configure Models...", "configureModels": "Configure Models...",
"onlyVerifiedShown": "Only verified models are shown", "onlyVerifiedShown": "Only verified models are shown",
"showUnvalidatedModels": "Show unvalidated models", "showUnvalidatedModels": "Show unvalidated models",
"allModelsShown": "All models are shown (including unvalidated)", "allModelsShown": "All models are shown (including unvalidated)",
"unvalidatedModelWarning": "This model has not been validated", "unvalidatedModelWarning": "This model has not been validated"
"serverDefaultModel": "Server default model"
} }
} }

View File

@@ -305,13 +305,10 @@
"noModelsFound": "モデルが見つかりません。", "noModelsFound": "モデルが見つかりません。",
"default": "デフォルト", "default": "デフォルト",
"serverDefault": "サーバーデフォルト", "serverDefault": "サーバーデフォルト",
"serverModels": "サーバーモデル",
"userModels": "ユーザーモデル",
"configureModels": "モデルを設定...", "configureModels": "モデルを設定...",
"onlyVerifiedShown": "検証済みのモデルのみ表示", "onlyVerifiedShown": "検証済みのモデルのみ表示",
"showUnvalidatedModels": "未検証のモデルを表示", "showUnvalidatedModels": "未検証のモデルを表示",
"allModelsShown": "すべてのモデルを表示(未検証を含む)", "allModelsShown": "すべてのモデルを表示(未検証を含む)",
"unvalidatedModelWarning": "このモデルは検証されていません", "unvalidatedModelWarning": "このモデルは検証されていません"
"serverDefaultModel": "サーバーデフォルトモデル"
} }
} }

View File

@@ -305,13 +305,10 @@
"noModelsFound": "未找到模型。", "noModelsFound": "未找到模型。",
"default": "默认", "default": "默认",
"serverDefault": "服务器默认", "serverDefault": "服务器默认",
"serverModels": "服务器模型",
"userModels": "用户模型",
"configureModels": "配置模型...", "configureModels": "配置模型...",
"onlyVerifiedShown": "仅显示已验证的模型", "onlyVerifiedShown": "仅显示已验证的模型",
"showUnvalidatedModels": "显示未验证的模型", "showUnvalidatedModels": "显示未验证的模型",
"allModelsShown": "显示所有模型(包括未验证的)", "allModelsShown": "显示所有模型(包括未验证的)",
"unvalidatedModelWarning": "此模型尚未验证", "unvalidatedModelWarning": "此模型尚未验证"
"serverDefaultModel": "服务器默认模型"
} }
} }

View File

@@ -1,151 +0,0 @@
import fs from "fs/promises"
import path from "path"
import { z } from "zod"
import type { ProviderName } from "@/lib/types/model-config"
import { PROVIDER_INFO } from "@/lib/types/model-config"
export const ProviderNameSchema: z.ZodType<ProviderName> = z
.string()
.refine((val): val is ProviderName => val in PROVIDER_INFO, {
message: "Invalid provider name",
})
export const ServerProviderSchema = z.object({
name: z.string().min(1),
provider: ProviderNameSchema,
models: z.array(z.string().min(1)),
// Optional: custom environment variable name for API key
// e.g., "OPENAI_API_KEY_TEAM_A" instead of default "OPENAI_API_KEY"
apiKeyEnv: z.string().min(1).optional(),
// Optional: custom environment variable name for base URL
baseUrlEnv: z.string().min(1).optional(),
// Optional: mark the first model in this provider as the default
default: z.boolean().optional(),
})
export const ServerModelsConfigSchema = z.object({
providers: z.array(ServerProviderSchema),
})
export type ServerProviderConfig = z.infer<typeof ServerProviderSchema>
export type ServerModelsConfig = z.infer<typeof ServerModelsConfigSchema>
export interface FlattenedServerModel {
id: string // "server:<slugified-name>:<modelId>" - name ensures uniqueness for multiple API keys per provider
modelId: string
provider: ProviderName
providerLabel: string
isDefault: boolean
// Custom env var names for credentials (optional)
apiKeyEnv?: string
baseUrlEnv?: string
}
/**
* Convert provider name to URL-safe slug for use in model ID
* e.g., "OpenAI Production" → "openai-production"
*/
function slugify(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "")
}
function getConfigPath(): string {
const custom = process.env.AI_MODELS_CONFIG_PATH
if (custom && custom.trim().length > 0) return custom
return path.join(process.cwd(), "ai-models.json")
}
export async function loadRawServerModelsConfig(): Promise<ServerModelsConfig | null> {
// Priority 1: AI_MODELS_CONFIG env var (JSON string) - for cloud deployments
const envConfig = process.env.AI_MODELS_CONFIG
if (envConfig && envConfig.trim().length > 0) {
try {
const json = JSON.parse(envConfig)
return ServerModelsConfigSchema.parse(json)
} catch (err) {
console.error(
"[server-model-config] Failed to parse AI_MODELS_CONFIG:",
err,
)
return null
}
}
// Priority 2: ai-models.json file
const configPath = getConfigPath()
try {
const jsonStr = await fs.readFile(configPath, "utf8")
const json = JSON.parse(jsonStr)
return ServerModelsConfigSchema.parse(json)
} catch (err: any) {
if (err?.code === "ENOENT") {
return null
}
console.error(
"[server-model-config] Failed to load ai-models.json:",
err,
)
return null
}
}
export async function loadFlattenedServerModels(): Promise<
FlattenedServerModel[]
> {
const cfg = await loadRawServerModelsConfig()
if (!cfg) return []
const defaultProvider = process.env.AI_PROVIDER as ProviderName | undefined
const defaultModelId = process.env.AI_MODEL
const flattened: FlattenedServerModel[] = []
for (const p of cfg.providers) {
const providerLabel =
p.name || PROVIDER_INFO[p.provider]?.label || p.provider
// Use slugified name for unique ID (supports multiple API keys per provider)
const nameSlug = slugify(p.name)
for (const modelId of p.models) {
const id = `server:${nameSlug}:${modelId}`
// Default model priority:
// 1. From ai-models.json: first model of provider with default: true
// 2. From env vars: AI_MODEL matches (legacy behavior)
const isDefault =
(p.default === true && modelId === p.models[0]) ||
(!!defaultModelId &&
modelId === defaultModelId &&
(!defaultProvider || defaultProvider === p.provider))
flattened.push({
id,
modelId,
provider: p.provider,
providerLabel,
isDefault,
apiKeyEnv: p.apiKeyEnv,
baseUrlEnv: p.baseUrlEnv,
})
}
}
return flattened
}
/**
* Find a server model by its ID (format: "server:<slugified-name>:<modelId>")
* Returns the model config including apiKeyEnv/baseUrlEnv if configured
*/
export async function findServerModelById(
modelId: string,
): Promise<FlattenedServerModel | null> {
if (!modelId.startsWith("server:")) return null
const models = await loadFlattenedServerModels()
return models.find((m) => m.id === modelId) || null
}

View File

@@ -4,7 +4,6 @@ export type ProviderName =
| "openai" | "openai"
| "anthropic" | "anthropic"
| "google" | "google"
| "vertexai"
| "azure" | "azure"
| "bedrock" | "bedrock"
| "ollama" | "ollama"
@@ -37,9 +36,6 @@ export interface ProviderConfig {
awsSecretAccessKey?: string awsSecretAccessKey?: string
awsRegion?: string awsRegion?: string
awsSessionToken?: string // Optional, for temporary credentials awsSessionToken?: string // Optional, for temporary credentials
// Vertex AI specific fields
vertexApiKey?: string // Express Mode API key
models: ModelConfig[] models: ModelConfig[]
validated?: boolean // Has API key been validated validated?: boolean // Has API key been validated
} }
@@ -54,7 +50,7 @@ export interface MultiModelConfig {
// Flattened model for dropdown display // Flattened model for dropdown display
export interface FlattenedModel { export interface FlattenedModel {
id: string // Model config UUID or synthetic server ID (e.g., "server:provider:modelId") id: string // Model config UUID
modelId: string // Actual model ID modelId: string // Actual model ID
provider: ProviderName provider: ProviderName
providerLabel: string // Provider display name providerLabel: string // Provider display name
@@ -65,17 +61,7 @@ export interface FlattenedModel {
awsSecretAccessKey?: string awsSecretAccessKey?: string
awsRegion?: string awsRegion?: string
awsSessionToken?: string awsSessionToken?: string
// Vertex AI specific fields
vertexApiKey?: string // Express Mode API key
validated?: boolean // Has this model been validated validated?: boolean // Has this model been validated
// Source of this model config: user-defined (client) or server-defined
source?: "user" | "server"
// Whether this model is the server default (matches AI_MODEL env var)
isDefault?: boolean
// Custom env var names for server models (allows multiple API keys per provider)
apiKeyEnv?: string
baseUrlEnv?: string
} }
// Provider metadata // Provider metadata
@@ -89,7 +75,6 @@ export const PROVIDER_INFO: Record<
defaultBaseUrl: "https://api.anthropic.com/v1", defaultBaseUrl: "https://api.anthropic.com/v1",
}, },
google: { label: "Google" }, google: { label: "Google" },
vertexai: { label: "Google Vertex AI" },
azure: { label: "Azure OpenAI" }, azure: { label: "Azure OpenAI" },
bedrock: { label: "Amazon Bedrock" }, bedrock: { label: "Amazon Bedrock" },
ollama: { ollama: {
@@ -172,17 +157,6 @@ export const SUGGESTED_MODELS: Partial<Record<ProviderName, string[]>> = {
// Legacy // Legacy
"gemini-pro", "gemini-pro",
], ],
vertexai: [
// Gemini 2.5 series
"gemini-2.5-pro",
"gemini-2.5-flash",
// Gemini 2.0 series
"gemini-2.0-flash",
"gemini-2.0-flash-exp",
// Gemini 1.5 series
"gemini-1.5-pro",
"gemini-1.5-flash",
],
azure: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4", "gpt-35-turbo"], azure: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4", "gpt-35-turbo"],
bedrock: [ bedrock: [
// Anthropic Claude // Anthropic Claude
@@ -314,7 +288,7 @@ export function createModelConfig(modelId: string): ModelConfig {
} }
} }
// Get all models as flattened list for dropdown (user-defined only) // Get all models as flattened list for dropdown
export function flattenModels(config: MultiModelConfig): FlattenedModel[] { export function flattenModels(config: MultiModelConfig): FlattenedModel[] {
const models: FlattenedModel[] = [] const models: FlattenedModel[] = []
@@ -336,12 +310,7 @@ export function flattenModels(config: MultiModelConfig): FlattenedModel[] {
awsSecretAccessKey: provider.awsSecretAccessKey, awsSecretAccessKey: provider.awsSecretAccessKey,
awsRegion: provider.awsRegion, awsRegion: provider.awsRegion,
awsSessionToken: provider.awsSessionToken, awsSessionToken: provider.awsSessionToken,
// Vertex AI fields
vertexApiKey: provider.vertexApiKey,
validated: model.validated, validated: model.validated,
source: "user",
isDefault: false,
}) })
} }
} }

363
package-lock.json generated
View File

@@ -15,7 +15,6 @@
"@ai-sdk/deepseek": "^2.0.0", "@ai-sdk/deepseek": "^2.0.0",
"@ai-sdk/gateway": "^3.0.0", "@ai-sdk/gateway": "^3.0.0",
"@ai-sdk/google": "^3.0.0", "@ai-sdk/google": "^3.0.0",
"@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",
"@aws-sdk/client-dynamodb": "^3.957.0", "@aws-sdk/client-dynamodb": "^3.957.0",
@@ -207,106 +206,13 @@
} }
}, },
"node_modules/@ai-sdk/google": { "node_modules/@ai-sdk/google": {
"version": "3.0.9", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-3.0.9.tgz", "resolved": "https://registry.npmjs.org/@ai-sdk/google/-/google-3.0.6.tgz",
"integrity": "sha512-whRdK0gCZL92UbEHdmQ6edm3cp5ZZSqwE79AIvTEaJR+BeaMUJchTxz/I5w9l3EHGOiDxrKYssklqO3z45KGXg==", "integrity": "sha512-Nr7E+ouWd/bKO9SFlgLnJJ1+fiGHC07KAeFr08faT+lvkECWlxVox3aL0dec8uCgBDUghYbq7f4S5teUrCc+QQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@ai-sdk/provider": "3.0.3", "@ai-sdk/provider": "3.0.2",
"@ai-sdk/provider-utils": "4.0.7" "@ai-sdk/provider-utils": "4.0.4"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/google-vertex": {
"version": "4.0.16",
"resolved": "https://registry.npmjs.org/@ai-sdk/google-vertex/-/google-vertex-4.0.16.tgz",
"integrity": "sha512-paY4NGCaqMbd0kssK27ssTMggDybNPIFudJ99QsdzRDJ/Kgo0EjeXZn7G/CoBY/Snmf0fA0PEZW34yV9kloFCQ==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/anthropic": "3.0.14",
"@ai-sdk/google": "3.0.9",
"@ai-sdk/provider": "3.0.3",
"@ai-sdk/provider-utils": "4.0.7",
"google-auth-library": "^10.5.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/google-vertex/node_modules/@ai-sdk/anthropic": {
"version": "3.0.14",
"resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-3.0.14.tgz",
"integrity": "sha512-71BaVg60FM6tN0JaRY7kRb/6qZtHi5R9PFF3NES+kqonY3nVD4PCPP8DoMb/tqxC7f/XezlCqsHrveT2mGfi3A==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "3.0.3",
"@ai-sdk/provider-utils": "4.0.7"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/google-vertex/node_modules/@ai-sdk/provider": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.3.tgz",
"integrity": "sha512-qGPYdoAuECaUXPrrz0BPX1SacZQuJ6zky0aakxpW89QW1hrY0eF4gcFm/3L9Pk8C5Fwe+RvBf2z7ZjDhaPjnlg==",
"license": "Apache-2.0",
"dependencies": {
"json-schema": "^0.4.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@ai-sdk/google-vertex/node_modules/@ai-sdk/provider-utils": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.7.tgz",
"integrity": "sha512-ItzTdBxRLieGz1GHPwl9X3+HKfwTfFd9MdIa91aXRnOjUVRw68ENjAGKm3FcXGsBLkXDLaFWgjbTVdXe2igs2w==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "3.0.3",
"@standard-schema/spec": "^1.1.0",
"eventsource-parser": "^3.0.6"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/google/node_modules/@ai-sdk/provider": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.3.tgz",
"integrity": "sha512-qGPYdoAuECaUXPrrz0BPX1SacZQuJ6zky0aakxpW89QW1hrY0eF4gcFm/3L9Pk8C5Fwe+RvBf2z7ZjDhaPjnlg==",
"license": "Apache-2.0",
"dependencies": {
"json-schema": "^0.4.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@ai-sdk/google/node_modules/@ai-sdk/provider-utils": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.7.tgz",
"integrity": "sha512-ItzTdBxRLieGz1GHPwl9X3+HKfwTfFd9MdIa91aXRnOjUVRw68ENjAGKm3FcXGsBLkXDLaFWgjbTVdXe2igs2w==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "3.0.3",
"@standard-schema/spec": "^1.1.0",
"eventsource-parser": "^3.0.6"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@@ -9862,6 +9768,7 @@
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"engines": { "engines": {
@@ -13560,6 +13467,7 @@
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 14" "node": ">= 14"
@@ -14183,6 +14091,7 @@
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -14218,15 +14127,6 @@
"require-from-string": "^2.0.2" "require-from-string": "^2.0.2"
} }
}, },
"node_modules/bignumber.js": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/bl": { "node_modules/bl": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -14400,12 +14300,6 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -15399,15 +15293,6 @@
"dev": true, "dev": true,
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/data-urls": { "node_modules/data-urls": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz",
@@ -15915,15 +15800,6 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/eciesjs": { "node_modules/eciesjs": {
"version": "0.4.16", "version": "0.4.16",
"resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.16.tgz", "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.16.tgz",
@@ -17406,38 +17282,6 @@
"pend": "~1.2.0" "pend": "~1.2.0"
} }
}, },
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/fetch-blob/node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -17644,18 +17488,6 @@
"node": ">= 12.20" "node": ">= 12.20"
} }
}, },
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -17789,112 +17621,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/gaxios": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
"integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"node-fetch": "^3.3.2",
"rimraf": "^5.0.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/gaxios/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/gaxios/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/gaxios/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/gaxios/node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/gaxios/node_modules/rimraf": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
"integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
"license": "ISC",
"dependencies": {
"glob": "^10.3.7"
},
"bin": {
"rimraf": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/gcp-metadata": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
"integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^7.0.0",
"google-logging-utils": "^1.0.0",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/generator-function": { "node_modules/generator-function": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
@@ -18126,33 +17852,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/google-auth-library": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz",
"integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^7.0.0",
"gcp-metadata": "^8.0.0",
"google-logging-utils": "^1.0.0",
"gtoken": "^8.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/google-logging-utils": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
"integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -18205,19 +17904,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/gtoken": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
"integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
"license": "MIT",
"dependencies": {
"gaxios": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/gzip-size": { "node_modules/gzip-size": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
@@ -18522,6 +18208,7 @@
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"agent-base": "^7.1.2", "agent-base": "^7.1.2",
@@ -19458,6 +19145,7 @@
"version": "3.4.3", "version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"@isaacs/cliui": "^8.0.2" "@isaacs/cliui": "^8.0.2"
@@ -19590,15 +19278,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"license": "MIT",
"dependencies": {
"bignumber.js": "^9.0.0"
}
},
"node_modules/json-buffer": { "node_modules/json-buffer": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -19682,27 +19361,6 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -24084,6 +23742,7 @@
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",

View File

@@ -37,7 +37,6 @@
"@ai-sdk/deepseek": "^2.0.0", "@ai-sdk/deepseek": "^2.0.0",
"@ai-sdk/gateway": "^3.0.0", "@ai-sdk/gateway": "^3.0.0",
"@ai-sdk/google": "^3.0.0", "@ai-sdk/google": "^3.0.0",
"@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",
"@aws-sdk/client-dynamodb": "^3.957.0", "@aws-sdk/client-dynamodb": "^3.957.0",
@@ -50,7 +49,6 @@
"@next/third-parties": "^16.0.6", "@next/third-parties": "^16.0.6",
"@opennextjs/cloudflare": "1.14.8", "@opennextjs/cloudflare": "1.14.8",
"@openrouter/ai-sdk-provider": "^1.5.4", "@openrouter/ai-sdk-provider": "^1.5.4",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.209.0", "@opentelemetry/exporter-trace-otlp-http": "^0.209.0",
"@opentelemetry/sdk-trace-node": "^2.2.0", "@opentelemetry/sdk-trace-node": "^2.2.0",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-alert-dialog": "^1.1.15",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@next-ai-drawio/mcp-server", "name": "@next-ai-drawio/mcp-server",
"version": "0.1.13", "version": "0.1.12",
"description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview", "description": "MCP server for Next AI Draw.io - AI-powered diagram generation with real-time browser preview",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -260,7 +260,6 @@ COMMON STYLES:
// Update session state // Update session state
currentSession.xml = xml currentSession.xml = xml
currentSession.version++ currentSession.version++
currentSession.lastGetDiagramTime = Date.now()
// Push to embedded server state // Push to embedded server state
setState(currentSession.id, xml) setState(currentSession.id, xml)

View File

@@ -1,85 +0,0 @@
import { afterEach, describe, expect, it } from "vitest"
import {
loadFlattenedServerModels,
type ServerModelsConfig,
ServerModelsConfigSchema,
} from "@/lib/server-model-config"
const ORIGINAL_ENV = { ...process.env }
afterEach(() => {
process.env.AI_PROVIDER = ORIGINAL_ENV.AI_PROVIDER
process.env.AI_MODEL = ORIGINAL_ENV.AI_MODEL
process.env.AI_MODELS_CONFIG_PATH = ORIGINAL_ENV.AI_MODELS_CONFIG_PATH
process.env.AI_MODELS_CONFIG = ORIGINAL_ENV.AI_MODELS_CONFIG
})
describe("ServerModelsConfigSchema", () => {
it("accepts valid provider names", () => {
const config: ServerModelsConfig = {
providers: [
{
name: "OpenAI Server",
provider: "openai",
models: ["gpt-4o"],
},
],
}
expect(() => ServerModelsConfigSchema.parse(config)).not.toThrow()
})
it("rejects invalid provider names", () => {
const invalidConfig = {
providers: [
{
name: "Invalid Provider",
// Cast to any so we can verify runtime validation, not TypeScript
provider: "invalid-provider" as any,
models: ["model-1"],
},
],
}
expect(() =>
ServerModelsConfigSchema.parse(invalidConfig as any),
).toThrow()
})
})
describe("loadFlattenedServerModels", () => {
it("returns empty array when config file is missing", async () => {
// Point to a non-existent config path so fs.readFile throws ENOENT
process.env.AI_MODELS_CONFIG_PATH = `non-existent-config-${Date.now()}.json`
const models = await loadFlattenedServerModels()
expect(models).toEqual([])
})
it("flattens providers and marks default model from env var config", async () => {
// Use AI_MODELS_CONFIG env var instead of file
const config: ServerModelsConfig = {
providers: [
{
name: "OpenAI Server",
provider: "openai",
models: ["gpt-4o", "gpt-4o-mini"],
default: true,
},
],
}
process.env.AI_MODELS_CONFIG = JSON.stringify(config)
process.env.AI_MODELS_CONFIG_PATH = "" // Clear file path
const models = await loadFlattenedServerModels()
expect(models.length).toBe(2)
const defaults = models.filter((m) => m.isDefault)
expect(defaults.length).toBe(1)
const defaultModel = defaults[0]
expect(defaultModel.provider).toBe("openai")
expect(defaultModel.modelId).toBe("gpt-4o") // First model of default provider
})
})