mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-09-01 17:10:24 +08:00
Compare commits
6 Commits
feat/mater
...
fix/electr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c083782802 | ||
|
|
a5d1554c3f | ||
|
|
e171fbcdd8 | ||
|
|
89d3968733 | ||
|
|
ac3570c1b0 | ||
|
|
41dc0b2b42 |
@@ -663,7 +663,7 @@ Available libraries:
|
||||
- Networking: cisco19, network, kubernetes, vvd, rack
|
||||
- Business: bpmn, lean_mapping
|
||||
- General: flowchart, basic, arrows2, infographic, sitemap
|
||||
- UI/Mockups: android
|
||||
- UI/Mockups: android, material_design
|
||||
- Enterprise: citrix, sap, mscae, atlassian
|
||||
- Engineering: fluidpower, electrical, pid, cabinets, floorplan
|
||||
- Icons: webicons
|
||||
@@ -708,7 +708,7 @@ Call this tool to get shape names and usage syntax for a specific library.`,
|
||||
if (
|
||||
(error as NodeJS.ErrnoException).code === "ENOENT"
|
||||
) {
|
||||
return `Library "${library}" not found. Available: aws4, azure2, gcp2, alibaba_cloud, cisco19, kubernetes, network, bpmn, flowchart, basic, arrows2, vvd, salesforce, citrix, sap, mscae, atlassian, fluidpower, electrical, pid, cabinets, floorplan, webicons, infographic, sitemap, android, lean_mapping, openstack, rack`
|
||||
return `Library "${library}" not found. Available: aws4, azure2, gcp2, alibaba_cloud, cisco19, kubernetes, network, bpmn, flowchart, basic, arrows2, vvd, salesforce, citrix, sap, mscae, atlassian, fluidpower, electrical, pid, cabinets, floorplan, webicons, infographic, sitemap, android, material_design, lean_mapping, openstack, rack`
|
||||
}
|
||||
console.error(
|
||||
`[get_shape_library] Error loading "${library}":`,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { allowPrivateUrls, isPrivateUrl } from "@/lib/ssrf-protection"
|
||||
|
||||
const MAX_CONTENT_LENGTH = 150000 // Match PDF limit
|
||||
const EXTRACT_TIMEOUT_MS = 15000
|
||||
const USER_AGENT = "Mozilla/5.0 (compatible; NextAIDrawio/1.0)"
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
@@ -34,6 +35,31 @@ export async function POST(req: Request) {
|
||||
{ status: 400 },
|
||||
)
|
||||
}
|
||||
const headController = new AbortController()
|
||||
const headTimeout = setTimeout(() => headController.abort(), 3000)
|
||||
try {
|
||||
const headResponse = await fetch(url, {
|
||||
method: "HEAD",
|
||||
headers: { "User-Agent": USER_AGENT },
|
||||
signal: headController.signal,
|
||||
})
|
||||
const contentType = headResponse.headers.get("content-type")
|
||||
if (contentType?.includes("application/pdf")) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "PDF URLs are not supported. Please download and upload the PDF file directly",
|
||||
},
|
||||
{ status: 422 },
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"HEAD pre-check failed, proceeding with extraction:",
|
||||
err,
|
||||
)
|
||||
} finally {
|
||||
clearTimeout(headTimeout)
|
||||
}
|
||||
|
||||
// Extract article content with timeout to avoid tying up server resources
|
||||
const controller = new AbortController()
|
||||
@@ -44,9 +70,7 @@ export async function POST(req: Request) {
|
||||
let article
|
||||
try {
|
||||
article = await extract(url, undefined, {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (compatible; NextAIDrawio/1.0)",
|
||||
},
|
||||
headers: { "User-Agent": USER_AGENT },
|
||||
signal: controller.signal,
|
||||
})
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -174,10 +174,21 @@ export async function POST(req: Request) {
|
||||
}
|
||||
|
||||
case "ollama": {
|
||||
const ollama = createOllama({
|
||||
baseURL: baseUrl || "http://localhost:11434",
|
||||
// SECURITY: Mirror ai-providers.ts guard — only use server
|
||||
// OLLAMA_API_KEY when the URL is also from server config.
|
||||
const ollamaApiKey = baseUrl
|
||||
? apiKey || undefined
|
||||
: apiKey || process.env.OLLAMA_API_KEY || undefined
|
||||
const ollamaProvider = createOllama({
|
||||
baseURL:
|
||||
baseUrl ||
|
||||
process.env.OLLAMA_BASE_URL ||
|
||||
"https://ollama.com/api",
|
||||
...(ollamaApiKey && {
|
||||
headers: { Authorization: `Bearer ${ollamaApiKey}` },
|
||||
}),
|
||||
})
|
||||
model = ollama(modelId)
|
||||
model = ollamaProvider(modelId)
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -282,6 +282,7 @@ export function ModelConfigDialog({
|
||||
// Check credentials based on provider type
|
||||
const isBedrock = selectedProvider.provider === "bedrock"
|
||||
const isEdgeOne = selectedProvider.provider === "edgeone"
|
||||
const isOllama = selectedProvider.provider === "ollama"
|
||||
const isVertexAI = selectedProvider.provider === "vertexai"
|
||||
if (isBedrock) {
|
||||
if (
|
||||
@@ -296,7 +297,7 @@ export function ModelConfigDialog({
|
||||
if (!selectedProvider.vertexApiKey) {
|
||||
return
|
||||
}
|
||||
} else if (!isEdgeOne && !selectedProvider.apiKey) {
|
||||
} else if (!isEdgeOne && !isOllama && !selectedProvider.apiKey) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1030,9 +1031,7 @@ export function ModelConfigDialog({
|
||||
</div>
|
||||
</>
|
||||
) : selectedProvider.provider ===
|
||||
"ollama" ||
|
||||
selectedProvider.provider ===
|
||||
"edgeone" ? (
|
||||
"edgeone" ? (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
@@ -1100,6 +1099,9 @@ export function ModelConfigDialog({
|
||||
dict.modelConfig
|
||||
.apiKey
|
||||
}
|
||||
{selectedProvider.provider ===
|
||||
"ollama" &&
|
||||
` ${dict.modelConfig.optional}`}
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
@@ -1163,7 +1165,9 @@ export function ModelConfigDialog({
|
||||
handleValidate
|
||||
}
|
||||
disabled={
|
||||
!selectedProvider.apiKey ||
|
||||
(selectedProvider.provider !==
|
||||
"ollama" &&
|
||||
!selectedProvider.apiKey) ||
|
||||
validationStatus ===
|
||||
"validating"
|
||||
}
|
||||
|
||||
367
docs/shape-libraries/material_design.md
Normal file
367
docs/shape-libraries/material_design.md
Normal file
@@ -0,0 +1,367 @@
|
||||
# material_design
|
||||
|
||||
**Type:** SVG images (Google Material Icons CDN)
|
||||
**URL Pattern:** `https://fonts.gstatic.com/s/i/materialicons/{icon_name}/v6/24px.svg`
|
||||
|
||||
## Usage
|
||||
|
||||
```xml
|
||||
<mxCell value="label" style="image;aspect=fixed;html=1;image=https://fonts.gstatic.com/s/i/materialicons/{icon_name}/v6/24px.svg;verticalLabelPosition=bottom;verticalAlign=top;align=center;" vertex="1" parent="1">
|
||||
<mxGeometry x="0" y="0" width="48" height="48" as="geometry" />
|
||||
</mxCell>
|
||||
```
|
||||
|
||||
Replace `{icon_name}` with any icon name from the list below.
|
||||
|
||||
## action (115)
|
||||
|
||||
- `account_balance`
|
||||
- `account_balance_wallet`
|
||||
- `account_box`
|
||||
- `account_circle`
|
||||
- `add_shopping_cart`
|
||||
- `admin_panel_settings`
|
||||
- `analytics`
|
||||
- `arrow_right_alt`
|
||||
- `article`
|
||||
- `assessment`
|
||||
- `assignment`
|
||||
- `assignment_ind`
|
||||
- `assignment_turned_in`
|
||||
- `autorenew`
|
||||
- `bookmark`
|
||||
- `bookmark_border`
|
||||
- `build`
|
||||
- `calendar_month`
|
||||
- `calendar_today`
|
||||
- `card_giftcard`
|
||||
- `check_circle`
|
||||
- `check_circle_outline`
|
||||
- `code`
|
||||
- `contact_support`
|
||||
- `credit_card`
|
||||
- `dashboard`
|
||||
- `date_range`
|
||||
- `delete`
|
||||
- `delete_forever`
|
||||
- `delete_outline`
|
||||
- `description`
|
||||
- `dns`
|
||||
- `done`
|
||||
- `done_all`
|
||||
- `done_outline`
|
||||
- `drag_indicator`
|
||||
- `event`
|
||||
- `exit_to_app`
|
||||
- `explore`
|
||||
- `face`
|
||||
- `fact_check`
|
||||
- `favorite`
|
||||
- `favorite_border`
|
||||
- `feedback`
|
||||
- `filter_alt`
|
||||
- `fingerprint`
|
||||
- `flight_takeoff`
|
||||
- `grade`
|
||||
- `help`
|
||||
- `help_outline`
|
||||
- `highlight_off`
|
||||
- `history`
|
||||
- `home`
|
||||
- `info`
|
||||
- `label`
|
||||
- `language`
|
||||
- `launch`
|
||||
- `leaderboard`
|
||||
- `lightbulb`
|
||||
- `list`
|
||||
- `lock`
|
||||
- `lock_open`
|
||||
- `login`
|
||||
- `logout`
|
||||
- `manage_accounts`
|
||||
- `note_add`
|
||||
- `open_in_full`
|
||||
- `open_in_new`
|
||||
- `paid`
|
||||
- `payment`
|
||||
- `pending`
|
||||
- `pending_actions`
|
||||
- `perm_identity`
|
||||
- `pets`
|
||||
- `power_settings_new`
|
||||
- `preview`
|
||||
- `print`
|
||||
- `published_with_changes`
|
||||
- `question_answer`
|
||||
- `receipt`
|
||||
- `reorder`
|
||||
- `report_problem`
|
||||
- `room`
|
||||
- `savings`
|
||||
- `schedule`
|
||||
- `search`
|
||||
- `settings`
|
||||
- `shopping_bag`
|
||||
- `shopping_basket`
|
||||
- `shopping_cart`
|
||||
- `star_rate`
|
||||
- `stars`
|
||||
- `store`
|
||||
- `supervisor_account`
|
||||
- `swap_horiz`
|
||||
- `sync_alt`
|
||||
- `task_alt`
|
||||
- `thumb_up`
|
||||
- `thumb_up_off_alt`
|
||||
- `timeline`
|
||||
- `tips_and_updates`
|
||||
- `today`
|
||||
- `touch_app`
|
||||
- `trending_up`
|
||||
- `update`
|
||||
- `verified`
|
||||
- `verified_user`
|
||||
- `view_in_ar`
|
||||
- `view_list`
|
||||
- `visibility`
|
||||
- `visibility_off`
|
||||
- `watch_later`
|
||||
- `work`
|
||||
- `work_outline`
|
||||
- `zoom_in`
|
||||
|
||||
## alert (4)
|
||||
|
||||
- `error`
|
||||
- `error_outline`
|
||||
- `warning`
|
||||
- `warning_amber`
|
||||
|
||||
## av (12)
|
||||
|
||||
- `library_books`
|
||||
- `mic`
|
||||
- `pause`
|
||||
- `play_arrow`
|
||||
- `play_circle`
|
||||
- `play_circle_filled`
|
||||
- `play_circle_outline`
|
||||
- `replay`
|
||||
- `skip_next`
|
||||
- `videocam`
|
||||
- `volume_off`
|
||||
- `volume_up`
|
||||
|
||||
## communication (13)
|
||||
|
||||
- `alternate_email`
|
||||
- `business`
|
||||
- `call`
|
||||
- `chat`
|
||||
- `chat_bubble_outline`
|
||||
- `email`
|
||||
- `forum`
|
||||
- `list_alt`
|
||||
- `location_on`
|
||||
- `mail_outline`
|
||||
- `phone`
|
||||
- `qr_code_scanner`
|
||||
- `vpn_key`
|
||||
|
||||
## content (27)
|
||||
|
||||
- `add`
|
||||
- `add_box`
|
||||
- `add_circle`
|
||||
- `add_circle_outline`
|
||||
- `block`
|
||||
- `bolt`
|
||||
- `calculate`
|
||||
- `clear`
|
||||
- `content_copy`
|
||||
- `create`
|
||||
- `filter_list`
|
||||
- `flag`
|
||||
- `how_to_reg`
|
||||
- `insights`
|
||||
- `inventory`
|
||||
- `inventory_2`
|
||||
- `link`
|
||||
- `mail`
|
||||
- `push_pin`
|
||||
- `remove`
|
||||
- `remove_circle`
|
||||
- `remove_circle_outline`
|
||||
- `reply`
|
||||
- `save`
|
||||
- `send`
|
||||
- `sort`
|
||||
- `undo`
|
||||
|
||||
## device (9)
|
||||
|
||||
- `dark_mode`
|
||||
- `devices`
|
||||
- `light_mode`
|
||||
- `password`
|
||||
- `restart_alt`
|
||||
- `sell`
|
||||
- `signal_cellular_alt`
|
||||
- `summarize`
|
||||
- `task`
|
||||
|
||||
## editor (9)
|
||||
|
||||
- `attach_file`
|
||||
- `attach_money`
|
||||
- `bar_chart`
|
||||
- `checklist`
|
||||
- `edit_note`
|
||||
- `format_list_bulleted`
|
||||
- `mode_edit`
|
||||
- `monetization_on`
|
||||
- `post_add`
|
||||
|
||||
## file (8)
|
||||
|
||||
- `cloud_upload`
|
||||
- `download`
|
||||
- `file_download`
|
||||
- `file_upload`
|
||||
- `folder`
|
||||
- `folder_open`
|
||||
- `grid_view`
|
||||
- `upload_file`
|
||||
|
||||
## hardware (6)
|
||||
|
||||
- `computer`
|
||||
- `keyboard_arrow_down`
|
||||
- `keyboard_arrow_right`
|
||||
- `phone_iphone`
|
||||
- `security`
|
||||
- `smartphone`
|
||||
|
||||
## image (16)
|
||||
|
||||
- `add_a_photo`
|
||||
- `auto_awesome`
|
||||
- `auto_stories`
|
||||
- `circle`
|
||||
- `collections`
|
||||
- `edit`
|
||||
- `image`
|
||||
- `navigate_before`
|
||||
- `navigate_next`
|
||||
- `palette`
|
||||
- `photo_camera`
|
||||
- `picture_as_pdf`
|
||||
- `receipt_long`
|
||||
- `remove_red_eye`
|
||||
- `timer`
|
||||
- `tune`
|
||||
|
||||
## maps (11)
|
||||
|
||||
- `badge`
|
||||
- `category`
|
||||
- `directions_car`
|
||||
- `local_fire_department`
|
||||
- `local_offer`
|
||||
- `local_shipping`
|
||||
- `map`
|
||||
- `menu_book`
|
||||
- `place`
|
||||
- `restaurant`
|
||||
- `volunteer_activism`
|
||||
|
||||
## navigation (29)
|
||||
|
||||
- `apps`
|
||||
- `arrow_back`
|
||||
- `arrow_back_ios`
|
||||
- `arrow_back_ios_new`
|
||||
- `arrow_downward`
|
||||
- `arrow_drop_down`
|
||||
- `arrow_drop_up`
|
||||
- `arrow_forward`
|
||||
- `arrow_forward_ios`
|
||||
- `arrow_right`
|
||||
- `arrow_upward`
|
||||
- `campaign`
|
||||
- `cancel`
|
||||
- `check`
|
||||
- `chevron_left`
|
||||
- `chevron_right`
|
||||
- `close`
|
||||
- `double_arrow`
|
||||
- `east`
|
||||
- `expand_less`
|
||||
- `expand_more`
|
||||
- `fullscreen`
|
||||
- `menu`
|
||||
- `menu_open`
|
||||
- `more_horiz`
|
||||
- `more_vert`
|
||||
- `payments`
|
||||
- `refresh`
|
||||
- `unfold_more`
|
||||
|
||||
## notification (6)
|
||||
|
||||
- `account_tree`
|
||||
- `event_available`
|
||||
- `priority_high`
|
||||
- `support_agent`
|
||||
- `sync`
|
||||
- `wifi`
|
||||
|
||||
## places (2)
|
||||
|
||||
- `apartment`
|
||||
- `storefront`
|
||||
|
||||
## search (2)
|
||||
|
||||
- `feed`
|
||||
- `manage_search`
|
||||
|
||||
## social (23)
|
||||
|
||||
- `construction`
|
||||
- `emoji_emotions`
|
||||
- `emoji_events`
|
||||
- `engineering`
|
||||
- `group`
|
||||
- `group_add`
|
||||
- `groups`
|
||||
- `health_and_safety`
|
||||
- `notifications`
|
||||
- `notifications_active`
|
||||
- `notifications_none`
|
||||
- `people`
|
||||
- `people_alt`
|
||||
- `person`
|
||||
- `person_add`
|
||||
- `person_outline`
|
||||
- `psychology`
|
||||
- `public`
|
||||
- `school`
|
||||
- `share`
|
||||
- `thumb_up_alt`
|
||||
- `travel_explore`
|
||||
- `water_drop`
|
||||
|
||||
## toggle (8)
|
||||
|
||||
- `check_box`
|
||||
- `check_box_outline_blank`
|
||||
- `radio_button_checked`
|
||||
- `radio_button_unchecked`
|
||||
- `star`
|
||||
- `star_border`
|
||||
- `star_outline`
|
||||
- `toggle_on`
|
||||
|
||||
Total: 300 icons (top by popularity from 2100+ available)
|
||||
@@ -359,9 +359,9 @@ const PROVIDER_ENV_MAP: Record<string, { apiKey: string; baseUrl: string }> = {
|
||||
baseUrl: "MODELSCOPE_BASE_URL",
|
||||
},
|
||||
gateway: { apiKey: "AI_GATEWAY_API_KEY", baseUrl: "AI_GATEWAY_BASE_URL" },
|
||||
// bedrock and ollama don't use API keys in the same way
|
||||
// bedrock doesn't use API keys in the same way
|
||||
bedrock: { apiKey: "", baseUrl: "" },
|
||||
ollama: { apiKey: "", baseUrl: "OLLAMA_BASE_URL" },
|
||||
ollama: { apiKey: "OLLAMA_API_KEY", baseUrl: "OLLAMA_BASE_URL" },
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -94,7 +94,8 @@ if (!gotTheLock) {
|
||||
if (
|
||||
url.includes("diagrams.net") ||
|
||||
url.includes("draw.io") ||
|
||||
url.startsWith("http://localhost")
|
||||
url.startsWith("http://localhost") ||
|
||||
url.startsWith("http://127.0.0.1")
|
||||
) {
|
||||
return { action: "allow" }
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export async function startNextServer(): Promise<string> {
|
||||
const env: Record<string, string> = {
|
||||
NODE_ENV: "production",
|
||||
PORT: String(port),
|
||||
HOSTNAME: "localhost",
|
||||
HOSTNAME: "127.0.0.1",
|
||||
// Enable Node.js built-in proxy support for fetch (Node.js 24+)
|
||||
NODE_USE_ENV_PROXY: "1",
|
||||
}
|
||||
|
||||
@@ -9,9 +9,11 @@ import { app } from "electron"
|
||||
const PORT_CONFIG = {
|
||||
// Development mode uses fixed port for hot reload compatibility
|
||||
development: 6002,
|
||||
// Production mode uses fixed port (61337) to preserve localStorage
|
||||
// Falls back to sequential ports if unavailable
|
||||
production: 61337,
|
||||
// Legacy production port — tried first to preserve localStorage for existing users
|
||||
legacyProduction: 61337,
|
||||
// New production port below the ephemeral range (49152-65535)
|
||||
// to avoid conflicts with Windows Hyper-V / ephemeral port reservations
|
||||
production: 13370,
|
||||
// Maximum attempts to find an available port (fallback)
|
||||
maxAttempts: 100,
|
||||
}
|
||||
@@ -27,7 +29,10 @@ let allocatedPort: number | null = null
|
||||
export function isPortAvailable(port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const server = net.createServer()
|
||||
server.once("error", () => resolve(false))
|
||||
server.once("error", (err: NodeJS.ErrnoException) => {
|
||||
console.warn(`Port ${port} unavailable: ${err.code}`)
|
||||
resolve(false)
|
||||
})
|
||||
server.once("listening", () => {
|
||||
server.close()
|
||||
resolve(true)
|
||||
@@ -39,12 +44,12 @@ export function isPortAvailable(port: number): Promise<boolean> {
|
||||
/**
|
||||
* Find an available port
|
||||
* - In development: uses fixed port (6002)
|
||||
* - In production: uses fixed port (61337) to preserve localStorage
|
||||
* - In production: uses fixed port (13370) to preserve localStorage
|
||||
* - Falls back to sequential ports if preferred port is unavailable
|
||||
* - Last resort: lets the OS assign a port (port 0)
|
||||
*
|
||||
* @param reuseExisting If true, try to reuse the previously allocated port
|
||||
* @returns Promise<number> The available port
|
||||
* @throws Error if no available port found after max attempts
|
||||
*/
|
||||
export async function findAvailablePort(reuseExisting = true): Promise<number> {
|
||||
const isDev = !app.isPackaged
|
||||
@@ -64,7 +69,16 @@ export async function findAvailablePort(reuseExisting = true): Promise<number> {
|
||||
allocatedPort = null
|
||||
}
|
||||
|
||||
// Try preferred port first
|
||||
// In production, try legacy port first to preserve existing users' localStorage
|
||||
if (!isDev) {
|
||||
const legacyPort = PORT_CONFIG.legacyProduction
|
||||
if (await isPortAvailable(legacyPort)) {
|
||||
allocatedPort = legacyPort
|
||||
return legacyPort
|
||||
}
|
||||
}
|
||||
|
||||
// Try preferred port
|
||||
if (await isPortAvailable(preferredPort)) {
|
||||
allocatedPort = preferredPort
|
||||
return preferredPort
|
||||
@@ -84,9 +98,23 @@ export async function findAvailablePort(reuseExisting = true): Promise<number> {
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Failed to find available port after ${PORT_CONFIG.maxAttempts} attempts`,
|
||||
// Last resort: let the OS pick an available port
|
||||
console.warn(
|
||||
"All sequential ports failed. Requesting OS-assigned port (localStorage may not persist across restarts).",
|
||||
)
|
||||
const osPort = await new Promise<number>((resolve, reject) => {
|
||||
const server = net.createServer()
|
||||
server.once("error", reject)
|
||||
server.once("listening", () => {
|
||||
const addr = server.address()
|
||||
const port = (addr as net.AddressInfo).port
|
||||
server.close(() => resolve(port))
|
||||
})
|
||||
server.listen(0, "127.0.0.1")
|
||||
})
|
||||
allocatedPort = osPort
|
||||
console.log(`OS assigned port: ${osPort}`)
|
||||
return osPort
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,5 +141,5 @@ export function getServerUrl(): string {
|
||||
"No port allocated yet. Call findAvailablePort() first.",
|
||||
)
|
||||
}
|
||||
return `http://localhost:${allocatedPort}`
|
||||
return `http://127.0.0.1:${allocatedPort}`
|
||||
}
|
||||
|
||||
@@ -66,7 +66,11 @@ export function createWindow(serverUrl: string): BrowserWindow {
|
||||
|
||||
// Handle page title updates
|
||||
mainWindow.webContents.on("page-title-updated", (event, title) => {
|
||||
if (title && !title.includes("localhost")) {
|
||||
if (
|
||||
title &&
|
||||
!title.includes("localhost") &&
|
||||
!title.includes("127.0.0.1")
|
||||
) {
|
||||
mainWindow?.setTitle(title)
|
||||
} else {
|
||||
event.preventDefault()
|
||||
|
||||
@@ -59,8 +59,9 @@ AI_MODEL=global.anthropic.claude-sonnet-4-5-20250929-v1:0
|
||||
# AZURE_REASONING_EFFORT=low # Optional: Azure reasoning effort (low, medium, high)
|
||||
# AZURE_REASONING_SUMMARY=detailed
|
||||
|
||||
# Ollama (Local) Configuration
|
||||
# OLLAMA_BASE_URL=http://localhost:11434/api # Optional, defaults to localhost
|
||||
# Ollama Configuration (Local or Cloud)
|
||||
# OLLAMA_BASE_URL=https://ollama.com/api # Optional, defaults to Ollama Cloud
|
||||
# OLLAMA_API_KEY=your-ollama-cloud-api-key # Optional: For Ollama Cloud or authenticated remote instances
|
||||
# OLLAMA_ENABLE_THINKING=true # Optional: Enable thinking for models that support it (e.g., qwen3)
|
||||
|
||||
# OpenRouter Configuration
|
||||
|
||||
@@ -596,7 +596,7 @@ function validateProviderCredentials(
|
||||
* - GOOGLE_GENERATIVE_AI_API_KEY: Google API key
|
||||
* - AZURE_RESOURCE_NAME, AZURE_API_KEY: Azure OpenAI credentials
|
||||
* - AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY: AWS Bedrock credentials
|
||||
* - OLLAMA_BASE_URL: Ollama server URL (optional, defaults to http://localhost:11434)
|
||||
* - OLLAMA_BASE_URL: Ollama server URL (optional, defaults to https://ollama.com/api)
|
||||
* - OPENROUTER_API_KEY: OpenRouter API key
|
||||
* - DEEPSEEK_API_KEY: DeepSeek API key
|
||||
* - DEEPSEEK_BASE_URL: DeepSeek endpoint (optional)
|
||||
@@ -611,13 +611,15 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
||||
// SECURITY: Prevent SSRF attacks (GHSA-9qf7-mprq-9qgm)
|
||||
// If a custom baseUrl is provided, an API key MUST also be provided.
|
||||
// This prevents attackers from redirecting server API keys to malicious endpoints.
|
||||
// Exception: EdgeOne and Ollama providers don't require API keys
|
||||
// Exception: EdgeOne doesn't require API keys.
|
||||
// Ollama is exempt only when no server OLLAMA_API_KEY is configured;
|
||||
// when it IS configured, the outer guard also enforces client apiKey for custom baseUrls.
|
||||
if (
|
||||
overrides?.baseUrl &&
|
||||
!overrides?.apiKey &&
|
||||
!(overrides?.provider === "vertexai" && overrides?.vertexApiKey) &&
|
||||
overrides?.provider !== "edgeone" &&
|
||||
overrides?.provider !== "ollama"
|
||||
!(overrides?.provider === "ollama" && !process.env.OLLAMA_API_KEY)
|
||||
) {
|
||||
throw new Error(
|
||||
`API key is required when using a custom base URL. ` +
|
||||
@@ -878,8 +880,19 @@ export function getAIModel(overrides?: ClientOverrides): ModelConfig {
|
||||
|
||||
case "ollama": {
|
||||
const baseURL = overrides?.baseUrl || process.env.OLLAMA_BASE_URL
|
||||
if (baseURL) {
|
||||
const customOllama = createOllama({ baseURL })
|
||||
// SECURITY: When client provides a custom base URL, only use
|
||||
// client-provided API key. Never fall back to server OLLAMA_API_KEY
|
||||
// to prevent leaking server credentials to user-controlled endpoints.
|
||||
const apiKey = overrides?.baseUrl
|
||||
? overrides?.apiKey || undefined
|
||||
: resolveApiKey(overrides, "OLLAMA_API_KEY")
|
||||
if (baseURL || apiKey) {
|
||||
const customOllama = createOllama({
|
||||
...(baseURL && { baseURL }),
|
||||
...(apiKey && {
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
}),
|
||||
})
|
||||
model = customOllama(modelId)
|
||||
} else {
|
||||
model = ollama(modelId)
|
||||
@@ -1217,7 +1230,12 @@ export function supportsImageInput(modelId: string): boolean {
|
||||
}
|
||||
|
||||
// Qwen text models (not vision variants like qwen-vl)
|
||||
if (lowerModelId.includes("qwen") && !hasVisionIndicator) {
|
||||
// qwen3.5-plus is a vision model
|
||||
if (
|
||||
lowerModelId.includes("qwen") &&
|
||||
!hasVisionIndicator &&
|
||||
!lowerModelId.includes("qwen3.5-plus")
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -51,9 +51,9 @@ parameters: {
|
||||
}
|
||||
---Tool4---
|
||||
tool name: get_shape_library
|
||||
description: Get shape/icon library documentation. Use this to discover available icon shapes (AWS, Azure, GCP, Kubernetes, etc.) before creating diagrams with cloud/tech icons.
|
||||
description: Get shape/icon library documentation. Use this to discover available icon shapes (AWS, Azure, GCP, Kubernetes, Material Design, etc.) before creating diagrams with special icons. ALWAYS call this before using any icon library — never guess the syntax.
|
||||
parameters: {
|
||||
library: string // Library name: aws4, azure2, gcp2, kubernetes, cisco19, flowchart, bpmn, etc.
|
||||
library: string // Library name: aws4, azure2, gcp2, kubernetes, cisco19, flowchart, bpmn, material_design, etc.
|
||||
}
|
||||
---End of tools---
|
||||
|
||||
@@ -61,7 +61,7 @@ IMPORTANT: Choose the right tool:
|
||||
- Use display_diagram for: Creating new diagrams, major restructuring, or when the current diagram XML is empty
|
||||
- Use edit_diagram for: Small modifications, adding/removing elements, changing text/colors, repositioning items
|
||||
- Use append_diagram for: ONLY when display_diagram was truncated due to output length - continue generating from where you stopped
|
||||
- Use get_shape_library for: Discovering available icons/shapes when creating cloud architecture or technical diagrams (call BEFORE display_diagram)
|
||||
- Use get_shape_library for: Discovering available icons/shapes when creating diagrams with any icon library (cloud, material design, etc.) — call BEFORE display_diagram
|
||||
|
||||
Core capabilities:
|
||||
- Generate valid, well-formed XML strings for draw.io diagrams
|
||||
@@ -92,7 +92,7 @@ Note that:
|
||||
- When artistic drawings are requested, creatively compose them using standard diagram shapes and connectors while maintaining visual clarity.
|
||||
- Return XML only via tool calls, never in text responses.
|
||||
- If user asks you to replicate a diagram based on an image, remember to match the diagram style and layout as closely as possible. Especially, pay attention to the lines and shapes, for example, if the lines are straight or curved, and if the shapes are rounded or square.
|
||||
- For cloud/tech diagrams (AWS, Azure, GCP, K8s), call get_shape_library first to discover available icon shapes and their syntax.
|
||||
- For cloud/tech diagrams (AWS, Azure, GCP, K8s) or when using icon libraries (material_design, webicons, etc.), call get_shape_library first to discover available icon shapes and their correct syntax. NEVER guess icon style syntax — always look it up first.
|
||||
- NEVER include XML comments (<!-- ... -->) in your generated XML. Draw.io strips comments, which breaks edit_diagram patterns.
|
||||
|
||||
When using edit_diagram tool:
|
||||
|
||||
@@ -104,7 +104,7 @@ export const PROVIDER_INFO: Record<
|
||||
bedrock: { label: "Amazon Bedrock" },
|
||||
ollama: {
|
||||
label: "Ollama",
|
||||
defaultBaseUrl: "http://localhost:11434",
|
||||
defaultBaseUrl: "https://ollama.com/api",
|
||||
},
|
||||
openrouter: {
|
||||
label: "OpenRouter",
|
||||
@@ -264,6 +264,7 @@ export const SUGGESTED_MODELS: Partial<Record<ProviderName, string[]>> = {
|
||||
"Qwen/Qwen2.5-Coder-32B-Instruct",
|
||||
"Qwen/Qwen2.5-7B-Instruct",
|
||||
"Qwen/Qwen2-VL-72B-Instruct",
|
||||
"qwen3.5-plus",
|
||||
],
|
||||
sglang: [
|
||||
// SGLang is OpenAI-compatible, models depend on deployment
|
||||
@@ -293,6 +294,7 @@ export const SUGGESTED_MODELS: Partial<Record<ProviderName, string[]>> = {
|
||||
"Qwen/Qwen3-235B-A22B-Instruct-2507",
|
||||
"Qwen/Qwen3-VL-235B-A22B-Instruct",
|
||||
"Qwen/Qwen3-32B",
|
||||
"qwen3.5-plus",
|
||||
// DeepSeek
|
||||
"deepseek-ai/DeepSeek-R1-0528",
|
||||
"deepseek-ai/DeepSeek-V3.2",
|
||||
|
||||
1247
package-lock.json
generated
1247
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,7 @@
|
||||
"@aws-sdk/client-dynamodb": "^3.957.0",
|
||||
"@aws-sdk/credential-providers": "^3.943.0",
|
||||
"@extractus/article-extractor": "^8.0.18",
|
||||
"@formatjs/intl-localematcher": "^0.7.2",
|
||||
"@formatjs/intl-localematcher": "^0.8.0",
|
||||
"@langfuse/client": "^4.4.9",
|
||||
"@langfuse/otel": "^4.4.4",
|
||||
"@langfuse/tracing": "^4.4.9",
|
||||
@@ -51,7 +51,7 @@
|
||||
"@opennextjs/cloudflare": "1.16.1",
|
||||
"@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.211.0",
|
||||
"@opentelemetry/sdk-trace-node": "^2.2.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
@@ -72,7 +72,7 @@
|
||||
"cmdk": "^1.1.1",
|
||||
"idb": "^8.0.3",
|
||||
"jsonrepair": "^3.13.1",
|
||||
"lucide-react": "^0.562.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
"motion": "^12.23.25",
|
||||
"nanoid": "^5.0.0",
|
||||
"negotiator": "^1.0.0",
|
||||
|
||||
69
packages/mcp-server/package-lock.json
generated
69
packages/mcp-server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@next-ai-drawio/mcp-server",
|
||||
"version": "0.1.12",
|
||||
"version": "0.1.16",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@next-ai-drawio/mcp-server",
|
||||
"version": "0.1.12",
|
||||
"version": "0.1.16",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
@@ -469,9 +469,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@hono/node-server": {
|
||||
"version": "1.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz",
|
||||
"integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==",
|
||||
"version": "1.19.9",
|
||||
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz",
|
||||
"integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
@@ -481,12 +481,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz",
|
||||
"integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==",
|
||||
"version": "1.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
|
||||
"integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.7",
|
||||
"@hono/node-server": "^1.19.9",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"content-type": "^1.0.5",
|
||||
@@ -494,14 +494,15 @@
|
||||
"cross-spawn": "^7.0.5",
|
||||
"eventsource": "^3.0.2",
|
||||
"eventsource-parser": "^3.0.0",
|
||||
"express": "^5.0.1",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"jose": "^6.1.1",
|
||||
"express": "^5.2.1",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"hono": "^4.11.4",
|
||||
"jose": "^6.1.3",
|
||||
"json-schema-typed": "^8.0.2",
|
||||
"pkce-challenge": "^5.0.0",
|
||||
"raw-body": "^3.0.0",
|
||||
"zod": "^3.25 || ^4.0",
|
||||
"zod-to-json-schema": "^3.25.0"
|
||||
"zod-to-json-schema": "^3.25.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -520,9 +521,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.10.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.6.tgz",
|
||||
"integrity": "sha512-B8h60xgJMR/xmgyX9fncRzEW9gCxoJjdenUhke2v1JGOd/V66KopmWrLPXi5oUI4VuiGK+d+HlXJjDRZMj21EQ==",
|
||||
"version": "24.10.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.12.tgz",
|
||||
"integrity": "sha512-68e+T28EbdmLSTkPgs3+UacC6rzmqrcWFPQs1C8mwJhI/r5Uxr0yEuQotczNRROd1gq30NGxee+fo0rSIxpyAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1074,10 +1075,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
|
||||
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
|
||||
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
@@ -1260,9 +1264,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.11.1",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.1.tgz",
|
||||
"integrity": "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg==",
|
||||
"version": "4.11.9",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz",
|
||||
"integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
@@ -1348,6 +1352,15 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -2051,9 +2064,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
|
||||
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
@@ -2061,9 +2074,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod-to-json-schema": {
|
||||
"version": "3.25.0",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz",
|
||||
"integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==",
|
||||
"version": "3.25.1",
|
||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
|
||||
"integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"zod": "^3.25 || ^4"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import {
|
||||
getAIModel,
|
||||
resolveBaseURL,
|
||||
supportsImageInput,
|
||||
supportsPromptCaching,
|
||||
@@ -189,3 +190,120 @@ describe("supportsImageInput", () => {
|
||||
expect(supportsImageInput("gemini-pro")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock("ollama-ai-provider-v2", () => {
|
||||
const mockModel = { modelId: "test-model" }
|
||||
const mockProviderFn = vi.fn(() => mockModel)
|
||||
const mockCreateOllama = vi.fn(() => mockProviderFn)
|
||||
const mockOllama = vi.fn(() => mockModel)
|
||||
return { createOllama: mockCreateOllama, ollama: mockOllama }
|
||||
})
|
||||
|
||||
describe("Ollama API key security", () => {
|
||||
let createOllamaMock: ReturnType<typeof vi.fn>
|
||||
const savedEnv: Record<string, string | undefined> = {}
|
||||
|
||||
beforeEach(async () => {
|
||||
savedEnv.OLLAMA_API_KEY = process.env.OLLAMA_API_KEY
|
||||
savedEnv.OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL
|
||||
delete process.env.OLLAMA_BASE_URL
|
||||
|
||||
const mod = await import("ollama-ai-provider-v2")
|
||||
createOllamaMock = mod.createOllama as ReturnType<typeof vi.fn>
|
||||
createOllamaMock.mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env.OLLAMA_API_KEY = savedEnv.OLLAMA_API_KEY
|
||||
process.env.OLLAMA_BASE_URL = savedEnv.OLLAMA_BASE_URL
|
||||
})
|
||||
|
||||
it("applies server OLLAMA_API_KEY when no client baseUrl is provided", () => {
|
||||
process.env.OLLAMA_API_KEY = "server-secret-key"
|
||||
|
||||
getAIModel({ provider: "ollama", modelId: "llama2" })
|
||||
|
||||
expect(createOllamaMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: "Bearer server-secret-key" },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("does NOT leak server OLLAMA_API_KEY when client provides a custom baseUrl", () => {
|
||||
process.env.OLLAMA_API_KEY = "server-secret-key"
|
||||
|
||||
// When server has OLLAMA_API_KEY, the SSRF guard rejects
|
||||
// client-provided baseUrl without an apiKey outright
|
||||
expect(() =>
|
||||
getAIModel({
|
||||
provider: "ollama",
|
||||
baseUrl: "https://evil-server.com",
|
||||
modelId: "llama2",
|
||||
}),
|
||||
).toThrow("API key is required")
|
||||
})
|
||||
|
||||
it("uses client API key when client provides both baseUrl and apiKey", () => {
|
||||
process.env.OLLAMA_API_KEY = "server-secret-key"
|
||||
|
||||
getAIModel({
|
||||
provider: "ollama",
|
||||
baseUrl: "https://my-ollama.com",
|
||||
apiKey: "client-key",
|
||||
modelId: "llama2",
|
||||
})
|
||||
|
||||
expect(createOllamaMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
baseURL: "https://my-ollama.com",
|
||||
headers: { Authorization: "Bearer client-key" },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("applies both server OLLAMA_BASE_URL and OLLAMA_API_KEY when no client overrides", () => {
|
||||
process.env.OLLAMA_BASE_URL = "https://cloud.ollama.com"
|
||||
process.env.OLLAMA_API_KEY = "server-key"
|
||||
|
||||
getAIModel({ provider: "ollama", modelId: "llama2" })
|
||||
|
||||
expect(createOllamaMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
baseURL: "https://cloud.ollama.com",
|
||||
headers: { Authorization: "Bearer server-key" },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("works when OLLAMA_API_KEY is set but OLLAMA_BASE_URL is not", () => {
|
||||
process.env.OLLAMA_API_KEY = "server-key"
|
||||
delete process.env.OLLAMA_BASE_URL
|
||||
|
||||
getAIModel({ provider: "ollama", modelId: "llama2" })
|
||||
|
||||
expect(createOllamaMock).toHaveBeenCalledTimes(1)
|
||||
const callArgs = createOllamaMock.mock.calls[0][0]
|
||||
expect(callArgs).not.toHaveProperty("baseURL")
|
||||
expect(callArgs).toEqual(
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: "Bearer server-key" },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("allows client custom baseUrl without apiKey when no server OLLAMA_API_KEY", () => {
|
||||
delete process.env.OLLAMA_API_KEY
|
||||
|
||||
getAIModel({
|
||||
provider: "ollama",
|
||||
baseUrl: "https://my-ollama.com",
|
||||
modelId: "llama2",
|
||||
})
|
||||
|
||||
expect(createOllamaMock).toHaveBeenCalledTimes(1)
|
||||
const callArgs = createOllamaMock.mock.calls[0][0]
|
||||
expect(callArgs.baseURL).toBe("https://my-ollama.com")
|
||||
expect(callArgs).not.toHaveProperty("headers")
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user