@rhizomatics/signalk-einklabel-genai-plugin 0.3.0 → 0.3.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 +11 -0
- package/README.md +16 -14
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/llmProviderRegistry.d.ts +4 -3
- package/dist/llmProviderRegistry.js +4 -3
- package/dist/plugin.js +7 -3
- package/dist/templateProvider.d.ts +12 -2
- package/dist/templateProvider.js +26 -15
- package/package.json +8 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
# [0.3.2]
|
|
2
|
+
|
|
3
|
+
- Fix: a disabled plugin no longer calls the LLM. Repaints of `(GenAI)` templates now fail fast with "not running" (core shows its fallback warning) instead of making paid LLM calls with the last config, or failing with a misleading "no LLM model configured" after a restart. Templates stay listed, so saved device choices are kept (#28)
|
|
4
|
+
- Fix: parallel renders of the same template to several labels no longer share one cached SVG file, which could paint one label with another's content at the wrong size (#28)
|
|
5
|
+
- `@rhizomatics/signalk-einklabel-plugin` is no longer an npm `dependency`, only `signalk.requires` (and a `devDependency` for types/tests) - the App Store installs it flat in `~/.signalk/node_modules`, so the plugin always registers with the one running copy rather than a possible nested duplicate
|
|
6
|
+
- Extension docs updated to match: provider packages should declare this plugin the same way
|
|
7
|
+
|
|
8
|
+
# [0.3.1]
|
|
9
|
+
|
|
10
|
+
- New `describeContent` API implemented to support debug use
|
|
11
|
+
|
|
1
12
|
# [0.3.0]
|
|
2
13
|
|
|
3
14
|
- Update timeout to 60 seconds to handle slower local or free models
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# eInk Label GenAI Templates
|
|
2
2
|
|
|
3
|
+
[](https://boat-tech-directory.rhizomatics.org.uk) [](https://github.com/rhizomatics/hq)
|
|
4
|
+
|
|
3
5
|
A companion plugin for [`@rhizomatics/signalk-einklabel-plugin`](https://github.com/rhizomatics/signalk-einklabel-plugin) that generates an image to paint on shelf label entirely from an LLM prompt, as an alternative to a hand-authored SVG template.
|
|
4
6
|
|
|
5
7
|
By default it uses [OpenRouter](https://openrouter.ai), however it can use any of the [Vercel AI SDK providers](https://ai-sdk.dev/providers/ai-sdk-providers) directly, including local LLMs like Ollama.
|
|
@@ -10,19 +12,7 @@ By default it uses [OpenRouter](https://openrouter.ai), however it can use any o
|
|
|
10
12
|
|
|
11
13
|
This plugin comes with a marine weather forecast example prompt. When installed, and SignalK server restarted, you should see this appear as a template to choose in the main eInk Label plugin.
|
|
12
14
|
|
|
13
|
-
###
|
|
14
|
-
|
|
15
|
-
If you don't already have an AI account, get a [free API key from OpenRouter](https://openrouter.ai/openrouter/free) - set the _model_ to one of the [specific free models](https://openrouter.ai/openrouter/free), or `openrouter/free`, OpenRouter's free-tier routing alias, although note you will get a random model every time it is called, some of which are specialized (e.g. "content safety") and will not generate an image. Free models may not lookup web pages or APIs, so guess things like weather, and the image quality will likely be much poorer than using a mainstream LLM.
|
|
16
|
-
|
|
17
|
-
Use the `esl-cli` CLI to see generated prompts or create test PNGs.
|
|
18
|
-
|
|
19
|
-
### Customizing Prompts
|
|
20
|
-
|
|
21
|
-
Prompts have the same template syntax as the SVG images, so the weather forecast will give the current GPS position, and the length and type of boat.
|
|
22
|
-
|
|
23
|
-
All prompts also get a generated appendix to the prompt that gives details about the eInk label, including manufacturer, name, pixel height and width, colour range, and the description provided in the main plugin.
|
|
24
|
-
|
|
25
|
-
### Full Example
|
|
15
|
+
### Expanded Example
|
|
26
16
|
|
|
27
17
|
#### Forecast
|
|
28
18
|
|
|
@@ -40,6 +30,18 @@ The following has been automatically added to the end of the standard prompt, wi
|
|
|
40
30
|
|
|
41
31
|
Be careful with GenAI generated content, it will often be very good but may be prone to hallucinations or omissions.
|
|
42
32
|
|
|
33
|
+
### Testing on your server
|
|
34
|
+
|
|
35
|
+
If you don't already have an AI account, get a [free API key from OpenRouter](https://openrouter.ai/openrouter/free) - set the _model_ to one of the [specific free models](https://openrouter.ai/openrouter/free), or `openrouter/free`, OpenRouter's free-tier routing alias, although note you will get a random model every time it is called, some of which are specialized (e.g. "content safety") and will not generate an image. Free models may not lookup web pages or APIs, so guess things like weather, and the image quality will likely be much poorer than using a mainstream LLM.
|
|
36
|
+
|
|
37
|
+
Use the `esl-cli` CLI to see generated prompts or create test PNGs.
|
|
38
|
+
|
|
39
|
+
### Customizing Prompts
|
|
40
|
+
|
|
41
|
+
Prompts have the same template syntax as the SVG images, so the weather forecast will give the current GPS position, and the length and type of boat.
|
|
42
|
+
|
|
43
|
+
All prompts also get a generated appendix to the prompt that gives details about the eInk label, including manufacturer, name, pixel height and width, colour range, and the description provided in the main plugin.
|
|
44
|
+
|
|
43
45
|
## Installation
|
|
44
46
|
|
|
45
47
|
Install both plugins from the SignalK AppStore (or `npm install @rhizomatics/signalk-einklabel-plugin @rhizomatics/signalk-einklabel-genai-plugin`), then install whichever LLM provider package(s) you'll actually use - these are `optionalDependencies`, not installed automatically, since most people will likely only ever need one. `openrouter` is the one exception: `@openrouter/ai-sdk-provider` is a regular `dependency`, so it's already installed and needs no extra step, as is the base `ai` package:
|
|
@@ -127,4 +129,4 @@ import genai from "@rhizomatics/signalk-einklabel-genai-plugin";
|
|
|
127
129
|
genai.registerLlmProvider("my-provider", (settings) => myOwnModelFactory(settings));
|
|
128
130
|
```
|
|
129
131
|
|
|
130
|
-
|
|
132
|
+
Don't list this package in your extension's `dependencies` (or `peerDependencies`) - declare it via `"signalk": { "requires": ["@rhizomatics/signalk-einklabel-genai-plugin"] }` in your extension's `package.json` instead, and add it as a `devDependency` for types and tests. The App Store installs required plugins flat in `~/.signalk/node_modules`, so your `require` still resolves at runtime, to the same copy the running plugin uses - see the core plugin's [Extending](https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/docs/extending.md) docs for why.
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { ServerAPI, Plugin } from "@signalk/server-api";
|
|
|
2
2
|
import { registerLlmProvider as registerLlmProviderImpl, getLlmProvider as getLlmProviderImpl } from "./llmProviderRegistry";
|
|
3
3
|
import type { LlmProviderFactory as LlmProviderFactoryType, LlmSettings as LlmSettingsType } from "./llmProviderRegistry";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* When constructed, registers this plugin's `TemplateProvider` with the core `signalk-einklabel-plugin`
|
|
6
6
|
* (`esl.registerTemplateProvider`, see `./templateProvider.ts` and `./plugin.ts`) - so its prompts show
|
|
7
7
|
* up in the core plugin's own "Template" picker, suffixed "(GenAI)".
|
|
8
8
|
*/
|
|
@@ -12,8 +12,8 @@ declare namespace plugin {
|
|
|
12
12
|
* Public extension point for a package adding support for an LLM provider this plugin doesn't
|
|
13
13
|
* already bundle - see `registerLlmProvider`'s own doc comment (`./llmProviderRegistry.ts`) for the
|
|
14
14
|
* full contract, and the core plugin's own `registerVendorDriver` doc comment for the dependency
|
|
15
|
-
* convention to follow (
|
|
16
|
-
* `peerDependency`).
|
|
15
|
+
* convention to follow (`"signalk": { "requires": [...] }` plus a `devDependency` for types/tests,
|
|
16
|
+
* **not** a regular `dependency` or `peerDependency`).
|
|
17
17
|
*/
|
|
18
18
|
const registerLlmProvider: typeof registerLlmProviderImpl;
|
|
19
19
|
const getLlmProvider: typeof getLlmProviderImpl;
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const plugin_1 = require("./plugin");
|
|
3
3
|
const llmProviderRegistry_1 = require("./llmProviderRegistry");
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* When constructed, registers this plugin's `TemplateProvider` with the core `signalk-einklabel-plugin`
|
|
6
6
|
* (`esl.registerTemplateProvider`, see `./templateProvider.ts` and `./plugin.ts`) - so its prompts show
|
|
7
7
|
* up in the core plugin's own "Template" picker, suffixed "(GenAI)".
|
|
8
8
|
*/
|
|
@@ -14,8 +14,8 @@ function plugin(app) {
|
|
|
14
14
|
* Public extension point for a package adding support for an LLM provider this plugin doesn't
|
|
15
15
|
* already bundle - see `registerLlmProvider`'s own doc comment (`./llmProviderRegistry.ts`) for the
|
|
16
16
|
* full contract, and the core plugin's own `registerVendorDriver` doc comment for the dependency
|
|
17
|
-
* convention to follow (
|
|
18
|
-
* `peerDependency`).
|
|
17
|
+
* convention to follow (`"signalk": { "requires": [...] }` plus a `devDependency` for types/tests,
|
|
18
|
+
* **not** a regular `dependency` or `peerDependency`).
|
|
19
19
|
*/
|
|
20
20
|
plugin.registerLlmProvider = llmProviderRegistry_1.registerLlmProvider;
|
|
21
21
|
plugin.getLlmProvider = llmProviderRegistry_1.getLlmProvider;
|
|
@@ -18,9 +18,10 @@ export type LlmProviderFactory = (settings: LlmSettings) => LlmModel;
|
|
|
18
18
|
* forking this plugin - `resolveModel` in `./llmGateway.ts` checks this registry first, by `llmProvider`
|
|
19
19
|
* name, before falling through to the built-in switch. Call this from your own SignalK plugin's
|
|
20
20
|
* `start()` (or `esl-cli --require <module>`, loaded before parsing per the core plugin's own CLI
|
|
21
|
-
* extension timing - see its README). Declare this package
|
|
22
|
-
* `
|
|
23
|
-
* resolves
|
|
21
|
+
* extension timing - see its README). Declare this package via `"signalk": { "requires": [...] }` plus
|
|
22
|
+
* a `devDependency` for types/tests - not a regular `dependency` or `peerDependency`, so `require`
|
|
23
|
+
* resolves the one copy the App Store installed (see the core plugin's own `registerVendorDriver`
|
|
24
|
+
* doc comment for why).
|
|
24
25
|
*/
|
|
25
26
|
export declare function registerLlmProvider(name: string, factory: LlmProviderFactory): void;
|
|
26
27
|
export declare function getLlmProvider(name: string): LlmProviderFactory | undefined;
|
|
@@ -14,9 +14,10 @@ const providers = new Map();
|
|
|
14
14
|
* forking this plugin - `resolveModel` in `./llmGateway.ts` checks this registry first, by `llmProvider`
|
|
15
15
|
* name, before falling through to the built-in switch. Call this from your own SignalK plugin's
|
|
16
16
|
* `start()` (or `esl-cli --require <module>`, loaded before parsing per the core plugin's own CLI
|
|
17
|
-
* extension timing - see its README). Declare this package
|
|
18
|
-
* `
|
|
19
|
-
* resolves
|
|
17
|
+
* extension timing - see its README). Declare this package via `"signalk": { "requires": [...] }` plus
|
|
18
|
+
* a `devDependency` for types/tests - not a regular `dependency` or `peerDependency`, so `require`
|
|
19
|
+
* resolves the one copy the App Store installed (see the core plugin's own `registerVendorDriver`
|
|
20
|
+
* doc comment for why).
|
|
20
21
|
*/
|
|
21
22
|
function registerLlmProvider(name, factory) {
|
|
22
23
|
providers.set(name, factory);
|
package/dist/plugin.js
CHANGED
|
@@ -41,8 +41,10 @@ function createPlugin(app) {
|
|
|
41
41
|
// Updated by `start()` on every (re)start - `createTemplateProvider`'s functions all read through
|
|
42
42
|
// this closure rather than capturing one fixed config object, so a saved config change takes effect
|
|
43
43
|
// on the *next* repaint without needing the core plugin (which holds the actual repaint schedule) to
|
|
44
|
-
// know or care that anything changed on this side.
|
|
45
|
-
|
|
44
|
+
// know or care that anything changed on this side. `undefined` while the plugin isn't running: the
|
|
45
|
+
// provider stays registered (the core plugin has no way to unregister one, and the server constructs
|
|
46
|
+
// every installed plugin, enabled or not), so `render()` uses this to refuse LLM calls while disabled.
|
|
47
|
+
let currentConfig;
|
|
46
48
|
signalk_einklabel_plugin_1.default.registerTemplateProvider((0, templateProvider_1.createTemplateProvider)(app, () => currentConfig));
|
|
47
49
|
return {
|
|
48
50
|
id: "signalk-einklabel-genai-plugin",
|
|
@@ -118,6 +120,8 @@ function createPlugin(app) {
|
|
|
118
120
|
runConnectionTest(app, currentConfig).catch((err) => app.debug(`connection test failed unexpectedly: ${err.message}`));
|
|
119
121
|
}
|
|
120
122
|
},
|
|
121
|
-
stop() {
|
|
123
|
+
stop() {
|
|
124
|
+
currentConfig = undefined;
|
|
125
|
+
},
|
|
122
126
|
};
|
|
123
127
|
}
|
|
@@ -5,8 +5,18 @@ import { PluginConfig } from "./config";
|
|
|
5
5
|
export declare const SUFFIX = "(GenAI)";
|
|
6
6
|
/**
|
|
7
7
|
* The `esl.TemplateProvider` this plugin registers with the core plugin (`esl.registerTemplateProvider`,
|
|
8
|
-
* called
|
|
8
|
+
* called when `./plugin.ts`'s `createPlugin` constructs the plugin, whether or not it is enabled). `listTemplates`/`describeBindings`/`render` all resolve the
|
|
9
9
|
* current `promptsDir` fresh each time, rather than once at startup, so adding/editing a `.md` prompt
|
|
10
10
|
* file takes effect without a plugin restart - same as the core plugin's own template directory.
|
|
11
11
|
*/
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* `esl.TemplateProvider` with its optional `describeContent` made required, so callers (and tests) can
|
|
14
|
+
* use it without a guard. Core logs it to debug output before each `render()`, so you can see what the
|
|
15
|
+
* LLM is sent; a future core feature (e.g. emailing a label) could reuse it too. Older core versions
|
|
16
|
+
* without `describeContent` simply never call it.
|
|
17
|
+
*/
|
|
18
|
+
export type GenAiTemplateProvider = esl.TemplateProvider & {
|
|
19
|
+
describeContent(request: esl.TemplateRenderRequest): string;
|
|
20
|
+
};
|
|
21
|
+
/** `getConfig` returns `undefined` while the plugin isn't running: templates are still listed, but `render()` rejects without calling the LLM. */
|
|
22
|
+
export declare function createTemplateProvider(app: ServerAPI, getConfig: () => PluginConfig | undefined): GenAiTemplateProvider;
|
package/dist/templateProvider.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.SUFFIX = void 0;
|
|
7
7
|
exports.createTemplateProvider = createTemplateProvider;
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
8
9
|
const fs_1 = require("fs");
|
|
9
10
|
const path_1 = require("path");
|
|
10
11
|
const signalk_einklabel_plugin_1 = __importDefault(require("@rhizomatics/signalk-einklabel-plugin"));
|
|
@@ -18,15 +19,10 @@ const GUIDANCE_NAME = (0, path_1.join)(".assets", "target_guidance.md");
|
|
|
18
19
|
function promptNameFor(templateName) {
|
|
19
20
|
return `${templateName.slice(0, templateName.length - ` ${exports.SUFFIX}`.length)}.md`;
|
|
20
21
|
}
|
|
21
|
-
/**
|
|
22
|
-
* The `esl.TemplateProvider` this plugin registers with the core plugin (`esl.registerTemplateProvider`,
|
|
23
|
-
* called from `./plugin.ts`'s `start()`). `listTemplates`/`describeBindings`/`render` all resolve the
|
|
24
|
-
* current `promptsDir` fresh each time, rather than once at startup, so adding/editing a `.md` prompt
|
|
25
|
-
* file takes effect without a plugin restart - same as the core plugin's own template directory.
|
|
26
|
-
*/
|
|
22
|
+
/** `getConfig` returns `undefined` while the plugin isn't running: templates are still listed, but `render()` rejects without calling the LLM. */
|
|
27
23
|
function createTemplateProvider(app, getConfig) {
|
|
28
24
|
function promptsDir() {
|
|
29
|
-
return (0, config_1.resolvePromptsDir)(getConfig().promptsDir);
|
|
25
|
+
return (0, config_1.resolvePromptsDir)((getConfig() ?? (0, config_1.defaultConfig)()).promptsDir);
|
|
30
26
|
}
|
|
31
27
|
function promptPathFor(templateName) {
|
|
32
28
|
const dir = promptsDir();
|
|
@@ -35,6 +31,13 @@ function createTemplateProvider(app, getConfig) {
|
|
|
35
31
|
guidancePath: (0, config_1.resolvePromptPath)(dir, GUIDANCE_NAME),
|
|
36
32
|
};
|
|
37
33
|
}
|
|
34
|
+
/** Shared by `render` (sent to the LLM) and `describeContent` (for debugging what the LLM is sent) - the prompt+guidance text with every `{...}` placeholder substituted against `context`. */
|
|
35
|
+
function buildPrompt(templateName, context) {
|
|
36
|
+
const { promptPath, guidancePath } = promptPathFor(templateName);
|
|
37
|
+
const promptText = (0, fs_1.readFileSync)(promptPath, "utf-8");
|
|
38
|
+
const guidanceText = (0, fs_1.readFileSync)(guidancePath, "utf-8");
|
|
39
|
+
return `${signalk_einklabel_plugin_1.default.substituteBindingsInText(promptText, context).trim()}\n\n${signalk_einklabel_plugin_1.default.substituteBindingsInText(guidanceText, context).trim()}\n`;
|
|
40
|
+
}
|
|
38
41
|
return {
|
|
39
42
|
suffix: exports.SUFFIX,
|
|
40
43
|
listTemplates() {
|
|
@@ -46,14 +49,15 @@ function createTemplateProvider(app, getConfig) {
|
|
|
46
49
|
const guidanceText = (0, fs_1.readFileSync)(guidancePath, "utf-8");
|
|
47
50
|
return signalk_einklabel_plugin_1.default.findBindingsInText(promptText, guidanceText);
|
|
48
51
|
},
|
|
52
|
+
describeContent(request) {
|
|
53
|
+
return buildPrompt(request.templateName, request.context);
|
|
54
|
+
},
|
|
49
55
|
async render(request) {
|
|
50
|
-
const { promptPath, guidancePath } = promptPathFor(request.templateName);
|
|
51
|
-
const promptText = (0, fs_1.readFileSync)(promptPath, "utf-8");
|
|
52
|
-
const guidanceText = (0, fs_1.readFileSync)(guidancePath, "utf-8");
|
|
53
|
-
const prompt = `${signalk_einklabel_plugin_1.default.substituteBindingsInText(promptText, request.context).trim()}\n\n${signalk_einklabel_plugin_1.default
|
|
54
|
-
.substituteBindingsInText(guidanceText, request.context)
|
|
55
|
-
.trim()}\n`;
|
|
56
56
|
const config = getConfig();
|
|
57
|
+
if (!config) {
|
|
58
|
+
throw new Error('signalk-einklabel-genai-plugin is not running - enable it to render "(GenAI)" templates');
|
|
59
|
+
}
|
|
60
|
+
const prompt = buildPrompt(request.templateName, request.context);
|
|
57
61
|
const llmSettings = {
|
|
58
62
|
llmProvider: config.llmProvider,
|
|
59
63
|
llmApiKey: config.llmApiKey,
|
|
@@ -77,9 +81,16 @@ function createTemplateProvider(app, getConfig) {
|
|
|
77
81
|
// reused unchanged, rather than needing its own copy of that logic.
|
|
78
82
|
const cacheDir = (0, path_1.join)(promptsDir(), ".genai-cache");
|
|
79
83
|
(0, fs_1.mkdirSync)(cacheDir, { recursive: true });
|
|
80
|
-
|
|
84
|
+
// One file per render: core repaints a device's targets in parallel, so a name shared by every
|
|
85
|
+
// render of the same template could be overwritten by another target's SVG before it is read.
|
|
86
|
+
const cachePath = (0, path_1.join)(cacheDir, `${Buffer.from(request.templateName).toString("hex")}-${(0, crypto_1.randomUUID)()}.svg`);
|
|
81
87
|
(0, fs_1.writeFileSync)(cachePath, svg);
|
|
82
|
-
|
|
88
|
+
try {
|
|
89
|
+
return await renderer.render(cachePath, request.context, request.width, request.height);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
(0, fs_1.rmSync)(cachePath, { force: true });
|
|
93
|
+
}
|
|
83
94
|
},
|
|
84
95
|
};
|
|
85
96
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rhizomatics/signalk-einklabel-genai-plugin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "GenAI prompt generated labels for the eInk Electronic Shelf Label plugin. Contributes prompts as an alternative to hand-authored SVG templates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eink",
|
|
@@ -52,14 +52,14 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@openrouter/ai-sdk-provider": "^3.0.0",
|
|
55
|
-
"@rhizomatics/signalk-einklabel-plugin": "^1.0.0",
|
|
56
55
|
"ai": "^7.0.37"
|
|
57
56
|
},
|
|
58
57
|
"devDependencies": {
|
|
58
|
+
"@rhizomatics/signalk-einklabel-plugin": "^1.0.0",
|
|
59
59
|
"@signalk/server-api": "^2.28.0",
|
|
60
|
-
"@types/node": "^
|
|
61
|
-
"commander": "^
|
|
62
|
-
"oxfmt": "^0.
|
|
60
|
+
"@types/node": "^26.6.2",
|
|
61
|
+
"commander": "^15.0.0",
|
|
62
|
+
"oxfmt": "^0.74.0",
|
|
63
63
|
"oxlint": "^1.74.0",
|
|
64
64
|
"oxlint-tsgolint": "^7.0.0",
|
|
65
65
|
"ts-node": "^10.9.2",
|
|
@@ -72,11 +72,12 @@
|
|
|
72
72
|
"@ai-sdk/moonshotai": "^3.0.17",
|
|
73
73
|
"@ai-sdk/openai": "^4.0.20",
|
|
74
74
|
"@ai-sdk/openai-compatible": "^3.0.14",
|
|
75
|
-
"@ai-sdk/xai": "^
|
|
75
|
+
"@ai-sdk/xai": "^5.0.4",
|
|
76
76
|
"ai-sdk-ollama": "^4.1.0"
|
|
77
77
|
},
|
|
78
78
|
"engines": {
|
|
79
|
-
"node": ">=
|
|
79
|
+
"node": ">=22",
|
|
80
|
+
"signalk-server": ">=2.25.0"
|
|
80
81
|
},
|
|
81
82
|
"signalk": {
|
|
82
83
|
"displayName": "eInk Label GenAI Templates",
|