iobroker.assistant 1.2.0 → 1.3.0

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.
Files changed (58) hide show
  1. package/README.md +11 -0
  2. package/admin/custom/.vite/manifest.json +18 -18
  3. package/admin/custom/assets/Components-x2Dfv8qE.js +2 -0
  4. package/admin/custom/assets/{_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-C5NPM0Of.js → _virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-B4YbqhBb.js} +1 -1
  5. package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-Cdyjt5KT.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-o4EU_Ut5.js} +10 -10
  6. package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare__shared-Dsw5zWS8.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare__shared--be2hWbV.js} +1 -1
  7. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-BsTz_EWE.js +122 -0
  8. package/admin/custom/assets/{hostInit-jJe8bIfV.js → hostInit-Blh7a-Qe.js} +1 -1
  9. package/admin/custom/assets/{lib-CL8mXvI6.js → lib-DBbNtolG.js} +1 -1
  10. package/admin/custom/assets/{mf-entry-bootstrap-0-ab227aa6.js → mf-entry-bootstrap-0-7abd30d3.js} +2 -2
  11. package/admin/custom/assets/{pendingShares-wrGtfliQ.js → pendingShares-U0D1MYK4.js} +1 -1
  12. package/admin/custom/customComponents.js +2 -2
  13. package/admin/custom/i18n/de.json +16 -0
  14. package/admin/custom/i18n/en.json +16 -0
  15. package/admin/custom/i18n/es.json +16 -0
  16. package/admin/custom/i18n/fr.json +16 -0
  17. package/admin/custom/i18n/it.json +16 -0
  18. package/admin/custom/i18n/nl.json +16 -0
  19. package/admin/custom/i18n/pl.json +16 -0
  20. package/admin/custom/i18n/pt.json +16 -0
  21. package/admin/custom/i18n/ru.json +16 -0
  22. package/admin/custom/i18n/uk.json +16 -0
  23. package/admin/custom/i18n/zh-cn.json +16 -0
  24. package/admin/custom/index.html +2 -2
  25. package/admin/custom/mf-manifest.json +1 -1
  26. package/admin/custom/mf-stats.json +1 -1
  27. package/admin/i18n/de.json +25 -2
  28. package/admin/i18n/en.json +25 -2
  29. package/admin/i18n/es.json +25 -2
  30. package/admin/i18n/fr.json +25 -2
  31. package/admin/i18n/it.json +25 -2
  32. package/admin/i18n/nl.json +25 -2
  33. package/admin/i18n/pl.json +25 -2
  34. package/admin/i18n/pt.json +25 -2
  35. package/admin/i18n/ru.json +25 -2
  36. package/admin/i18n/uk.json +25 -2
  37. package/admin/i18n/zh-cn.json +25 -2
  38. package/admin/jsonConfig.json +197 -13
  39. package/build/lib/credentials.js +11 -5
  40. package/build/lib/credentials.js.map +1 -1
  41. package/build/lib/llm.js +17 -3
  42. package/build/lib/llm.js.map +1 -1
  43. package/build/lib/nlu.js +249 -9
  44. package/build/lib/nlu.js.map +1 -1
  45. package/build/lib/piAgent.js +89 -0
  46. package/build/lib/piAgent.js.map +1 -0
  47. package/build/lib/systemAi.js +124 -0
  48. package/build/lib/systemAi.js.map +1 -0
  49. package/build/lib/voice/engines.js +31 -3
  50. package/build/lib/voice/engines.js.map +1 -1
  51. package/build/lib/voice/openrouter.js +220 -0
  52. package/build/lib/voice/openrouter.js.map +1 -0
  53. package/build/main.js +573 -59
  54. package/build/main.js.map +1 -1
  55. package/io-package.json +27 -17
  56. package/package.json +1 -1
  57. package/admin/custom/assets/Components-BSewCBfS.js +0 -2
  58. package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-SgSagTOv.js +0 -122
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "A fired timer/alarm keeps ringing (looping the sound) for up to this long, or until you say 'stop'/'halt'/'aufhören'/'cancel' (or write the stopRinging state). 0 = play the sound once.": "Увімкнений таймер/будильник дзвонить (зациклюється) протягом певного часу, або доки ви не скажете «стоп»/«зупинити»/«почути»/«скасувати» (або не запишете стан stopRinging). 0 = відтворити звук один раз.",
3
3
  "A phrase that runs a list of actions: \"Gute Nacht\" switches off five things and answers once. Phrases: several separated by commas (matched anywhere in the sentence, upper/lower case and umlauts do not matter). Actions as JSON, same shape as a trigger: [{\"setState\":{\"id\":\"hm-rpc.0.ABC.1.STATE\",\"value\":false}},{\"say\":\"Schlaf gut\",\"room\":\"Schlafzimmer\"}]. Reply: what the assistant answers (leave empty to stay silent). Matched before the offline rule engine and before the LLM, so a routine is never reinterpreted.": "Фраза, яка запускає список дій: \"Gute Nacht\" вимикає п'ять пристроїв і відповідає один раз. Фрази: кілька через кому (розпізнаються будь-де в реченні, регістр і умлаути не мають значення). Дії у форматі JSON, як у тригера: [{\"setState\":{\"id\":\"hm-rpc.0.ABC.1.STATE\",\"value\":false}},{\"say\":\"Schlaf gut\",\"room\":\"Schlafzimmer\"}]. Відповідь: що каже асистент (залиште порожнім, щоб нічого не казати). Перевіряється до офлайн-рушія правил і до LLM, тому рутина ніколи не буде витлумачена інакше.",
4
- "A successful switch command is confirmed by a short tone on the satellite instead of a spoken sentence - instant, where speaking costs a text-to-speech call. Questions, errors and the text channels are unaffected.": "Успішна команда перемикання підтверджується коротким сигналом на сателіті замість вимовленої фрази — миттєво, тоді як мовлення потребує виклику синтезу мовлення. Запитань, помилок і текстових каналів це не стосується.",
4
+ "A switch command that worked (on, off, set to a value) is confirmed by a short tone on the satellite instead of a spoken sentence, whether the offline rules or the AI carried it out. Questions, errors and the text channels are unaffected.": "Успішна команда перемикання (увімкнути, вимкнути, встановити значення) підтверджується коротким сигналом на сателіті замість вимовленої фрази — незалежно від того, чи виконали її офлайн-правила, чи ШІ. Питання, помилки й текстові канали не змінюються.",
5
5
  "A trigger watches states or the clock and then speaks, writes a state, or asks you a question and acts on your answer. Switch single triggers off at runtime with triggers.items.<id>.enabled, test one with its fire button, and all of them with triggers.enabled.": "Тригер стежить за станами або часом, а потім говорить, записує стан або ставить вам запитання й діє відповідно до вашої відповіді. Окремі тригери можна вимкнути під час роботи через triggers.items.<id>.enabled, перевірити кнопкою fire, а всі разом — через triggers.enabled.",
6
6
  "API key": "Ключ API",
7
7
  "API password (usually empty)": "Пароль API (зазвичай порожній)",
@@ -194,5 +194,28 @@
194
194
  "TV power (optional)": "Живлення ТВ (необов’язково)",
195
195
  "Start delay (s)": "Затримка старту (с)",
196
196
  "Name": "Ім’я",
197
- "TVs and devices of the youtube-remote adapter the assistant may play on, e.g. \"play the channel Sailing OceanAir\". The first row is the default when no player is named; a request from a satellite in the room of a row goes to that row. Player: the youtube-remote instance - it finds its paired TV itself, also after the TV was paired again; with several TVs in one instance pick one of them. Power: optional state that switches the TV on (e.g. samsungtv.0.<tv>.control.power) - if it is off, the TV is switched on first and the video starts after the start delay. A power button (toggle) is never pressed, because it would switch a running TV off. Needs \"Allow writing states\".": "Телевізори та пристрої адаптера youtube-remote, на яких асистент може відтворювати, наприклад «увімкни канал Sailing OceanAir». Перший рядок використовується за замовчуванням, якщо плеєр не названо; запит від супутника в кімнаті рядка йде на цей рядок. Плеєр: екземпляр youtube-remote — він сам знаходить пов’язаний телевізор, зокрема після повторного прив’язування; якщо в екземплярі кілька телевізорів, виберіть один із них. Живлення: необов’язковий стан, що вмикає телевізор (наприклад samsungtv.0.<tv>.control.power) — якщо телевізор вимкнено, його спочатку вмикають, а відео стартує після затримки. Кнопку живлення (перемикач) ніколи не натискають, бо вона вимкнула б працюючий телевізор. Потрібно «Дозволити запис станів»."
197
+ "TVs and devices of the youtube-remote adapter the assistant may play on, e.g. \"play the channel Sailing OceanAir\". The first row is the default when no player is named; a request from a satellite in the room of a row goes to that row. Player: the youtube-remote instance - it finds its paired TV itself, also after the TV was paired again; with several TVs in one instance pick one of them. Power: optional state that switches the TV on (e.g. samsungtv.0.<tv>.control.power) - if it is off, the TV is switched on first and the video starts after the start delay. A power button (toggle) is never pressed, because it would switch a running TV off. Needs \"Allow writing states\".": "Телевізори та пристрої адаптера youtube-remote, на яких асистент може відтворювати, наприклад «увімкни канал Sailing OceanAir». Перший рядок використовується за замовчуванням, якщо плеєр не названо; запит від супутника в кімнаті рядка йде на цей рядок. Плеєр: екземпляр youtube-remote — він сам знаходить пов’язаний телевізор, зокрема після повторного прив’язування; якщо в екземплярі кілька телевізорів, виберіть один із них. Живлення: необов’язковий стан, що вмикає телевізор (наприклад samsungtv.0.<tv>.control.power) — якщо телевізор вимкнено, його спочатку вмикають, а відео стартує після затримки. Кнопку живлення (перемикач) ніколи не натискають, бо вона вимкнула б працюючий телевізор. Потрібно «Дозволити запис станів».",
198
+ "Adapter pi-agent": "Адаптер pi-agent",
199
+ "pi-agent instance": "Екземпляр pi-agent",
200
+ "pi-agent timeout (s)": "Тайм-аут pi-agent (с)",
201
+ "Everything the local tiers cannot answer goes to the adapter pi-agent: its model and key (also the system-wide AI settings of admin), its tools and its write permissions. Every request source gets a conversation of its own there. The tools of this adapter (timers, memory, announcements, YouTube) are not available to it; the rule-based commands still handle timers and alarms.": "Усе, на що не можуть відповісти локальні рівні, передається адаптеру pi-agent: його модель і ключ (зокрема загальносистемні налаштування ШІ в admin), його інструменти та права запису. Кожне джерело запитів отримує там власну розмову. Інструменти цього адаптера (таймери, пам'ять, оголошення, YouTube) йому недоступні; таймери й будильники й надалі обробляють команди на основі правил.",
202
+ "System-wide AI settings (admin)": "Загальносистемні налаштування ШІ (admin)",
203
+ "OpenRouter": "OpenRouter",
204
+ "Provider, model and API key come from the system settings of admin (tab \"AI\"). A change there restarts this instance.": "Провайдер, модель і API-ключ беруться з системних налаштувань admin (вкладка «ШІ»). Їх зміна перезапускає цей екземпляр.",
205
+ "Empty: the system-wide AI settings of admin are used": "Порожньо: використовуються загальносистемні налаштування ШІ з admin",
206
+ "OpenRouter key for speech (optional)": "Ключ OpenRouter для мовлення (необов’язково)",
207
+ "OpenRouter credential": "Облікові дані OpenRouter",
208
+ "Empty: the key of the AI provider is used if that is OpenRouter (also from the system-wide AI settings).": "Порожньо: використовується ключ ШІ-провайдера, якщо це OpenRouter (також із загальносистемних налаштувань ШІ).",
209
+ "OpenRouter STT model (optional)": "Модель OpenRouter STT (необов’язково)",
210
+ "Empty = openai/whisper-large-v3-turbo.": "Порожньо = openai/whisper-large-v3-turbo.",
211
+ "OpenRouter TTS model (optional)": "Модель OpenRouter TTS (необов’язково)",
212
+ "Empty = elevenlabs/eleven-flash-v2.5.": "Порожньо = elevenlabs/eleven-flash-v2.5.",
213
+ "OpenRouter voice (optional)": "Голос OpenRouter (необов’язково)",
214
+ "The voices depend on the model. Empty = the first voice of the model for the language.": "Голоси залежать від моделі. Порожньо = перший голос моделі для мови.",
215
+ "Calendar for commands for later": "Календар для відкладених команд",
216
+ "A fullcalendar instance: \"turn off the light in 2 hours\" or \"blinds to 50 % at 10 pm\" becomes an event in this calendar, and the AI can plan, list and cancel events. Without it such commands go to the AI.": "Екземпляр fullcalendar: «вимкни світло через 2 години» або «жалюзі на 50 % о 22:00» стає подією в цьому календарі, а ШІ може планувати, показувати й видаляти події. Без нього такі команди йдуть до ШІ.",
217
+ "Person": "Людина",
218
+ "Guest": "Гість",
219
+ "Pet": "Улюбленець",
220
+ "Group": "Група"
198
221
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "A fired timer/alarm keeps ringing (looping the sound) for up to this long, or until you say 'stop'/'halt'/'aufhören'/'cancel' (or write the stopRinging state). 0 = play the sound once.": "触发的定时器/警报会持续响铃(循环播放声音),最长可达设定时间,或直到您说“停止”、“暂停”、“取消”(或写入 stopRinging 状态)。0 = 播放一次声音。",
3
3
  "A phrase that runs a list of actions: \"Gute Nacht\" switches off five things and answers once. Phrases: several separated by commas (matched anywhere in the sentence, upper/lower case and umlauts do not matter). Actions as JSON, same shape as a trigger: [{\"setState\":{\"id\":\"hm-rpc.0.ABC.1.STATE\",\"value\":false}},{\"say\":\"Schlaf gut\",\"room\":\"Schlafzimmer\"}]. Reply: what the assistant answers (leave empty to stay silent). Matched before the offline rule engine and before the LLM, so a routine is never reinterpreted.": "一个执行一系列动作的短语:\"Gute Nacht\" 关闭五个设备并只回答一次。短语:多个短语用逗号分隔(在句子任意位置匹配,不区分大小写和变音符号)。动作为 JSON,格式与触发器相同:[{\"setState\":{\"id\":\"hm-rpc.0.ABC.1.STATE\",\"value\":false}},{\"say\":\"Schlaf gut\",\"room\":\"Schlafzimmer\"}]。回复:助手的回答(留空则不出声)。在离线规则引擎和 LLM 之前匹配,因此例程永远不会被重新解读。",
4
- "A successful switch command is confirmed by a short tone on the satellite instead of a spoken sentence - instant, where speaking costs a text-to-speech call. Questions, errors and the text channels are unaffected.": "成功的开关命令会在卫星设备上以短促提示音确认,而不是说出一句话——即时完成,而语音播报需要一次文字转语音调用。问题、错误和文本通道不受影响。",
4
+ "A switch command that worked (on, off, set to a value) is confirmed by a short tone on the satellite instead of a spoken sentence, whether the offline rules or the AI carried it out. Questions, errors and the text channels are unaffected.": "执行成功的开关命令(开、关、设为某值)会在卫星设备上用一声短促的提示音确认,而不是说出一句话,无论是离线规则还是 AI 执行的。问题、错误和文本渠道不受影响。",
5
5
  "A trigger watches states or the clock and then speaks, writes a state, or asks you a question and acts on your answer. Switch single triggers off at runtime with triggers.items.<id>.enabled, test one with its fire button, and all of them with triggers.enabled.": "触发器监视状态或时钟,然后播报、写入状态,或向你提问并根据你的回答执行操作。运行时可通过 triggers.items.<id>.enabled 关闭单个触发器,用其 fire 按钮测试单个触发器,通过 triggers.enabled 控制全部触发器。",
6
6
  "API key": "API密钥",
7
7
  "API password (usually empty)": "API 密码(通常为空)",
@@ -194,5 +194,28 @@
194
194
  "TV power (optional)": "电视电源(可选)",
195
195
  "Start delay (s)": "启动延迟(秒)",
196
196
  "Name": "名称",
197
- "TVs and devices of the youtube-remote adapter the assistant may play on, e.g. \"play the channel Sailing OceanAir\". The first row is the default when no player is named; a request from a satellite in the room of a row goes to that row. Player: the youtube-remote instance - it finds its paired TV itself, also after the TV was paired again; with several TVs in one instance pick one of them. Power: optional state that switches the TV on (e.g. samsungtv.0.<tv>.control.power) - if it is off, the TV is switched on first and the video starts after the start delay. A power button (toggle) is never pressed, because it would switch a running TV off. Needs \"Allow writing states\".": "助手可以在其上播放的 youtube-remote 适配器的电视和设备,例如“播放频道 Sailing OceanAir”。未指定播放器时使用第一行;来自某行所在房间卫星的请求发送到该行。播放器:youtube-remote 实例——它会自行找到已配对的电视,重新配对后也一样;一个实例中有多台电视时,选择其中一台。电源:可选的开启电视的状态(例如 samsungtv.0.<tv>.control.power)——如果电视关闭,会先开机,视频在启动延迟后开始。电源按钮(切换键)永远不会被按下,因为它会关闭正在运行的电视。需要“允许写入状态”。"
197
+ "TVs and devices of the youtube-remote adapter the assistant may play on, e.g. \"play the channel Sailing OceanAir\". The first row is the default when no player is named; a request from a satellite in the room of a row goes to that row. Player: the youtube-remote instance - it finds its paired TV itself, also after the TV was paired again; with several TVs in one instance pick one of them. Power: optional state that switches the TV on (e.g. samsungtv.0.<tv>.control.power) - if it is off, the TV is switched on first and the video starts after the start delay. A power button (toggle) is never pressed, because it would switch a running TV off. Needs \"Allow writing states\".": "助手可以在其上播放的 youtube-remote 适配器的电视和设备,例如“播放频道 Sailing OceanAir”。未指定播放器时使用第一行;来自某行所在房间卫星的请求发送到该行。播放器:youtube-remote 实例——它会自行找到已配对的电视,重新配对后也一样;一个实例中有多台电视时,选择其中一台。电源:可选的开启电视的状态(例如 samsungtv.0.<tv>.control.power)——如果电视关闭,会先开机,视频在启动延迟后开始。电源按钮(切换键)永远不会被按下,因为它会关闭正在运行的电视。需要“允许写入状态”。",
198
+ "Adapter pi-agent": "pi-agent 适配器",
199
+ "pi-agent instance": "pi-agent 实例",
200
+ "pi-agent timeout (s)": "pi-agent 超时(秒)",
201
+ "Everything the local tiers cannot answer goes to the adapter pi-agent: its model and key (also the system-wide AI settings of admin), its tools and its write permissions. Every request source gets a conversation of its own there. The tools of this adapter (timers, memory, announcements, YouTube) are not available to it; the rule-based commands still handle timers and alarms.": "本地层级无法回答的所有内容都会交给 pi-agent 适配器:使用它的模型和密钥(也可以是 admin 的系统级 AI 设置)、它的工具和写入权限。每个请求来源在那里都有自己的对话。本适配器的工具(计时器、记忆、广播、YouTube)在那里不可用;计时器和闹钟仍由基于规则的命令处理。",
202
+ "System-wide AI settings (admin)": "系统级 AI 设置(admin)",
203
+ "OpenRouter": "OpenRouter",
204
+ "Provider, model and API key come from the system settings of admin (tab \"AI\"). A change there restarts this instance.": "提供商、模型和 API 密钥来自 admin 的系统设置(“AI”选项卡)。在那里更改会重启此实例。",
205
+ "Empty: the system-wide AI settings of admin are used": "留空:使用 admin 的系统级 AI 设置",
206
+ "OpenRouter key for speech (optional)": "用于语音的 OpenRouter 密钥(可选)",
207
+ "OpenRouter credential": "OpenRouter 凭据",
208
+ "Empty: the key of the AI provider is used if that is OpenRouter (also from the system-wide AI settings).": "留空:如果 AI 提供商是 OpenRouter(也包括系统级 AI 设置),则使用其密钥。",
209
+ "OpenRouter STT model (optional)": "OpenRouter STT 模型(可选)",
210
+ "Empty = openai/whisper-large-v3-turbo.": "留空 = openai/whisper-large-v3-turbo。",
211
+ "OpenRouter TTS model (optional)": "OpenRouter TTS 模型(可选)",
212
+ "Empty = elevenlabs/eleven-flash-v2.5.": "留空 = elevenlabs/eleven-flash-v2.5。",
213
+ "OpenRouter voice (optional)": "OpenRouter 语音(可选)",
214
+ "The voices depend on the model. Empty = the first voice of the model for the language.": "可用语音取决于模型。留空 = 该模型在此语言下的第一个语音。",
215
+ "Calendar for commands for later": "用于延时命令的日历",
216
+ "A fullcalendar instance: \"turn off the light in 2 hours\" or \"blinds to 50 % at 10 pm\" becomes an event in this calendar, and the AI can plan, list and cancel events. Without it such commands go to the AI.": "fullcalendar 实例:“2 小时后关灯”或“晚上 10 点把百叶窗调到 50 %”会成为此日历中的事件,AI 也可以计划、列出和删除事件。未设置时,此类命令交给 AI 处理。",
217
+ "Person": "人员",
218
+ "Guest": "访客",
219
+ "Pet": "宠物",
220
+ "Group": "组"
198
221
  }
@@ -21,25 +21,38 @@
21
21
  "type": "select",
22
22
  "label": "Provider",
23
23
  "options": [
24
+ { "label": "System-wide AI settings (admin)", "value": "system" },
24
25
  { "label": "OpenAI", "value": "openai" },
25
26
  { "label": "Anthropic (Claude)", "value": "anthropic" },
26
27
  { "label": "Google Gemini", "value": "gemini" },
27
28
  { "label": "DeepSeek", "value": "deepseek" },
29
+ { "label": "OpenRouter", "value": "openrouter" },
28
30
  { "label": "OpenAI-compatible (custom)", "value": "custom" },
31
+ { "label": "Adapter pi-agent", "value": "pi-agent" },
29
32
  { "label": "None (local only)", "value": "none" }
30
33
  ],
31
- "default": "openai",
34
+ "default": "system",
32
35
  "xs": 12,
33
36
  "sm": 12,
34
37
  "md": 4,
35
38
  "lg": 4,
36
39
  "xl": 4
37
40
  },
41
+ "_systemAiInfo": {
42
+ "type": "staticText",
43
+ "text": "Provider, model and API key come from the system settings of admin (tab \"AI\"). A change there restarts this instance.",
44
+ "hidden": "data.provider !== 'system'",
45
+ "xs": 12,
46
+ "sm": 12,
47
+ "md": 8,
48
+ "lg": 8,
49
+ "xl": 8
50
+ },
38
51
  "credentialType": {
39
52
  "newLine": true,
40
53
  "type": "select",
41
54
  "label": "Credential mode",
42
- "hidden": "data.provider === 'none'",
55
+ "hidden": "data.provider === 'none' || data.provider === 'pi-agent' || data.provider === 'system'",
43
56
  "options": [
44
57
  { "label": "Store key in adapter", "value": "manual" },
45
58
  { "label": "Central credential store", "value": "manager" }
@@ -54,7 +67,8 @@
54
67
  "apiKey": {
55
68
  "type": "password",
56
69
  "label": "API key",
57
- "hidden": "data.provider === 'none' || data.credentialType === 'manager'",
70
+ "help": "Empty: the system-wide AI settings of admin are used",
71
+ "hidden": "data.provider === 'none' || data.provider === 'pi-agent' || data.provider === 'system' || data.credentialType === 'manager'",
58
72
  "xs": 12,
59
73
  "sm": 12,
60
74
  "md": 4,
@@ -65,7 +79,7 @@
65
79
  "type": "credential",
66
80
  "credentialType": "ai",
67
81
  "label": "API-key credential",
68
- "hidden": "data.provider === 'none' || data.credentialType !== 'manager'",
82
+ "hidden": "data.provider === 'none' || data.provider === 'pi-agent' || data.provider === 'system' || data.credentialType !== 'manager'",
69
83
  "xs": 12,
70
84
  "sm": 12,
71
85
  "md": 4,
@@ -81,7 +95,7 @@
81
95
  "freeSolo": true,
82
96
  "label": "Model",
83
97
  "help": "Loaded from the provider (instance must run); you can also type a model id.",
84
- "hidden": "data.provider === 'none'",
98
+ "hidden": "data.provider === 'none' || data.provider === 'pi-agent' || data.provider === 'system'",
85
99
  "default": "gpt-4o-mini",
86
100
  "xs": 12,
87
101
  "sm": 12,
@@ -107,14 +121,50 @@
107
121
  "command": "testApiConnection",
108
122
  "jsonData": "{\"provider\":\"${data.provider}\",\"apiKey\":\"${data.apiKey}\",\"credentialType\":\"${data.credentialType}\",\"model\":\"${data.model}\",\"baseUrl\":\"${data.baseUrl}\",\"credentialId\":\"${data.credentialIdApiKey}\"}",
109
123
  "variant": "outlined",
110
- "hidden": "data.provider === 'none'",
111
- "disabled": "data.provider !== 'custom' && (data.credentialType === 'manager' ? !data.credentialIdApiKey : !data.apiKey)",
124
+ "hidden": "data.provider === 'none' || data.provider === 'pi-agent'",
125
+ "disabled": "data.provider !== 'custom' && data.provider !== 'system' && (data.credentialType === 'manager' ? !data.credentialIdApiKey : !data.apiKey)",
126
+ "xs": 12,
127
+ "sm": 12,
128
+ "md": 4,
129
+ "lg": 4,
130
+ "xl": 4
131
+ },
132
+ "piAgentInstance": {
133
+ "newLine": true,
134
+ "type": "instance",
135
+ "adapter": "pi-agent",
136
+ "label": "pi-agent instance",
137
+ "hidden": "data.provider !== 'pi-agent'",
112
138
  "xs": 12,
113
139
  "sm": 12,
114
140
  "md": 4,
115
141
  "lg": 4,
116
142
  "xl": 4
117
143
  },
144
+ "piAgentTimeoutSec": {
145
+ "type": "number",
146
+ "label": "pi-agent timeout (s)",
147
+ "hidden": "data.provider !== 'pi-agent'",
148
+ "default": 60,
149
+ "min": 5,
150
+ "max": 600,
151
+ "xs": 12,
152
+ "sm": 6,
153
+ "md": 2,
154
+ "lg": 2,
155
+ "xl": 2
156
+ },
157
+ "_piAgentInfo": {
158
+ "newLine": true,
159
+ "type": "staticText",
160
+ "text": "Everything the local tiers cannot answer goes to the adapter pi-agent: its model and key (also the system-wide AI settings of admin), its tools and its write permissions. Every request source gets a conversation of its own there. The tools of this adapter (timers, memory, announcements, YouTube) are not available to it; the rule-based commands still handle timers and alarms.",
161
+ "hidden": "data.provider !== 'pi-agent'",
162
+ "xs": 12,
163
+ "sm": 12,
164
+ "md": 12,
165
+ "lg": 12,
166
+ "xl": 12
167
+ },
118
168
  "_noneInfo": {
119
169
  "newLine": true,
120
170
  "type": "staticText",
@@ -130,7 +180,7 @@
130
180
  "newLine": true,
131
181
  "type": "number",
132
182
  "label": "Max tokens",
133
- "hidden": "data.provider === 'none'",
183
+ "hidden": "data.provider === 'none' || data.provider === 'pi-agent'",
134
184
  "default": 1024,
135
185
  "min": 64,
136
186
  "max": 8192,
@@ -154,9 +204,9 @@
154
204
  "confirmWithTone": {
155
205
  "type": "checkbox",
156
206
  "label": "Confirm switch commands with a beep",
157
- "help": "A successful switch command is confirmed by a short tone on the satellite instead of a spoken sentence - instant, where speaking costs a text-to-speech call. Questions, errors and the text channels are unaffected.",
207
+ "help": "A switch command that worked (on, off, set to a value) is confirmed by a short tone on the satellite instead of a spoken sentence, whether the offline rules or the AI carried it out. Questions, errors and the text channels are unaffected.",
158
208
  "default": false,
159
- "hidden": "!data.useLocalNlu",
209
+ "hidden": "!data.voiceEnabled",
160
210
  "xs": 12,
161
211
  "sm": 6,
162
212
  "md": 4,
@@ -243,6 +293,19 @@
243
293
  "lg": 6,
244
294
  "xl": 6
245
295
  },
296
+ "calendarInstance": {
297
+ "newLine": true,
298
+ "type": "instance",
299
+ "adapter": "fullcalendar",
300
+ "allowDeactivate": true,
301
+ "label": "Calendar for commands for later",
302
+ "help": "A fullcalendar instance: \"turn off the light in 2 hours\" or \"blinds to 50 % at 10 pm\" becomes an event in this calendar, and the AI can plan, list and cancel events. Without it such commands go to the AI.",
303
+ "xs": 12,
304
+ "sm": 12,
305
+ "md": 6,
306
+ "lg": 6,
307
+ "xl": 6
308
+ },
246
309
  "presence": {
247
310
  "newLine": true,
248
311
  "type": "table",
@@ -254,9 +317,20 @@
254
317
  "lg": 12,
255
318
  "xl": 12,
256
319
  "items": [
257
- { "type": "text", "attr": "id", "title": "State id", "width": "45%" },
320
+ { "type": "objectId", "attr": "id", "title": "State id", "width": "45%" },
258
321
  { "type": "text", "attr": "name", "title": "Name", "width": "25%" },
259
- { "type": "text", "attr": "kind", "title": "Kind (person/guest/pet)", "width": "15%" },
322
+ {
323
+ "type": "select",
324
+ "attr": "kind",
325
+ "title": "Kind (person/guest/pet)",
326
+ "width": "15%",
327
+ "options": [
328
+ { "label": "Person", "value": "person" },
329
+ { "label": "Guest", "value": "guest" },
330
+ { "label": "Pet", "value": "pet" }
331
+ ],
332
+ "default": "person"
333
+ },
260
334
  { "type": "text", "attr": "homeValue", "title": "Home value (empty = auto)", "width": "15%" }
261
335
  ]
262
336
  },
@@ -912,7 +986,17 @@
912
986
  "title": "Satellites, rooms or devices (comma separated)",
913
987
  "width": "60%"
914
988
  },
915
- { "type": "text", "attr": "kind", "title": "Kind (group/person)", "width": "15%" }
989
+ {
990
+ "type": "select",
991
+ "attr": "kind",
992
+ "title": "Kind (group/person)",
993
+ "width": "15%",
994
+ "options": [
995
+ { "label": "Group", "value": "group" },
996
+ { "label": "Person", "value": "person" }
997
+ ],
998
+ "default": "group"
999
+ }
916
1000
  ]
917
1001
  },
918
1002
  "sttProvider": {
@@ -921,6 +1005,7 @@
921
1005
  "label": "Speech-to-text provider",
922
1006
  "options": [
923
1007
  { "label": "OpenAI (Whisper)", "value": "openai" },
1008
+ { "label": "OpenRouter", "value": "openrouter" },
924
1009
  { "label": "Azure Speech", "value": "azure" },
925
1010
  { "label": "AWS Transcribe", "value": "aws" },
926
1011
  { "label": "Vosk (local)", "value": "vosk" }
@@ -938,6 +1023,7 @@
938
1023
  "label": "Text-to-speech provider",
939
1024
  "options": [
940
1025
  { "label": "OpenAI", "value": "openai" },
1026
+ { "label": "OpenRouter", "value": "openrouter" },
941
1027
  { "label": "Azure Speech", "value": "azure" },
942
1028
  { "label": "AWS Polly", "value": "aws" },
943
1029
  { "label": "Piper (local)", "value": "piper" }
@@ -958,6 +1044,7 @@
958
1044
  "options": [
959
1045
  { "label": "None", "value": "" },
960
1046
  { "label": "OpenAI (Whisper)", "value": "openai" },
1047
+ { "label": "OpenRouter", "value": "openrouter" },
961
1048
  { "label": "Azure Speech", "value": "azure" },
962
1049
  { "label": "AWS Transcribe", "value": "aws" },
963
1050
  { "label": "Vosk (local)", "value": "vosk" }
@@ -977,6 +1064,7 @@
977
1064
  "options": [
978
1065
  { "label": "None", "value": "" },
979
1066
  { "label": "OpenAI", "value": "openai" },
1067
+ { "label": "OpenRouter", "value": "openrouter" },
980
1068
  { "label": "Azure Speech", "value": "azure" },
981
1069
  { "label": "AWS Polly", "value": "aws" },
982
1070
  { "label": "Piper (local)", "value": "piper" }
@@ -1081,6 +1169,87 @@
1081
1169
  "lg": 4,
1082
1170
  "xl": 4
1083
1171
  },
1172
+ "_openrouterSpeechHeader": {
1173
+ "newLine": true,
1174
+ "type": "header",
1175
+ "size": 3,
1176
+ "text": "OpenRouter",
1177
+ "hidden": "!data.voiceEnabled || (data.sttProvider !== 'openrouter' && data.ttsProvider !== 'openrouter' && data.sttFallback !== 'openrouter' && data.ttsFallback !== 'openrouter')",
1178
+ "xs": 12,
1179
+ "sm": 12,
1180
+ "md": 12,
1181
+ "lg": 12,
1182
+ "xl": 12
1183
+ },
1184
+ "openrouterSpeechKey": {
1185
+ "newLine": true,
1186
+ "type": "password",
1187
+ "label": "OpenRouter key for speech (optional)",
1188
+ "help": "Empty: the key of the AI provider is used if that is OpenRouter (also from the system-wide AI settings).",
1189
+ "hidden": "!data.voiceEnabled || (data.sttProvider !== 'openrouter' && data.ttsProvider !== 'openrouter' && data.sttFallback !== 'openrouter' && data.ttsFallback !== 'openrouter') || data.voiceCredentialType === 'manager'",
1190
+ "xs": 12,
1191
+ "sm": 12,
1192
+ "md": 8,
1193
+ "lg": 8,
1194
+ "xl": 8
1195
+ },
1196
+ "openrouterCredentialId": {
1197
+ "newLine": true,
1198
+ "type": "credential",
1199
+ "credentialType": "ai",
1200
+ "label": "OpenRouter credential",
1201
+ "help": "Empty: the key of the AI provider is used if that is OpenRouter (also from the system-wide AI settings).",
1202
+ "hidden": "!data.voiceEnabled || (data.sttProvider !== 'openrouter' && data.ttsProvider !== 'openrouter' && data.sttFallback !== 'openrouter' && data.ttsFallback !== 'openrouter') || data.voiceCredentialType !== 'manager'",
1203
+ "xs": 12,
1204
+ "sm": 12,
1205
+ "md": 6,
1206
+ "lg": 6,
1207
+ "xl": 6
1208
+ },
1209
+ "openrouterSttModel": {
1210
+ "newLine": true,
1211
+ "type": "autocompleteSendTo",
1212
+ "command": "getSttModels",
1213
+ "jsonData": "{\"sttProvider\":\"openrouter\",\"language\":\"${data.voiceLanguage}\"}",
1214
+ "freeSolo": true,
1215
+ "label": "OpenRouter STT model (optional)",
1216
+ "help": "Empty = openai/whisper-large-v3-turbo.",
1217
+ "hidden": "!data.voiceEnabled || (data.sttProvider !== 'openrouter' && data.sttFallback !== 'openrouter')",
1218
+ "xs": 12,
1219
+ "sm": 6,
1220
+ "md": 4,
1221
+ "lg": 4,
1222
+ "xl": 4
1223
+ },
1224
+ "openrouterTtsModel": {
1225
+ "type": "autocompleteSendTo",
1226
+ "command": "getTtsModels",
1227
+ "jsonData": "{\"ttsProvider\":\"openrouter\"}",
1228
+ "freeSolo": true,
1229
+ "label": "OpenRouter TTS model (optional)",
1230
+ "help": "Empty = elevenlabs/eleven-flash-v2.5.",
1231
+ "hidden": "!data.voiceEnabled || (data.ttsProvider !== 'openrouter' && data.ttsFallback !== 'openrouter')",
1232
+ "xs": 12,
1233
+ "sm": 6,
1234
+ "md": 4,
1235
+ "lg": 4,
1236
+ "xl": 4
1237
+ },
1238
+ "openrouterVoice": {
1239
+ "type": "autocompleteSendTo",
1240
+ "command": "getVoices",
1241
+ "jsonData": "{\"ttsProvider\":\"openrouter\",\"language\":\"${data.voiceLanguage}\",\"openrouterTtsModel\":\"${data.openrouterTtsModel}\"}",
1242
+ "alsoDependsOn": ["openrouterTtsModel", "voiceLanguage"],
1243
+ "freeSolo": true,
1244
+ "label": "OpenRouter voice (optional)",
1245
+ "help": "The voices depend on the model. Empty = the first voice of the model for the language.",
1246
+ "hidden": "!data.voiceEnabled || (data.ttsProvider !== 'openrouter' && data.ttsFallback !== 'openrouter')",
1247
+ "xs": 12,
1248
+ "sm": 6,
1249
+ "md": 4,
1250
+ "lg": 4,
1251
+ "xl": 4
1252
+ },
1084
1253
  "_azureSpeechHeader": {
1085
1254
  "newLine": true,
1086
1255
  "type": "header",
@@ -1284,6 +1453,21 @@
1284
1453
  "md": 5,
1285
1454
  "lg": 5,
1286
1455
  "xl": 5
1456
+ },
1457
+ "_voiceTest": {
1458
+ "newLine": true,
1459
+ "type": "custom",
1460
+ "i18n": true,
1461
+ "url": "custom/customComponents.js",
1462
+ "name": "ConfigCustomAssistant/Components/VoiceTestComponent",
1463
+ "bundlerType": "module",
1464
+ "guiApi": 2,
1465
+ "hidden": "!data.voiceEnabled",
1466
+ "xs": 12,
1467
+ "sm": 12,
1468
+ "md": 8,
1469
+ "lg": 8,
1470
+ "xl": 8
1287
1471
  }
1288
1472
  }
1289
1473
  },
@@ -111,23 +111,28 @@ function str(v) {
111
111
  return '';
112
112
  }
113
113
  /**
114
- * Resolve the speech (STT/TTS) credentials for OpenAI, Azure and AWS.
114
+ * Resolve the speech (STT/TTS) credentials for OpenAI, OpenRouter, Azure and AWS.
115
115
  * - 'manual': the encrypted key/region config fields.
116
116
  * - 'manager': the central credential store, keyed by the *CredentialId fields
117
117
  * (azure values: { key, region }; aws values: { accessKeyId, secretAccessKey, region }).
118
- * `mainApiKey` is reused for OpenAI speech when no dedicated OpenAI speech credential is set and the
119
- * LLM provider is already OpenAI.
118
+ * The key of the cloud LLM (`llm`) is reused for OpenAI or OpenRouter speech when that speech vendor has no key of
119
+ * its own and the LLM is of the same vendor.
120
120
  */
121
- async function resolveVoiceCredentials(adapter, config, mainApiKey) {
122
- const openaiFallback = config.provider === 'openai' ? mainApiKey : '';
121
+ async function resolveVoiceCredentials(adapter, config, llm) {
122
+ const openaiFallback = llm?.provider === 'openai' ? llm.apiKey : '';
123
+ const openrouterFallback = llm?.provider === 'openrouter' ? llm.apiKey : '';
123
124
  if (config.voiceCredentialType === 'manager') {
124
125
  const openaiKey = config.voiceCredentialId
125
126
  ? await readCentralKey(adapter, config.voiceCredentialId)
126
127
  : openaiFallback;
128
+ const openrouterKey = config.openrouterCredentialId
129
+ ? await readCentralKey(adapter, config.openrouterCredentialId)
130
+ : openrouterFallback;
127
131
  const azure = config.azureCredentialId ? await readCentralValues(adapter, config.azureCredentialId) : {};
128
132
  const aws = config.awsCredentialId ? await readCentralValues(adapter, config.awsCredentialId) : {};
129
133
  return {
130
134
  openaiKey,
135
+ openrouterKey,
131
136
  azureKey: str(azure.key),
132
137
  azureRegion: str(azure.region),
133
138
  aws: {
@@ -139,6 +144,7 @@ async function resolveVoiceCredentials(adapter, config, mainApiKey) {
139
144
  }
140
145
  return {
141
146
  openaiKey: str(config.voiceApiKey) || openaiFallback,
147
+ openrouterKey: str(config.openrouterSpeechKey) || openrouterFallback,
142
148
  azureKey: str(config.azureSpeechKey),
143
149
  azureRegion: str(config.azureSpeechRegion),
144
150
  aws: {
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/lib/credentials.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Resolve the LLM API key from the adapter config.\n *\n * Two modes (`config.credentialType`):\n * - 'manual': the key is stored directly in the (encrypted) adapter config (`config.apiKey`).\n * - 'manager': the config only stores the ID of a credential in the central ioBroker\n * credential store (`system.credentials.*`); the real key is resolved at runtime\n * via the Credentials API.\n *\n * The central Credentials API (`Credentials.getCredentials`) is only available with\n * js-controller >= 7.2 / a recent \\@iobroker/adapter-core. We access it defensively so the\n * adapter still compiles and runs (in `manual` mode) on older controllers.\n */\nimport * as Credentials from '@iobroker/adapter-core/credentials';\nimport type { AdapterConfig } from '../types';\nimport type { VoiceCredentials } from './voice/engines';\n\n/** Read and decrypt a single credential's key from the central store; '' (and a warning) on error. */\nexport async function readCentralKey(adapter: ioBroker.Adapter, id: string): Promise<string> {\n if (!id) {\n return '';\n }\n if (!Credentials?.getCredentials) {\n adapter.log.warn(`Cannot read credential \"${id}\": the central Credentials API needs js-controller >= 7.2`);\n return '';\n }\n try {\n const cred = await Credentials.getCredentials(adapter, id);\n return ((cred?.values?.key as string) || '').trim();\n } catch (e) {\n adapter.log.warn(`Cannot read credential \"${id}\": ${(e as Error).message}`);\n return '';\n }\n}\n\n/**\n * Resolve the effective API key for the configured provider.\n * `overrideKey`/`overrideCredentialId` win over the stored config (used by the settings-dialog\n * Test button, where unsaved form values should be used).\n */\nexport async function resolveApiKey(\n adapter: ioBroker.Adapter,\n config: AdapterConfig,\n override?: { credentialType?: 'manual' | 'manager'; apiKey?: string; credentialId?: string },\n): Promise<string> {\n const mode = override?.credentialType || config.credentialType || 'manual';\n if (mode === 'manager') {\n const id = (override?.credentialId || config.credentialIdApiKey || '').trim();\n return readCentralKey(adapter, id);\n }\n return (override?.apiKey || config.apiKey || '').trim();\n}\n\n/** Read and decrypt all fields of a credential from the central store; {} (and a warning) on error. */\nexport async function readCentralValues(adapter: ioBroker.Adapter, id: string): Promise<Record<string, unknown>> {\n if (!id) {\n return {};\n }\n if (!Credentials?.getCredentials) {\n adapter.log.warn(`Cannot read credential \"${id}\": the central Credentials API needs js-controller >= 7.2`);\n return {};\n }\n try {\n const cred = await Credentials.getCredentials(adapter, id);\n return cred?.values || {};\n } catch (e) {\n adapter.log.warn(`Cannot read credential \"${id}\": ${(e as Error).message}`);\n return {};\n }\n}\n\n/** Safe stringify of a credential/config field (values are strings/numbers/booleans). */\nfunction str(v: unknown): string {\n if (typeof v === 'string') {\n return v.trim();\n }\n if (typeof v === 'number' || typeof v === 'boolean') {\n return String(v);\n }\n return '';\n}\n\n/**\n * Resolve the speech (STT/TTS) credentials for OpenAI, Azure and AWS.\n * - 'manual': the encrypted key/region config fields.\n * - 'manager': the central credential store, keyed by the *CredentialId fields\n * (azure values: { key, region }; aws values: { accessKeyId, secretAccessKey, region }).\n * `mainApiKey` is reused for OpenAI speech when no dedicated OpenAI speech credential is set and the\n * LLM provider is already OpenAI.\n */\nexport async function resolveVoiceCredentials(\n adapter: ioBroker.Adapter,\n config: AdapterConfig,\n mainApiKey: string,\n): Promise<VoiceCredentials> {\n const openaiFallback = config.provider === 'openai' ? mainApiKey : '';\n\n if (config.voiceCredentialType === 'manager') {\n const openaiKey = config.voiceCredentialId\n ? await readCentralKey(adapter, config.voiceCredentialId)\n : openaiFallback;\n const azure = config.azureCredentialId ? await readCentralValues(adapter, config.azureCredentialId) : {};\n const aws = config.awsCredentialId ? await readCentralValues(adapter, config.awsCredentialId) : {};\n return {\n openaiKey,\n azureKey: str(azure.key),\n azureRegion: str(azure.region),\n aws: {\n accessKeyId: str(aws.accessKeyId),\n secretAccessKey: str(aws.secretAccessKey),\n region: str(aws.region),\n },\n };\n }\n\n return {\n openaiKey: str(config.voiceApiKey) || openaiFallback,\n azureKey: str(config.azureSpeechKey),\n azureRegion: str(config.azureSpeechRegion),\n aws: {\n accessKeyId: str(config.awsAccessKeyId),\n secretAccessKey: str(config.awsSecretAccessKey),\n region: str(config.awsRegion),\n },\n };\n}\n"]}
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/lib/credentials.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Resolve the LLM API key from the adapter config.\n *\n * Two modes (`config.credentialType`):\n * - 'manual': the key is stored directly in the (encrypted) adapter config (`config.apiKey`).\n * - 'manager': the config only stores the ID of a credential in the central ioBroker\n * credential store (`system.credentials.*`); the real key is resolved at runtime\n * via the Credentials API.\n *\n * The central Credentials API (`Credentials.getCredentials`) is only available with\n * js-controller >= 7.2 / a recent \\@iobroker/adapter-core. We access it defensively so the\n * adapter still compiles and runs (in `manual` mode) on older controllers.\n */\nimport * as Credentials from '@iobroker/adapter-core/credentials';\nimport type { AdapterConfig } from '../types';\nimport type { VoiceCredentials } from './voice/engines';\n\n/** Read and decrypt a single credential's key from the central store; '' (and a warning) on error. */\nexport async function readCentralKey(adapter: ioBroker.Adapter, id: string): Promise<string> {\n if (!id) {\n return '';\n }\n if (!Credentials?.getCredentials) {\n adapter.log.warn(`Cannot read credential \"${id}\": the central Credentials API needs js-controller >= 7.2`);\n return '';\n }\n try {\n const cred = await Credentials.getCredentials(adapter, id);\n return ((cred?.values?.key as string) || '').trim();\n } catch (e) {\n adapter.log.warn(`Cannot read credential \"${id}\": ${(e as Error).message}`);\n return '';\n }\n}\n\n/**\n * Resolve the effective API key for the configured provider.\n * `overrideKey`/`overrideCredentialId` win over the stored config (used by the settings-dialog\n * Test button, where unsaved form values should be used).\n */\nexport async function resolveApiKey(\n adapter: ioBroker.Adapter,\n config: AdapterConfig,\n override?: { credentialType?: 'manual' | 'manager'; apiKey?: string; credentialId?: string },\n): Promise<string> {\n const mode = override?.credentialType || config.credentialType || 'manual';\n if (mode === 'manager') {\n const id = (override?.credentialId || config.credentialIdApiKey || '').trim();\n return readCentralKey(adapter, id);\n }\n return (override?.apiKey || config.apiKey || '').trim();\n}\n\n/** Read and decrypt all fields of a credential from the central store; {} (and a warning) on error. */\nexport async function readCentralValues(adapter: ioBroker.Adapter, id: string): Promise<Record<string, unknown>> {\n if (!id) {\n return {};\n }\n if (!Credentials?.getCredentials) {\n adapter.log.warn(`Cannot read credential \"${id}\": the central Credentials API needs js-controller >= 7.2`);\n return {};\n }\n try {\n const cred = await Credentials.getCredentials(adapter, id);\n return cred?.values || {};\n } catch (e) {\n adapter.log.warn(`Cannot read credential \"${id}\": ${(e as Error).message}`);\n return {};\n }\n}\n\n/** Safe stringify of a credential/config field (values are strings/numbers/booleans). */\nfunction str(v: unknown): string {\n if (typeof v === 'string') {\n return v.trim();\n }\n if (typeof v === 'number' || typeof v === 'boolean') {\n return String(v);\n }\n return '';\n}\n\n/** Vendor and key of the cloud LLM the instance works with (its own, or the one of the system-wide AI settings) */\nexport interface LlmKey {\n provider: string;\n apiKey: string;\n}\n\n/**\n * Resolve the speech (STT/TTS) credentials for OpenAI, OpenRouter, Azure and AWS.\n * - 'manual': the encrypted key/region config fields.\n * - 'manager': the central credential store, keyed by the *CredentialId fields\n * (azure values: { key, region }; aws values: { accessKeyId, secretAccessKey, region }).\n * The key of the cloud LLM (`llm`) is reused for OpenAI or OpenRouter speech when that speech vendor has no key of\n * its own and the LLM is of the same vendor.\n */\nexport async function resolveVoiceCredentials(\n adapter: ioBroker.Adapter,\n config: AdapterConfig,\n llm: LlmKey | null,\n): Promise<VoiceCredentials> {\n const openaiFallback = llm?.provider === 'openai' ? llm.apiKey : '';\n const openrouterFallback = llm?.provider === 'openrouter' ? llm.apiKey : '';\n\n if (config.voiceCredentialType === 'manager') {\n const openaiKey = config.voiceCredentialId\n ? await readCentralKey(adapter, config.voiceCredentialId)\n : openaiFallback;\n const openrouterKey = config.openrouterCredentialId\n ? await readCentralKey(adapter, config.openrouterCredentialId)\n : openrouterFallback;\n const azure = config.azureCredentialId ? await readCentralValues(adapter, config.azureCredentialId) : {};\n const aws = config.awsCredentialId ? await readCentralValues(adapter, config.awsCredentialId) : {};\n return {\n openaiKey,\n openrouterKey,\n azureKey: str(azure.key),\n azureRegion: str(azure.region),\n aws: {\n accessKeyId: str(aws.accessKeyId),\n secretAccessKey: str(aws.secretAccessKey),\n region: str(aws.region),\n },\n };\n }\n\n return {\n openaiKey: str(config.voiceApiKey) || openaiFallback,\n openrouterKey: str(config.openrouterSpeechKey) || openrouterFallback,\n azureKey: str(config.azureSpeechKey),\n azureRegion: str(config.azureSpeechRegion),\n aws: {\n accessKeyId: str(config.awsAccessKeyId),\n secretAccessKey: str(config.awsSecretAccessKey),\n region: str(config.awsRegion),\n },\n };\n}\n"]}
package/build/lib/llm.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.LlmAgent = exports.PROVIDER_PRESETS = void 0;
6
+ exports.LlmAgent = exports.OPENROUTER_HEADERS = exports.PROVIDER_PRESETS = void 0;
7
7
  exports.resolveProvider = resolveProvider;
8
8
  exports.effectiveApiKey = effectiveApiKey;
9
9
  const openai_1 = __importDefault(require("openai"));
@@ -19,8 +19,12 @@ exports.PROVIDER_PRESETS = {
19
19
  defaultModel: 'gemini-2.0-flash',
20
20
  },
21
21
  deepseek: { sdk: 'openai', baseUrl: 'https://api.deepseek.com', defaultModel: 'deepseek-chat' },
22
+ // OpenRouter names a model with its vendor in front
23
+ openrouter: { sdk: 'openai', baseUrl: 'https://openrouter.ai/api/v1', defaultModel: 'openai/gpt-4o-mini' },
22
24
  custom: { sdk: 'openai', baseUrl: '', defaultModel: '' },
23
25
  };
26
+ /** OpenRouter shows the calling app in its statistics (same attribution as `@iobroker/ai-core`) */
27
+ exports.OPENROUTER_HEADERS = { 'HTTP-Referer': 'https://www.iobroker.net', 'X-Title': 'ioBroker' };
24
28
  /** Resolve the effective SDK, base URL and default model for a provider (custom uses `configBaseUrl`). */
25
29
  function resolveProvider(provider, configBaseUrl) {
26
30
  const preset = exports.PROVIDER_PRESETS[provider || 'openai'] || exports.PROVIDER_PRESETS.openai;
@@ -103,7 +107,11 @@ class LlmAgent {
103
107
  this.anthropic = new sdk_1.default({ apiKey: opts.apiKey, baseURL: opts.baseUrl || undefined });
104
108
  }
105
109
  else {
106
- this.openai = new openai_1.default({ apiKey: opts.apiKey, baseURL: opts.baseUrl || undefined });
110
+ this.openai = new openai_1.default({
111
+ apiKey: opts.apiKey,
112
+ baseURL: opts.baseUrl || undefined,
113
+ defaultHeaders: opts.provider === 'openrouter' ? exports.OPENROUTER_HEADERS : undefined,
114
+ });
107
115
  }
108
116
  }
109
117
  /** The model requests go to — empty until {@link ensureModel} has looked it up, if none was configured. */
@@ -302,7 +310,13 @@ class LlmAgent {
302
310
  }
303
311
  else {
304
312
  const client = this.openai;
305
- await client.models.list();
313
+ if (this.providerName === 'openrouter') {
314
+ // its model list is public and accepts any key; /key needs a valid one
315
+ await client.get('/key');
316
+ }
317
+ else {
318
+ await client.models.list();
319
+ }
306
320
  }
307
321
  return { ok: true };
308
322
  }