@manny-est/node-red-flowpilot 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/lib/core/main.js +19 -3
- package/lib/core/modes.js +34 -9
- package/lib/provider-anthropic.js +107 -28
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to FlowPilot are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.6.2] - 2026-09-22
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Newer Anthropic models (Sonnet 5, Fable 5.1) didn't work at all —
|
|
9
|
+
every request failed with a 400 error.** ([#9](https://github.com/manny-est/flowpilot/issues/9))
|
|
10
|
+
Models released after Claude Opus 4.6 reject any `temperature` value
|
|
11
|
+
other than 1.0; FlowPilot's default of 0.2 meant every single chat,
|
|
12
|
+
generate, modify, document, and provider-capability-probe request
|
|
13
|
+
against a newer model failed outright with "temperature is deprecated
|
|
14
|
+
for this model." Older models (confirmed: `claude-sonnet-4-5`) were
|
|
15
|
+
unaffected. Fixed reactively — no model-name list to maintain: FlowPilot
|
|
16
|
+
now detects that exact error live, retries once with `temperature`
|
|
17
|
+
omitted, and remembers the model needs that treatment so it's a
|
|
18
|
+
one-time cost. Live-verified against Sonnet 5, Fable 5.1, and an older
|
|
19
|
+
model to confirm no regression there.
|
|
20
|
+
- **A multi-section plan could silently lose most of its steps from the
|
|
21
|
+
todo checklist.** A model's own explanation can legitimately structure
|
|
22
|
+
a larger plan as several numbered sections under short labels (e.g. a
|
|
23
|
+
Dashboard 2.0 build broken into "Widgets to add:" / "Final steps:")
|
|
24
|
+
rather than one unbroken numbered list. The checklist parser stopped
|
|
25
|
+
reading at the first blank line, so only the first section's items
|
|
26
|
+
ever made it onto the checklist — a 5-step plan could show as a
|
|
27
|
+
1-item checklist, which read as "the numbering is wrong" but was
|
|
28
|
+
actually silent data loss. Now keeps reading across section breaks and
|
|
29
|
+
only stops at the real closing explanation paragraph.
|
|
30
|
+
- **No way to clear a pinned node selection without losing your place.**
|
|
31
|
+
Once a mode (Generate/Modify/Document/Build) is armed with a
|
|
32
|
+
selection, that selection intentionally stays attached across
|
|
33
|
+
follow-up turns so you don't have to reselect every time — but there
|
|
34
|
+
was no way to just drop it if you no longer wanted those nodes
|
|
35
|
+
attached, short of fully re-arming the mode or clearing the whole
|
|
36
|
+
chat. The status line now shows a small ✕ next to a pinned selection
|
|
37
|
+
that clears just the pin, leaving the mode itself armed.
|
|
38
|
+
|
|
5
39
|
## [0.6.1] - 2026-09-02
|
|
6
40
|
|
|
7
41
|
### Fixed
|
package/lib/core/main.js
CHANGED
|
@@ -2482,9 +2482,25 @@
|
|
|
2482
2482
|
: armedExecuteAction === "document" ? "Document"
|
|
2483
2483
|
: armedExecuteAction === "modify" ? "Modify"
|
|
2484
2484
|
: armedExecuteAction === "build" ? "Build" : "Execute";
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2485
|
+
// 0.6.2 hotfix (pinned nodes don't clear): a pin is coupled to
|
|
2486
|
+
// armedExecuteAction by design (persists across follow-up turns
|
|
2487
|
+
// in the same mode so the user doesn't need to reselect every
|
|
2488
|
+
// time) — that part is intentional, not a bug. What was
|
|
2489
|
+
// actually missing is any way to clear JUST the pin, short of
|
|
2490
|
+
// re-arming/disarming the mode entirely or Clear Chat. Mirrors
|
|
2491
|
+
// updateDebugStatus's own "attached context + a ✕ to drop it"
|
|
2492
|
+
// pattern (main-window only, same as Modify/Build's other
|
|
2493
|
+
// live-RED.*-state features) rather than a plain text label.
|
|
2494
|
+
$status.empty().addClass("fp-has-selection");
|
|
2495
|
+
$status.append(document.createTextNode("Pinned: " + count + (count === 1 ? " node" : " nodes") +
|
|
2496
|
+
" for " + actionLabel + " — will be sent as context "));
|
|
2497
|
+
var $clearPin = $("<a>").attr("href", "#").text("✕").attr("title", "Clear pinned node context (stays in " + actionLabel + " mode)");
|
|
2498
|
+
$clearPin.on("click", function (ev) {
|
|
2499
|
+
ev.preventDefault();
|
|
2500
|
+
pinnedSelectionIds = null;
|
|
2501
|
+
updateSelectionStatus();
|
|
2502
|
+
});
|
|
2503
|
+
$status.append($clearPin);
|
|
2488
2504
|
} else {
|
|
2489
2505
|
$status.text(count + (count === 1 ? " node" : " nodes") + groupNote +
|
|
2490
2506
|
" selected — will be sent as context")
|
package/lib/core/modes.js
CHANGED
|
@@ -1675,21 +1675,46 @@
|
|
|
1675
1675
|
|
|
1676
1676
|
// W4: parse a "Plan:" block from the model's explanation field.
|
|
1677
1677
|
// Returns an array of { text, status } items, or [] if none found.
|
|
1678
|
-
//
|
|
1679
|
-
//
|
|
1680
|
-
//
|
|
1678
|
+
// Status starts as "pending" for all items — the caller sets the first
|
|
1679
|
+
// to "active" before rendering.
|
|
1680
|
+
//
|
|
1681
|
+
// 0.6.2 hotfix (todo-list numbering bug): this used to stop at the
|
|
1682
|
+
// FIRST blank line after "Plan:", on the assumption a plan is always
|
|
1683
|
+
// one unbroken numbered block followed by closing prose. A real
|
|
1684
|
+
// Dashboard 2.0 explanation can legitimately structure its plan as
|
|
1685
|
+
// several numbered sections separated by short labels ("Widgets to
|
|
1686
|
+
// add:", "Final steps:") — each its own blank-line-delimited block —
|
|
1687
|
+
// and the old logic silently dropped every item after the first
|
|
1688
|
+
// section (observed as "wrong numbering": a 5-item plan producing a
|
|
1689
|
+
// 1-item checklist). Now scans every line after "Plan:" to the end of
|
|
1690
|
+
// the explanation and keeps collecting numbered/bulleted lines across
|
|
1691
|
+
// blank lines and short one-line section labels; it only stops at two
|
|
1692
|
+
// CONSECUTIVE non-list, non-blank lines — a real prose paragraph, the
|
|
1693
|
+
// closing explanation that follows every plan today. A single-line
|
|
1694
|
+
// label between numbered sections never counts as that paragraph, so
|
|
1695
|
+
// sections keep getting picked up; a genuine multi-line prose block
|
|
1696
|
+
// still ends collection exactly as before.
|
|
1681
1697
|
function parseTodoPlan(explanation) {
|
|
1682
1698
|
if (!explanation || typeof explanation !== "string") { return []; }
|
|
1683
1699
|
var planStart = explanation.indexOf("Plan:");
|
|
1684
1700
|
if (planStart === -1) { return []; }
|
|
1685
1701
|
var afterPlan = explanation.slice(planStart + 5);
|
|
1686
|
-
var
|
|
1687
|
-
var lines = planBlock.split("\n");
|
|
1702
|
+
var lines = afterPlan.split("\n");
|
|
1688
1703
|
var items = [];
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1704
|
+
var proseStreak = 0;
|
|
1705
|
+
for (var i = 0; i < lines.length; i++) {
|
|
1706
|
+
var line = lines[i];
|
|
1707
|
+
if (!line.trim()) { continue; } // a blank line alone never ends the plan
|
|
1708
|
+
var isListLine = /^\s*\d+[.):\s]/.test(line) || /^\s*[-*]\s+/.test(line);
|
|
1709
|
+
if (isListLine) {
|
|
1710
|
+
proseStreak = 0;
|
|
1711
|
+
var stripped = line.replace(/^\s*\d+[.):\s]+/, "").replace(/^\s*[-*]\s+/, "").trim();
|
|
1712
|
+
if (stripped) { items.push({ text: stripped, status: "pending" }); }
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
proseStreak++;
|
|
1716
|
+
if (proseStreak >= 2) { break; }
|
|
1717
|
+
}
|
|
1693
1718
|
return items;
|
|
1694
1719
|
}
|
|
1695
1720
|
|
|
@@ -6,6 +6,28 @@ const ANTHROPIC_API_BASE = "https://api.anthropic.com";
|
|
|
6
6
|
const ANTHROPIC_VERSION = "2023-06-01";
|
|
7
7
|
const DEFAULT_MAX_TOKENS = 8192;
|
|
8
8
|
|
|
9
|
+
// ---- temperature-deprecated detection (GitHub #9) ----
|
|
10
|
+
// Models released after Claude Opus 4.6 reject any temperature value other
|
|
11
|
+
// than 1.0 (Anthropic's own Messages API docs) — FlowPilot's default is 0.2,
|
|
12
|
+
// so every request against a newer model fails outright with no reactive
|
|
13
|
+
// handling. No model-name allowlist or version parsing: detect the actual
|
|
14
|
+
// error shape live and adapt. In-memory only (module scope, keyed by model
|
|
15
|
+
// name) — resets on a server restart, which is an acceptable, deliberate
|
|
16
|
+
// simplification for a hotfix; the storage-backed persistence
|
|
17
|
+
// `supportsTools` uses would need threading through every one of this
|
|
18
|
+
// module's ~8 call sites in flowpilot.js and isn't worth that churn for a
|
|
19
|
+
// per-process cache that already makes this a one-time cost per model for
|
|
20
|
+
// the life of the running server.
|
|
21
|
+
const modelsWithoutTemperatureSupport = new Map();
|
|
22
|
+
|
|
23
|
+
function isTemperatureDeprecatedError(err) {
|
|
24
|
+
if (!err || typeof err.message !== "string") { return false; }
|
|
25
|
+
const msg = err.message;
|
|
26
|
+
return msg.indexOf("invalid_request_error") !== -1 &&
|
|
27
|
+
msg.indexOf("temperature") !== -1 &&
|
|
28
|
+
msg.indexOf("deprecated") !== -1;
|
|
29
|
+
}
|
|
30
|
+
|
|
9
31
|
// ---- HTTP helpers ----
|
|
10
32
|
|
|
11
33
|
function postJson(urlString, headers, body, timeoutMs) {
|
|
@@ -109,8 +131,19 @@ function postStream(urlString, headers, body, timeoutMs, onDelta, onReasoningDel
|
|
|
109
131
|
}, (res) => {
|
|
110
132
|
res.setEncoding("utf8");
|
|
111
133
|
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
112
|
-
|
|
113
|
-
|
|
134
|
+
// Capture the error body (same shape as postJson's non-2xx path)
|
|
135
|
+
// so callers can detect a specific error type (e.g. the
|
|
136
|
+
// temperature-deprecated case below) — this stays internal to the
|
|
137
|
+
// retry decision, never echoed to the HTTP client as the raw body
|
|
138
|
+
// (same ADR-007 boundary postJson already holds).
|
|
139
|
+
let errBody = "";
|
|
140
|
+
res.on("data", (chunk) => { errBody += chunk; });
|
|
141
|
+
res.on("end", () => {
|
|
142
|
+
let parsed = null;
|
|
143
|
+
try { parsed = errBody ? JSON.parse(errBody) : null; } catch (e) { /* fall through to generic message */ }
|
|
144
|
+
const msg = parsed && parsed.error ? JSON.stringify(parsed.error) : ("status " + res.statusCode);
|
|
145
|
+
reject(new Error("Provider request failed (" + res.statusCode + "): " + msg));
|
|
146
|
+
});
|
|
114
147
|
return;
|
|
115
148
|
}
|
|
116
149
|
|
|
@@ -263,23 +296,37 @@ async function chat(settings, messages, options) {
|
|
|
263
296
|
const { system, messages: anthropicMessages } = convertMessages(messages);
|
|
264
297
|
const temperature = settings.temperature !== undefined ? Number(settings.temperature) : 0.2;
|
|
265
298
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
};
|
|
273
|
-
if (system) { body.system = system; }
|
|
274
|
-
if (options && Array.isArray(options.tools) && options.tools.length) {
|
|
275
|
-
body.tools = options.tools.map(toAnthropicTool).filter(Boolean);
|
|
276
|
-
body.tool_choice = {
|
|
277
|
-
type: options.toolChoice === "required" ? "any" : "auto"
|
|
299
|
+
function buildBody(includeTemperature) {
|
|
300
|
+
const body = {
|
|
301
|
+
model: settings.model,
|
|
302
|
+
messages: anthropicMessages,
|
|
303
|
+
max_tokens: DEFAULT_MAX_TOKENS,
|
|
304
|
+
stream: false
|
|
278
305
|
};
|
|
306
|
+
if (includeTemperature) { body.temperature = temperature; }
|
|
307
|
+
if (system) { body.system = system; }
|
|
308
|
+
if (options && Array.isArray(options.tools) && options.tools.length) {
|
|
309
|
+
body.tools = options.tools.map(toAnthropicTool).filter(Boolean);
|
|
310
|
+
body.tool_choice = {
|
|
311
|
+
type: options.toolChoice === "required" ? "any" : "auto"
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return body;
|
|
279
315
|
}
|
|
280
316
|
|
|
317
|
+
const skipTemperature = modelsWithoutTemperatureSupport.get(settings.model) === true;
|
|
281
318
|
const startedAt = Date.now();
|
|
282
|
-
|
|
319
|
+
let response;
|
|
320
|
+
try {
|
|
321
|
+
response = await postJson(baseUrl + "/v1/messages", anthropicHeaders(settings), buildBody(!skipTemperature), settings.requestTimeoutMs || 180000);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
if (skipTemperature || !isTemperatureDeprecatedError(err)) { throw err; }
|
|
324
|
+
// First time seeing this on this model this process — cache it so
|
|
325
|
+
// every later call to this model skips straight to the no-temperature
|
|
326
|
+
// body, then retry this one request once.
|
|
327
|
+
modelsWithoutTemperatureSupport.set(settings.model, true);
|
|
328
|
+
response = await postJson(baseUrl + "/v1/messages", anthropicHeaders(settings), buildBody(false), settings.requestTimeoutMs || 180000);
|
|
329
|
+
}
|
|
283
330
|
const totalMs = Date.now() - startedAt;
|
|
284
331
|
|
|
285
332
|
const contentArray = (response && Array.isArray(response.content)) ? response.content : [];
|
|
@@ -307,16 +354,31 @@ async function chatStream(settings, messages, onDelta, onReasoningDelta) {
|
|
|
307
354
|
const { system, messages: anthropicMessages } = convertMessages(messages);
|
|
308
355
|
const temperature = settings.temperature !== undefined ? Number(settings.temperature) : 0.2;
|
|
309
356
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
357
|
+
function buildBody(includeTemperature) {
|
|
358
|
+
const body = {
|
|
359
|
+
model: settings.model,
|
|
360
|
+
messages: anthropicMessages,
|
|
361
|
+
max_tokens: DEFAULT_MAX_TOKENS,
|
|
362
|
+
stream: true
|
|
363
|
+
};
|
|
364
|
+
if (includeTemperature) { body.temperature = temperature; }
|
|
365
|
+
if (system) { body.system = system; }
|
|
366
|
+
return body;
|
|
367
|
+
}
|
|
318
368
|
|
|
319
|
-
const
|
|
369
|
+
const skipTemperature = modelsWithoutTemperatureSupport.get(settings.model) === true;
|
|
370
|
+
let result;
|
|
371
|
+
try {
|
|
372
|
+
result = await postStream(baseUrl + "/v1/messages", anthropicHeaders(settings), buildBody(!skipTemperature), settings.requestTimeoutMs || 180000, onDelta, onReasoningDelta);
|
|
373
|
+
} catch (err) {
|
|
374
|
+
if (skipTemperature || !isTemperatureDeprecatedError(err)) { throw err; }
|
|
375
|
+
// Same one-time detection as chat() above. Safe to retry whole-hog:
|
|
376
|
+
// postStream rejects on the response's status line, before any SSE
|
|
377
|
+
// data event is parsed, so onDelta/onReasoningDelta can't have already
|
|
378
|
+
// emitted partial content from the failed attempt.
|
|
379
|
+
modelsWithoutTemperatureSupport.set(settings.model, true);
|
|
380
|
+
result = await postStream(baseUrl + "/v1/messages", anthropicHeaders(settings), buildBody(false), settings.requestTimeoutMs || 180000, onDelta, onReasoningDelta);
|
|
381
|
+
}
|
|
320
382
|
|
|
321
383
|
return {
|
|
322
384
|
content: result.content || "",
|
|
@@ -362,8 +424,8 @@ async function probeTools(settings) {
|
|
|
362
424
|
if (!settings.model) { throw new Error("Model is required."); }
|
|
363
425
|
const baseUrl = resolveBaseUrl(settings);
|
|
364
426
|
|
|
365
|
-
|
|
366
|
-
const
|
|
427
|
+
function buildProbeBody(includeTemperature) {
|
|
428
|
+
const body = {
|
|
367
429
|
model: settings.model,
|
|
368
430
|
messages: [{ role: "user", content: "Call the \"ping\" tool now with no arguments." }],
|
|
369
431
|
system: "You are being tested for tool/function-calling support.",
|
|
@@ -374,9 +436,26 @@ async function probeTools(settings) {
|
|
|
374
436
|
}],
|
|
375
437
|
tool_choice: { type: "auto" },
|
|
376
438
|
max_tokens: 128,
|
|
377
|
-
temperature: 0,
|
|
378
439
|
stream: false
|
|
379
|
-
}
|
|
440
|
+
};
|
|
441
|
+
if (includeTemperature) { body.temperature = 0; }
|
|
442
|
+
return body;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const skipTemperature = modelsWithoutTemperatureSupport.get(settings.model) === true;
|
|
446
|
+
try {
|
|
447
|
+
let response;
|
|
448
|
+
try {
|
|
449
|
+
response = await postJson(baseUrl + "/v1/messages", anthropicHeaders(settings), buildProbeBody(!skipTemperature), settings.requestTimeoutMs || 30000);
|
|
450
|
+
} catch (err) {
|
|
451
|
+
// A temperature-deprecated failure here is not "no tool support" —
|
|
452
|
+
// it's an unrelated request-shape error. Retry without temperature
|
|
453
|
+
// before concluding anything about tool support (same detection as
|
|
454
|
+
// chat()/chatStream() above).
|
|
455
|
+
if (skipTemperature || !isTemperatureDeprecatedError(err)) { throw err; }
|
|
456
|
+
modelsWithoutTemperatureSupport.set(settings.model, true);
|
|
457
|
+
response = await postJson(baseUrl + "/v1/messages", anthropicHeaders(settings), buildProbeBody(false), settings.requestTimeoutMs || 30000);
|
|
458
|
+
}
|
|
380
459
|
|
|
381
460
|
const contentArray = (response && Array.isArray(response.content)) ? response.content : [];
|
|
382
461
|
const hasToolUse = contentArray.some(function (b) { return b.type === "tool_use" && b.name === "ping"; });
|