Measured on Bedrock: Claude Opus 4.7, 4.8 and every Claude 5 model reject
a thinking budget ("thinking.type.enabled") and a temperature, and all of
them accept adaptive thinking. The retry used to drop thinking, so with
a thinking budget set these models did not think at all. It now switches
to adaptive thinking with display "summarized"; without that setting the
models think but send no thinking text to show.
The suggested Bedrock ids had no region prefix, and the newer models only
answer through an inference profile id: "anthropic.claude-sonnet-5-5"
fails with "on-demand throughput isn't supported". Each suggested id was
called once; the Claude ones now start with "global.", Llama, Pixtral and
Nova 2 Lite with "us.". Nova Premier (end of life) and the ids without a
working profile are gone. The docs example had the same problem and a
wrong date.
The suggested models stopped at Claude Opus 4.8 and GPT 5.5. They now
start with Claude Opus 5.5, Sonnet 5.5 and Fable 5.1, and gpt-6.1-sol,
gpt-6-sol, gpt-6-luna and gpt-6-astra (ids checked against the provider
lists). The Bedrock list is unchanged until its ids are checked.
The reasoning summary was only turned on for model ids containing o1,
o3, o4 or gpt-5, so GPT 6 models showed no thinking. It now matches the
o-series and gpt-5 or later by version, like the OpenAI SDK.
- An error object sent inside the stream (OpenRouter's { code, message })
showed as "[object Object]"; its message and status code are read now.
- A problem+json "detail" is added to the message: NVIDIA only said
"Gone" for a retired model. 410 counts as model not found.
- "Cannot connect to API" from the SDK gets the connection hint.
- Text that is only whitespace (Kimi K2.6 sends a space before a tool
call) no longer shows an empty bubble.
- allowSystemInMessages stops the warning on every request. Our system
messages carry cache points; a client's own system messages are already
dropped by the empty-content filter.
draw.io reads any text inside a page as compressed page data, so a stray
text node makes the whole page fail with an atob error. gpt-5-mini sends
new cells with a literal "\n" between the tags; the edit card said
Complete while draw.io showed the error and kept the old diagram.
Validation now reports text between tags, and auto-fix turns a literal
\n, \t or \r between tags into whitespace. Other text goes back to the
model as an error. The compressed data directly under <diagram> is fine.
The "Fetch models" button asks the provider for its models (OpenAI-style
/models, Anthropic, Google, Ollama, OpenRouter, Vercel Gateway, AIHubMix)
and shows them in a searchable picker. This replaces the route that only
worked for AIHubMix.
A snapshot of models.dev (MIT) says which models support tool calls.
Models without them get a "no tool calls" badge in the picker and a hint
in the model list, since drawing needs tool calls. Refresh the snapshot
with scripts/update-model-catalog.mjs.
- lib/llm-errors.ts sorts an error into about a dozen kinds (key
rejected, no access, unknown model, no credit, rate limited, context
too long, no image input, no tool calls, output cut off, provider down,
cannot connect, timeout): first texts that name the cause precisely,
then the HTTP status code, then general texts. It unwraps RetryError and
hides keys and Bearer tokens in the provider's message
- The chat route uses it for errors before the stream and, through
toUIMessageStreamResponse's onError, for errors in the stream. Errors
of the model's own tool call stay as they are: the same text goes back
to the model so it can fix the call
- The chat shows the hint in the user's language, then the provider's
message; a rejected key, missing access or unknown model adds an "Open
model settings" button. The Test button shows the same hints
- Fixes: our message "API key is required when using a custom base URL"
was replaced by "Authentication failed" because it contains "key"; a
provider's "Rate limit exceeded" opened this site's quota toast; an
error body like {"error": ...} was shown as raw JSON; the Test button
matched "401" in the message, where providers rarely put it
- Remove the string matching fallbacks in the chat panel
- A "Get API key" link next to the API Key field for the 19 providers
that have a key page (from env.example and the providers' docs). 17
answered 200 to curl; OpenAI's is behind a Cloudflare challenge and
DeepSeek's behind a regional block, both checked in Chrome
- Base URLs drop spaces, trailing slashes and a pasted endpoint path
(/chat/completions, /completions, /messages, /responses), which the
SDK would otherwise append a second time and get a 404. getAIModel does
this for the chat and the Test button; the field does it on blur and
shows the URL requests go to
- getAIModel resolves credentials (client key, server env vars, the
existing SSRF rules) and createModel builds the model by SDK. The
provider-by-provider switch shrinks from 24 cases to the few that
differ (lib/ai-providers.ts 1531 -> 1106 lines)
- /api/validate-model calls getAIModel instead of its own 24-case switch
(503 -> 175 lines), which had drifted from the chat: it built Azure
with createOpenAI, Kimi and MiMo with createOpenAI instead of
createDeepSeek, and the official OpenAI endpoint with Chat Completions.
A passing test now means the chat works
- Plain OpenAI-compatible providers (SiliconFlow, SGLang, ModelScope,
GLM, Qwen, Qiniu, Novita, Atlas Cloud, EdgeOne, Doubao, MiniMax in
OpenAI mode, AIHubMix on a custom URL) use @ai-sdk/openai-compatible,
which reads reasoning_content, so their reasoning shows, and accepts
SGLang's stream as is (its 95-line stream rewrite is gone).
includeUsage keeps token usage for quotas. <think> tags in their text
become reasoning (extractReasoningMiddleware)
- SGLang without a base URL used OpenAI's endpoint; it now defaults to
http://127.0.0.1:8000/v1 like the Test button did
- Chat requests to a client base URL refuse redirects, as the Test
button already did (redirectGuardedFetch moves to lib/ssrf-protection)
- The Test button streams like the chat (the ModelScope special case is
gone), times out after 15 s, does not retry, asks the model to call a
ping tool and warns when it answers without one, and tests all models
at once. The time each test took shows on its check mark
- Unknown provider names are rejected with Object.hasOwn, and the error
texts list providers from PROVIDER_INFO instead of hand-kept lists
- useChat throttles streamed message updates (experimental_throttle,
150 ms), replacing the two hand-written 150 ms timers of the
display_diagram and edit_diagram previews (94 lines less). The preview
now only runs while the input streams; once it is complete the tool
handler takes over, so a queued preview can no longer redraw an edit
the handler rejected and rolled back. Measured on a streamed 60-cell
diagram: 41 redraws at least 97 ms apart, before 37 with gaps down to
48 ms
- The diagram check endpoint uses streamText with Output.object instead
of the deprecated streamObject, and returns its fixed result as a plain
text response; new route test
- Import createGateway/gateway from ai and drop the direct
@ai-sdk/gateway dependency
- The per-request message structure logs only print with
DEBUG_LLM_PAYLOAD=true
- Remove an empty onFinish callback
- Update ai to 6.0.300 and the @ai-sdk providers to their latest v6-line
versions. @ai-sdk/anthropic 3.0.47 did not know claude-opus-4-7/4-8
and capped their output at 32000 tokens; 3.0.127 allows 128000
- Drop the fine-grained-tool-streaming beta header for the Anthropic API:
the provider now streams tool input per tool (eager_input_streaming)
- Claude 4.7 and later reject a non-default temperature/top_p/top_k and
the extended thinking budget with a 400. A middleware retries once
without them, so TEMPERATURE and *_THINKING_BUDGET_TOKENS no longer
break those models
- Prompt caching also reaches Claude on the Anthropic API and OpenRouter;
before, only Bedrock got a cache marker
- GOOGLE_TOP_K and GOOGLE_TOP_P never reached Gemini: they were sent as
Google provider options, which drops them. They are call settings now.
GOOGLE_CANDIDATE_COUNT and GOOGLE_REASONING_EFFORT, which the provider
does not support, are removed
- Add @ai-sdk/openai-compatible as a direct dependency
- The three XML examples (swimlanes, two edges, waypoints) that the web
system prompt, the display_diagram description and the MCP drawing
guide each had a copy of now live in packages/mcp-server/src/xml-examples.ts
- The shape library group list and reader come from the MCP
shape-library.ts; getShapeLibrary takes the folder, and the web route
passes docs/shape-libraries under the working directory as before.
Only the 30 known library names are read. Error texts differ slightly
- next.config.ts traces docs/shape-libraries/*.md for /api/chat, since
the read now happens in another module
- Every prompt is byte-for-byte unchanged: the web system prompt for five
model ids with and without minimal style, and the MCP drawing guide,
compared before and after; the library list and swimlane example match
the old tool description text exactly
- edit_diagram runs the MCP server's editDiagram: every new_xml is checked
first, one cell per operation, and after the edit only the target page
is checked, rejecting only errors this edit introduced. An unrelated
problem elsewhere in the document no longer blocks every edit. The
error lists each failed operation
- The streaming edit preview uses the MCP applyDiagramOperations
- Delete applyDiagramOperations (292 lines) and wrapWithMxFile from
lib/utils.ts, and the unused hand-copied scripts/test-diagram-operations.mjs
- One blank document (BLANK_MXFILE) for the web app and the MCP preview,
replacing four copies
- Saving a .drawio wraps a bare model with normalizeToMxfile
- The empty-diagram check uses hasCells, which also counts cells wrapped
in a UserObject/object
- DiagramOperation is the MCP type
- The wrapped-cell and empty-diagram tests now run against the MCP code
- New e2e test: edit_diagram changes the canvas, and a failing edit
leaves it as it was
- Delete the web app's own copy of the XML checks and repairs from
lib/utils.ts (1,074 lines). loadDiagram now uses the MCP server's
validateAndFixXml without the strict checks, because the XML may hold
the user's own diagram
- display_diagram and append_diagram prepare the model's XML with the new
shared prepareNewDiagram, also used by the MCP create_new_diagram: wrap,
validate strictly and auto-fix while it is still a bare model (where
duplicate ids are renamed), then turn it into an mxfile
- The streaming preview of display_diagram no longer redraws the model's
raw cells after the tool handler loaded the checked diagram, and drops
a queued preview once the input is complete. That redraw lost
auto-fixes and UserObject/object wrappers, so a linked cell lost its
label; it also showed a second error toast
- The web repair regression tests now run against the MCP functions
- New e2e test checks the canvas content after display_diagram
- Fix the e2e upload tests, whose file input locator also matched the
template import input
The web app will reuse the MCP server's XML engine instead of its own
copy in lib/utils.ts, so these modules now run in the browser too.
- Relative imports end in .ts, rewritten to .js by tsc
(rewriteRelativeImportExtensions); Next.js resolves them directly
- Every module uses the global DOMParser/XMLSerializer: native in the
browser, linkedom in Node via installDomPolyfill. pages.ts parsed with
linkedom but serialized with the global serializer, which throws in
the browser
- The saxes syntax check moves to xml-syntax.ts, so the browser does not
pull in linkedom; it now also rejects undeclared prefixes such as
xlink:, as the browser does
- Page decompression uses pako and atob instead of node:zlib and Buffer
- hasCells moves to pages.ts, away from the file system code
- The duplicate cell id check counts UserObject/object ids
- wrapCellsInModel drops comments and text before the first cell, which
the web app accepts today
- validateAndFixXml takes { strict: false } for diagrams with user content
- Web tests run these modules with a browser DOM (jsdom)
- saxes becomes a direct dependency of the web app
- /api/log-save accepted only drawio, png and svg, so saving as Editable
SVG (xmlsvg) got a 400 and was never recorded in Langfuse
- The default download name and the template export name used the UTC
date, which is the previous day on mornings in East Asia
- Remove the Electron openFile/saveFile IPC handlers, their preload
bindings and types; nothing in the app calls them
- Preview page: keep an MCP export open until the server has its result.
A poll answered before that still saw the request and started the same
export again, so a parallel page export could write the previous
page's image into its file
- Auto-save only removes its own mcp-*.drawio files, so a DRAWIO_DATA_DIR
that also holds the user's diagrams keeps them
- screenshot_diagram captures a page that has no id attribute by loading
just that page, like export_diagram
- An empty <Array as="points"/> no longer hides orphan mxPoints that
come after it
- POST /api/state refuses a push without xml, which used to wipe the
stored diagram
- Clear exportOptions when an export ends, reuse hasCells for the empty
diagram check, and reword two log lines
- MCP server README: 13 tools, drawing rules and shape libraries,
screenshots, all-or-nothing edits, auto-save and how to continue a
diagram later, DRAWIO_DATA_DIR and DEBUG, offline use with a local
draw.io, and what to do when a background tab makes exports time out
- Fix two errors: History is a button at the top right of the preview
page, and exports are not limited to .drawio
- Claude Code plugin README: same tool list, formats and settings
- Root READMEs (English, Chinese, Japanese): short list of what the MCP
server can do
- Pass themes=1 and dark=auto to the draw.io iframe, so the Extras menu
offers the Theme submenu and draw.io follows the system dark mode; the
header and dialogs follow it too through prefers-color-scheme
- Download dialog: new Editable SVG (.drawio.svg) format through draw.io's
xmlsvg export, default name diagram-YYYY-MM-DD, Enter saves and Escape
closes it and the history dialog
- export_diagram accepts format "drawio.svg" and detects the .drawio.svg
extension
- Use system fonts instead of Google Fonts, so the page works offline with
a self-hosted draw.io (DRAWIO_BASE_URL)
The 580-line page template in http-server.ts becomes index.html,
preview.css and preview.js, copied to dist/preview by the build and
filled at request time. The rendered page is unchanged apart from the
session id and draw.io origin now coming from a small config script.
Biome skips the folder because of the {{placeholders}}, as it never
linted the old template string either.
- Save the latest diagram of every session 1 second after each change
(AI write, browser edit, history restore) to ~/.next-ai-drawio/<id>.drawio,
keep the newest 50, flush on shutdown; DRAWIO_DATA_DIR changes the folder
and "off" disables it, like the web app's IndexedDB sessions
- start_session names the file, so a resumed conversation can reopen the
diagram with load_diagram after the MCP process restarted
- Fix PNG/SVG exports randomly timing out: a previous export's 10 second
timer cleared the export in progress, and a late reply could be taken
for the current one; exports are now numbered
- New read-only screenshot_diagram tool returns the rendered page as a PNG
plus the web app's visual checklist (overlaps, edges crossing shapes,
readability, layout, rendering errors), replacing the web app's
separate vision model with the host model's own vision
- PNG exports use draw.io's width and pageId options: screenshots stay
under ~140,000 base64 characters and page exports no longer swap the
page on screen
- Fail fast with a clear message when the preview tab stopped polling
(browsers throttle background tabs)
- Mention the screenshot step in the drawing guide and instructions
- Add a drawing guide adapted from the web system prompt (layout, edge
routing, styles, minimal style, editing rules), returned by
start_session, a new get_drawing_guide tool and the diagram-workflow prompt
- Add get_shape_library with the 30 icon libraries; the build copies
docs/shape-libraries into dist and CI checks the packed files
- Accept bare mxCell lists in create_new_diagram and add_page; the server
adds the wrapper and root cells
- Send server instructions, shorten create_new_diagram's description to
fit Claude Code's 2,048 character limit, and annotate every tool
- Fix dead links and the totals in docs/shape-libraries/README.md
- edit_diagram applies nothing when any operation fails, rejects invalid or
multi-cell new_xml, validates only the target page, and returns the
current page XML on every rejection (including stale edits)
- Fix get_diagram reading the old diagram right after an AI write: the
preview pushed its sync reply with a newer version than it was taken at
- Keep a user edit that loses the race with an AI write in history and
tell the user in the preview
- Autofix removes only exact foreign tags (a stray <mxGraph/> deleted
<mxGraphModel>), fixes tag case, drops orphan <mxPoint>s, and rejects
unknown element names in model XML
- Edit empty and compressed pages; PNG exports use the page on screen;
tag download exports; reload from the server after a page export
- Expand ~ in paths, tell the model when the browser sync timed out,
use registerPrompt, require SDK ^1.31.0
- Validate and escape the mcp session id; only serve localhost Host/Origin
- Malformed URLs and session ids return errors instead of crashing the process
- Strict XML syntax check with saxes (linkedom never reports parse errors)
- autoFixXml no longer corrupts valid XML; attribute newlines serialized as entities
- Sessions stay alive while polled; browser pushes carry a base version (409 on conflict)
- Page tools respect the edit gate; UTF-8 bodies decoded correctly
- Export replies matched to requests and serialized; xml sync export handled
- UserObject/object cells addressable by id; history restored by stable id; logs off stdout
- Apply preset env after app ready, so Windows/Linux get decrypted keys
- Never re-encrypt ciphertext; restore env when switching or removing presets
- Block navigation away from the app, open external links in the browser, check IPC senders
- Keep inherited proxy settings, default NO_PROXY for localhost
- Serialize server start/restart, kill stuck processes, follow port changes
- Atomic config writes, keep corrupt files as backups, remember the server port
- Menu and settings window stay in sync; dev script gets the decrypted preset env
- Use app.isPackaged, parse inline .env comments, drop .env files from the bundle
- Fall back to the default server model when a saved one disappears
- Sync model config across tabs
- Validation uses the base path and sends the access code
- Model ids edited as drafts (no empty, duplicate or padded ids)
- Credential changes reset validation; stale validation results are dropped
- Admin: generateId over HTTP, env-locked group switches, discard and toggle fixes,
clearing a secret field keeps the saved key, first provider not auto-default when .env sets AI_MODEL
- Model selector items use unique values
- Template dialogs no longer submit the outer chat form
- Sending is blocked while files or URLs are still extracting
- File and URL extraction no longer drop or resurrect entries
- IME composition Enter no longer sends
- Tool call cards show the error text; keyboard handling on cards fixed
- Template import available when empty, edit dialog resets, saved templates refresh
- Only png/jpeg/gif/webp images accepted, SVG sent as text; PDF objects released
- parse-url request sends the access code
- Autosave guard reads refs, so edits after a theme or dark mode switch are kept
- Duplicate-id check and rename run per page; repair loop no longer quadratic
- autoFixXml no longer breaks style values, rich text " or single-line cells
- extractCompleteMxCells keeps the cell after a self-closing cell
- Better truncation detection; object/UserObject wrapped cells are editable
- Exports for thumbnail, PNG and save are routed by tag instead of a shared resolver
- History stores the full document; storage errors are reported, no auto-deletion of chats
- IndexedDB connection reopens after errors; focus refresh throttled
- Keep ?session= on locale redirect, map zh-Hant to zh-tw for draw.io
- Restored sessions no longer replay the last display_diagram over the saved diagram
- Failed or stopped edit_diagram restores the canvas
- Message snapshots keep the full multi-page document
- "Improve with suggestions" uses the normal send path (headers, xml, retry counters)
- Editing a message keeps its file/URL sections; cached example edits work
- New chat's first autosave no longer resets the UI
- Validation retries counted per user turn; validate-diagram sends the access code
- Cached examples only match the example files on an empty canvas
- Template sends keep attachments and wait for extraction
- Shared checkAccessCode for validate-diagram, validate-model, parse-url, verify-access-code
- parse-url: 5 MB streamed body limit; validate-diagram: 5 MB image limit
- validate-model refuses redirects when private URLs are blocked
- Admin settings state shared across module instances via globalThis
- Server model ids: unique slugs (non-ASCII names encoded), duplicates rejected
- Panel Bedrock credentials stored as ADMIN_AWS_* so the DynamoDB client keeps its own
- Locale redirect keeps basePath and query; EdgeOne function drops open CORS and checks the access code
- Providers payload reports whether .env sets a default model
- Vertex: a client-supplied base URL only works with the client's own Vertex key
- Accept only data: URLs for file parts in every message, so the server never downloads them
- Output budget retry accounts for the thinking budget Bedrock/Anthropic add, and reads
Volcengine, DashScope, SGLang and vLLM rejections; falls back to 16000 once
- x-max-output-tokens can only lower the budget on server credentials
- On server credentials only server models or AI_MODEL entries can be used
- Drop tool results together with the invalid tool calls they belong to
- Count quota tokens as input + output (cached tokens were counted twice)
- Private-URL check for custom base URLs, end Langfuse traces on error/abort/early return
- Fix repairToolCall ordering and placeholder, align edit_diagram prompt with operations
- Panel Bedrock keys are read from ADMIN_AWS_*; forward the access code to EdgeOne
- isMinimalDiagram only treats root cells as an empty canvas
* fix: raise the output budget so reasoning models reach the tool call
A reasoning model spends the output budget in order: thinking first, then prose,
then the tool call. With 16000 the thinking alone can consume all of it, so the
turn ends with finishReason "length" before display_diagram is ever called. The
canvas stays empty and nothing surfaces in the UI, because no tool call means no
tool error, and the client never reads finishReason.
Measured on openrouter deepseek/deepseek-v4-flash, the model from the report:
- max_tokens=800 with reasoning on returns reasoning_tokens=800, empty content,
finish_reason length. So reasoning is billed against this budget, not exempt.
- refining an existing diagram (19k chars of XML in the input) produced 49142
chars of reasoning, zero tool calls, finishReason "length" at 16000
- the same request at 40000 finished and called edit_diagram with 12 operations
64000 cannot just be sent to every model: bedrock claude-3-haiku caps at 4096,
nova-lite at 10000, and the openrouter deepseek-r1 endpoint counts input and
output against one 64000 ceiling. All three name the real limit in the 400, so
parse it and retry once. Verified: nova-lite logs "64000 rejected, retrying with
10000" and then completes its tool call.
Also expose the budget in Settings. It is sent as a header rather than read from
env only, so desktop users can raise it themselves without an env file.
vercel.json goes back to the 300s it had before #238 traded it for $2-4/month.
That is now Vercel's own default, and billing pauses while the function waits on
the model, so the saving that motivated 120s no longer applies. edgeone.json is
left alone: its 120 may be that platform's actual ceiling.
* fix: only reinterpret an error as a budget rejection when it says so
Review of the first commit found the retry could fire on errors that have
nothing to do with the budget, which would replace a readable provider error
with a truncated response: exactly the symptom this PR exists to remove.
- Drop the generic "lower than N" pattern. For the Bedrock message it was dead
code, since "model limit of N" matches first with the same number. Left live,
it would read a number out of any message shaped like "must be lower than 2".
- Skip errors whose status is not 400 or 422, so auth and rate-limit failures
are never reinterpreted.
- Require the parsed ceiling to be at least 1024. Below that a diagram cannot
come out whole, so retrying would hide the error behind broken XML.
- Validate MAX_OUTPUT_TOKENS from env the same way as the header, so a stray
"-1" falls back instead of reaching the provider.
Adds tests for the retry wrapper itself, which had none: it retries once with
the named ceiling, leaves a 401 alone, does not retry when the ceiling is not
smaller, propagates a second rejection, and preserves the other call options.
Re-verified against the live APIs: bedrock nova-lite still logs "64000 rejected,
retrying with 10000" and completes its tool call, and deepseek-v4-flash still
finishes normally at 64000.
Unset does not mean the model's maximum — the provider fills in its own, and
Bedrock's is 4096 (measured: converse with no inferenceConfig on
us.anthropic.claude-opus-5 returns stopReason=max_tokens at exactly 4096).
A 30-cell diagram is ~3000 tokens of XML, so anything larger arrived as truncated
JSON and nothing reached the canvas. Small diagrams fitted, which made it look
intermittent.
* fix: preserve multi-page diagrams after export
* fix: keep chartXML sourced from autosave to preserve multi-page state
The export event's data.xml (xmlsvg format) contains compressed <diagram>
payloads, which would break applyDiagramOperations/replaceNodes consumers
that need plain <root> elements. Instead of writing export results into
chartXML, stop overwriting it entirely: autosave already delivers the full
uncompressed multi-page document, and loadDiagram covers AI-driven updates.
Also stop overwriting chartXMLRef with the page-only export before sending
a chat message, so session persistence never sees single-page XML.
Keep the data.xml preference for .drawio file downloads (compressed pages
are a valid drawio format).
---------
Co-authored-by: dayuan.jiang <[email protected]>
Biome 2.5.7 (auto-merged by Renovate in #904 with failing checks) started
parsing SVG files, breaking lint on generated assets in public/. Exclude
the whole public/ directory instead of just *.svg so future parser
changes can't hit generated files again.
Biome minor updates can also introduce new lint rules for source code,
so require manual review for its Renovate PRs instead of automerge.
* feat(mcp): add load_diagram tool to load .drawio files into the session
Loading a file previously required the agent to read the file itself and
pass the entire XML through create_new_diagram - wasteful for large
diagrams and impossible for draw.io's compressed save format.
load_diagram takes a file path; the server reads it, decompresses any
compressed pages (base64 -> raw deflate -> URI-decode, per page), and
replaces the session document. The loaded XML is deliberately NOT marked
as seen by the edit gate: the model only supplied a path, so it must
call get_diagram once before editing.
* chore(mcp): version 0.2.3
* fix(mcp): report package.json version in the MCP handshake
The McpServer metadata version was a separate hardcoded string that
never matched the published version (stuck at 0.1.2, then 0.3.0 while
npm shipped 0.2.x). Read it from package.json at startup instead —
works from both src/ (tsx) and dist/ (published build).
* fix(mcp): keep diagram context valid during edits
Closes#885
* fix(mcp): replace edit_diagram time gate with content comparison
The 30s wall-clock gate rejected slow-but-correct clients (#885).
Instead of a timeout, remember the exact state-store XML the model
last saw (get_diagram / create_new_diagram / edit_diagram / page CRUD)
and reject edit_diagram only when the live browser state differs -
i.e. the user made edits the model hasn't seen yet. Slow reasoning
no longer trips the gate, while unseen manual edits still do.
* docs(mcp): align edit_diagram/get_diagram descriptions with content-based gate
The 'You MUST call get_diagram BEFORE this tool' requirement and the
'Skipping get_diagram WILL cause user's changes to be LOST' warning no
longer match server behavior: a stale edit is rejected with no side
effects, never silently applied. Describe the freshness check instead,
and direct get_diagram usage at its real purpose - learning the current
diagram content when the model doesn't already know it.
* fix(mcp): compare diagram content structurally in the edit gate
draw.io re-serialises the document when pushing state back (attribute
order, pretty-printing, regenerated diagram ids, viewport attributes,
mxfile host), so byte comparison could flag an unchanged diagram as
stale. Fingerprint what a user can actually change instead - page set,
page names, and each page's root cell tree with sorted attributes -
keeping byte equality as the fast path. A bare mxGraphModel now also
fingerprints identically to its single-page mxfile wrapping.
* fix(mcp): don't compare page names against bare mxGraphModel pushes
A bare <mxGraphModel> pushed by the embed/sync path carries no page name,
so normalizeToMxfile invents "Page-1" — falsely reading any custom page
name as a content change and re-triggering the stale rejection on every
edit. When either side of the gate comparison is a bare mxGraphModel,
fingerprint cell trees only; full-mxfile comparisons still detect renames.
* chore(mcp): bump version to 0.2.2
* chore(mcp): sync package-lock.json version to 0.2.2
---------
Co-authored-by: dayuan.jiang <[email protected]>
Publishes @next-ai-drawio/mcp-server when packages/mcp-server changes on
main and the package.json version isn't on npm yet. Uses npm trusted
publishing (OIDC) - no token secret, no OTP, works with the strictest
2FA setting.
* feat: add MiMo (Xiaomi) as AI provider
* fix: correct MiMo default base URL, suggested models, and reasoning support
- Default base URL was the China Token Plan endpoint (tp- keys only);
switch to https://api.xiaomimimo.com/v1 which works with standard
pay-as-you-go sk- keys. Token Plan users can override in settings.
- Replace deprecated mimo-v2-flash suggestion with mimo-v2.5
(v2 series was deprecated on 2026-06-30).
- Use createDeepSeek instead of createOpenAI so reasoning_content is
passed back during multi-turn tool calls (MiMo returns 400 without
it), matching the existing Kimi implementation.
- Add mimo to SINGLE_SYSTEM_PROVIDERS so system messages are merged.
- Fold validate-model case into the shared OpenAI-compatible group.
- Drop the Bot icon special case; models.dev serves a real xiaomi logo
via PROVIDER_LOGO_MAP.
- Document MIMO_API_KEY/MIMO_BASE_URL in env.example and
docs/{en,cn,ja}/ai-providers.md.
* feat: show base URL hint for MiMo in provider settings
MiMo has two endpoints tied to key type: pay-as-you-go keys (sk-...)
use the default api.xiaomimimo.com/v1, while Token Plan keys (tp-...)
require token-plan-cn.xiaomimimo.com/v1. Surface this under the Base
URL field like the existing MiniMax hint, in all four locales.
---------
Co-authored-by: mapengfei <[email protected]>
Co-authored-by: dayuan.jiang <[email protected]>
* fix: resolve DNS before SSRF check and block redirects in parse-url
isPrivateUrl() did string-only hostname matching and never resolved DNS,
so a public-looking name that maps to an internal IP (e.g.
127-0-0-1.sslip.io -> 127.0.0.1) passed the check while fetch/extract
later resolved it and reached internal services (GHSA-wqcv-5qvx-vx75).
- isPrivateUrl is now async: it keeps the fast string/literal-IP path,
then resolves the hostname via DNS and rejects if any address is private.
- parse-url now fetches the page itself with redirect: "error" and parses
via extractFromHtml(), since article-extractor follows redirects
internally and drops a redirect option, which allowed a public URL to
302 to an internal host.
- Update validate-model call site to await; add regression tests.
* fix: preserve charset detection and block CGNAT range in parse-url SSRF fix
Follow-up to the multi-reviewer review of the SSRF fix:
- Restore charset handling lost when switching from extract() to
response.text(): non-UTF-8 pages (Shift_JIS/GBK/EUC/Big5, common on CJK
sites this project targets) decoded as mojibake. Now read the body as
bytes, detect charset from Content-Type / <meta charset>, and decode
with TextDecoder before extractFromHtml.
- Wrap extractFromHtml in try/catch: it throws (not returns null) on
empty/non-HTML bodies, which previously surfaced as a 500 instead of the
intended 400.
- Add 100.64.0.0/10 (RFC 6598 CGNAT) to isPrivateIp; it is routable inside
some cloud internal networks and was a residual SSRF target.
- Add tests for CGNAT, its boundaries, 0.0.0.0, and DNS-resolved IPv6.