@intranefr/superbackend 1.4.4 → 1.5.1

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 (195) hide show
  1. package/.env.example +5 -0
  2. package/README.md +11 -0
  3. package/index.js +39 -1
  4. package/package.json +11 -3
  5. package/public/sdk/ui-components.iife.js +191 -0
  6. package/sdk/ui-components/browser/src/index.js +228 -0
  7. package/src/admin/endpointRegistry.js +120 -0
  8. package/src/controllers/admin.controller.js +111 -5
  9. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  10. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  11. package/src/controllers/adminCache.controller.js +342 -0
  12. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  13. package/src/controllers/adminCrons.controller.js +388 -0
  14. package/src/controllers/adminDbBrowser.controller.js +124 -0
  15. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  16. package/src/controllers/adminHeadless.controller.js +91 -2
  17. package/src/controllers/adminHealthChecks.controller.js +570 -0
  18. package/src/controllers/adminI18n.controller.js +51 -29
  19. package/src/controllers/adminLlm.controller.js +126 -2
  20. package/src/controllers/adminPages.controller.js +720 -0
  21. package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
  22. package/src/controllers/adminProxy.controller.js +113 -0
  23. package/src/controllers/adminRateLimits.controller.js +138 -0
  24. package/src/controllers/adminRbac.controller.js +803 -0
  25. package/src/controllers/adminScripts.controller.js +320 -0
  26. package/src/controllers/adminSeoConfig.controller.js +71 -48
  27. package/src/controllers/adminTerminals.controller.js +39 -0
  28. package/src/controllers/adminUiComponents.controller.js +315 -0
  29. package/src/controllers/adminUiComponentsAi.controller.js +34 -0
  30. package/src/controllers/blogAdmin.controller.js +279 -0
  31. package/src/controllers/blogAiAdmin.controller.js +224 -0
  32. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  33. package/src/controllers/blogInternal.controller.js +26 -0
  34. package/src/controllers/blogPublic.controller.js +89 -0
  35. package/src/controllers/fileManager.controller.js +190 -0
  36. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  37. package/src/controllers/healthChecksPublic.controller.js +196 -0
  38. package/src/controllers/metrics.controller.js +64 -4
  39. package/src/controllers/orgAdmin.controller.js +366 -0
  40. package/src/controllers/uiComponentsPublic.controller.js +118 -0
  41. package/src/middleware/auth.js +7 -0
  42. package/src/middleware/internalCronAuth.js +29 -0
  43. package/src/middleware/rbac.js +62 -0
  44. package/src/middleware.js +879 -56
  45. package/src/models/BlockDefinition.js +27 -0
  46. package/src/models/BlogAutomationLock.js +14 -0
  47. package/src/models/BlogAutomationRun.js +39 -0
  48. package/src/models/BlogPost.js +42 -0
  49. package/src/models/CacheEntry.js +26 -0
  50. package/src/models/ConsoleEntry.js +32 -0
  51. package/src/models/ConsoleLog.js +23 -0
  52. package/src/models/ContextBlockDefinition.js +33 -0
  53. package/src/models/CronExecution.js +47 -0
  54. package/src/models/CronJob.js +70 -0
  55. package/src/models/ExternalDbConnection.js +49 -0
  56. package/src/models/FileEntry.js +22 -0
  57. package/src/models/HeadlessModelDefinition.js +10 -0
  58. package/src/models/HealthAutoHealAttempt.js +57 -0
  59. package/src/models/HealthCheck.js +132 -0
  60. package/src/models/HealthCheckRun.js +51 -0
  61. package/src/models/HealthIncident.js +49 -0
  62. package/src/models/Page.js +95 -0
  63. package/src/models/PageCollection.js +42 -0
  64. package/src/models/ProxyEntry.js +66 -0
  65. package/src/models/RateLimitCounter.js +19 -0
  66. package/src/models/RateLimitMetricBucket.js +20 -0
  67. package/src/models/RbacGrant.js +25 -0
  68. package/src/models/RbacGroup.js +16 -0
  69. package/src/models/RbacGroupMember.js +13 -0
  70. package/src/models/RbacGroupRole.js +13 -0
  71. package/src/models/RbacRole.js +25 -0
  72. package/src/models/RbacUserRole.js +13 -0
  73. package/src/models/ScriptDefinition.js +42 -0
  74. package/src/models/ScriptRun.js +22 -0
  75. package/src/models/UiComponent.js +29 -0
  76. package/src/models/UiComponentProject.js +26 -0
  77. package/src/models/UiComponentProjectComponent.js +18 -0
  78. package/src/routes/admin.routes.js +1 -0
  79. package/src/routes/adminBlog.routes.js +21 -0
  80. package/src/routes/adminBlogAi.routes.js +16 -0
  81. package/src/routes/adminBlogAutomation.routes.js +27 -0
  82. package/src/routes/adminCache.routes.js +20 -0
  83. package/src/routes/adminConsoleManager.routes.js +302 -0
  84. package/src/routes/adminCrons.routes.js +25 -0
  85. package/src/routes/adminDbBrowser.routes.js +65 -0
  86. package/src/routes/adminEjsVirtual.routes.js +2 -1
  87. package/src/routes/adminHeadless.routes.js +8 -1
  88. package/src/routes/adminHealthChecks.routes.js +28 -0
  89. package/src/routes/adminI18n.routes.js +4 -3
  90. package/src/routes/adminLlm.routes.js +4 -2
  91. package/src/routes/adminPages.routes.js +55 -0
  92. package/src/routes/adminProxy.routes.js +15 -0
  93. package/src/routes/adminRateLimits.routes.js +17 -0
  94. package/src/routes/adminRbac.routes.js +38 -0
  95. package/src/routes/adminScripts.routes.js +21 -0
  96. package/src/routes/adminSeoConfig.routes.js +5 -4
  97. package/src/routes/adminTerminals.routes.js +13 -0
  98. package/src/routes/adminUiComponents.routes.js +30 -0
  99. package/src/routes/blogInternal.routes.js +14 -0
  100. package/src/routes/blogPublic.routes.js +9 -0
  101. package/src/routes/fileManager.routes.js +62 -0
  102. package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
  103. package/src/routes/healthChecksPublic.routes.js +9 -0
  104. package/src/routes/log.routes.js +43 -60
  105. package/src/routes/metrics.routes.js +4 -2
  106. package/src/routes/orgAdmin.routes.js +6 -0
  107. package/src/routes/pages.routes.js +123 -0
  108. package/src/routes/proxy.routes.js +46 -0
  109. package/src/routes/rbac.routes.js +47 -0
  110. package/src/routes/uiComponentsPublic.routes.js +9 -0
  111. package/src/routes/webhook.routes.js +2 -1
  112. package/src/routes/workflows.routes.js +4 -0
  113. package/src/services/blockDefinitionsAi.service.js +247 -0
  114. package/src/services/blog.service.js +99 -0
  115. package/src/services/blogAutomation.service.js +978 -0
  116. package/src/services/blogCronsBootstrap.service.js +184 -0
  117. package/src/services/blogPublishing.service.js +58 -0
  118. package/src/services/cacheLayer.service.js +696 -0
  119. package/src/services/consoleManager.service.js +700 -0
  120. package/src/services/consoleOverride.service.js +6 -1
  121. package/src/services/cronScheduler.service.js +350 -0
  122. package/src/services/dbBrowser.service.js +536 -0
  123. package/src/services/ejsVirtual.service.js +102 -32
  124. package/src/services/fileManager.service.js +475 -0
  125. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  126. package/src/services/headlessExternalModels.service.js +292 -0
  127. package/src/services/headlessModels.service.js +26 -6
  128. package/src/services/healthChecks.service.js +650 -0
  129. package/src/services/healthChecksBootstrap.service.js +109 -0
  130. package/src/services/healthChecksScheduler.service.js +106 -0
  131. package/src/services/llmDefaults.service.js +190 -0
  132. package/src/services/migrationAssets/s3.js +2 -2
  133. package/src/services/pages.service.js +602 -0
  134. package/src/services/pagesContext.service.js +331 -0
  135. package/src/services/pagesContextBlocksAi.service.js +349 -0
  136. package/src/services/proxy.service.js +535 -0
  137. package/src/services/rateLimiter.service.js +623 -0
  138. package/src/services/rbac.service.js +212 -0
  139. package/src/services/scriptsRunner.service.js +259 -0
  140. package/src/services/terminals.service.js +152 -0
  141. package/src/services/terminalsWs.service.js +100 -0
  142. package/src/services/uiComponentsAi.service.js +299 -0
  143. package/src/services/uiComponentsCrypto.service.js +39 -0
  144. package/src/services/workflow.service.js +23 -8
  145. package/src/utils/orgRoles.js +14 -0
  146. package/src/utils/rbac/engine.js +60 -0
  147. package/src/utils/rbac/rightsRegistry.js +29 -0
  148. package/views/admin-blog-automation.ejs +877 -0
  149. package/views/admin-blog-edit.ejs +542 -0
  150. package/views/admin-blog.ejs +399 -0
  151. package/views/admin-cache.ejs +681 -0
  152. package/views/admin-console-manager.ejs +680 -0
  153. package/views/admin-crons.ejs +645 -0
  154. package/views/admin-db-browser.ejs +445 -0
  155. package/views/admin-ejs-virtual.ejs +16 -10
  156. package/views/admin-file-manager.ejs +942 -0
  157. package/views/admin-headless.ejs +294 -24
  158. package/views/admin-health-checks.ejs +725 -0
  159. package/views/admin-i18n.ejs +59 -5
  160. package/views/admin-llm.ejs +99 -1
  161. package/views/admin-organizations.ejs +528 -10
  162. package/views/admin-pages.ejs +2424 -0
  163. package/views/admin-proxy.ejs +491 -0
  164. package/views/admin-rate-limiter.ejs +625 -0
  165. package/views/admin-rbac.ejs +1331 -0
  166. package/views/admin-scripts.ejs +497 -0
  167. package/views/admin-seo-config.ejs +61 -7
  168. package/views/admin-terminals.ejs +328 -0
  169. package/views/admin-ui-components.ejs +741 -0
  170. package/views/admin-users.ejs +261 -4
  171. package/views/admin-workflows.ejs +7 -7
  172. package/views/file-manager.ejs +866 -0
  173. package/views/pages/blocks/contact.ejs +27 -0
  174. package/views/pages/blocks/cta.ejs +18 -0
  175. package/views/pages/blocks/faq.ejs +20 -0
  176. package/views/pages/blocks/features.ejs +19 -0
  177. package/views/pages/blocks/hero.ejs +13 -0
  178. package/views/pages/blocks/html.ejs +5 -0
  179. package/views/pages/blocks/image.ejs +14 -0
  180. package/views/pages/blocks/testimonials.ejs +26 -0
  181. package/views/pages/blocks/text.ejs +10 -0
  182. package/views/pages/layouts/default.ejs +51 -0
  183. package/views/pages/layouts/minimal.ejs +42 -0
  184. package/views/pages/layouts/sidebar.ejs +54 -0
  185. package/views/pages/partials/footer.ejs +13 -0
  186. package/views/pages/partials/header.ejs +12 -0
  187. package/views/pages/partials/sidebar.ejs +8 -0
  188. package/views/pages/runtime/page.ejs +10 -0
  189. package/views/pages/templates/article.ejs +20 -0
  190. package/views/pages/templates/default.ejs +12 -0
  191. package/views/pages/templates/landing.ejs +14 -0
  192. package/views/pages/templates/listing.ejs +15 -0
  193. package/views/partials/admin-image-upload-modal.ejs +221 -0
  194. package/views/partials/dashboard/nav-items.ejs +14 -0
  195. package/views/partials/llm-provider-model-picker.ejs +183 -0
@@ -13,6 +13,7 @@
13
13
  items: [
14
14
  { id: 'users', label: 'Users', path: adminPath + '/users', icon: 'ti-users' },
15
15
  { id: 'orgs', label: 'Organizations', path: adminPath + '/organizations', icon: 'ti-building-community' },
16
+ { id: 'rbac', label: 'RBAC', path: adminPath + '/rbac', icon: 'ti-lock' },
16
17
  { id: 'notifications', label: 'Notifications', path: adminPath + '/notifications', icon: 'ti-bell' },
17
18
  { id: 'invites', label: 'Waiting List', path: adminPath + '/waiting-list', icon: 'ti-mail-forward' },
18
19
  ]
@@ -25,7 +26,11 @@
25
26
  { id: 'json', label: 'JSON Configs', path: adminPath + '/json-configs', icon: 'ti-braces' },
26
27
  { id: 'seo', label: 'SEO Config', path: adminPath + '/seo-config', icon: 'ti-search' },
27
28
  { id: 'assets', label: 'Assets', path: adminPath + '/assets', icon: 'ti-photo' },
29
+ { id: 'file-manager', label: 'File Manager', path: adminPath + '/file-manager', icon: 'ti-folder' },
30
+ { id: 'ui-components', label: 'UI Components', path: adminPath + '/ui-components', icon: 'ti-components' },
28
31
  { id: 'headless', label: 'Headless CMS', path: adminPath + '/headless', icon: 'ti-table' },
32
+ { id: 'pages', label: 'Pages', path: adminPath + '/pages', icon: 'ti-file-text' },
33
+ { id: 'blog-system', label: 'Blog system', path: adminPath + '/blog', icon: 'ti-news' },
29
34
  ]
30
35
  },
31
36
  {
@@ -34,6 +39,10 @@
34
39
  { id: 'settings', label: 'Global Settings', path: adminPath + '/global-settings', icon: 'ti-settings' },
35
40
  { id: 'flags', label: 'Feature Flags', path: adminPath + '/feature-flags', icon: 'ti-flag' },
36
41
  { id: 'ejs', label: 'Virtual EJS', path: adminPath + '/ejs-virtual', icon: 'ti-code' },
42
+ { id: 'rate-limiter', label: 'Rate Limiter', path: adminPath + '/rate-limiter', icon: 'ti-traffic-cone' },
43
+ { id: 'proxy', label: 'Proxy system', path: adminPath + '/proxy', icon: 'ti-world' },
44
+ { id: 'cache', label: 'Cache Layer', path: adminPath + '/cache', icon: 'ti-database' },
45
+ { id: 'db-browser', label: 'DB Browser', path: adminPath + '/db-browser', icon: 'ti-database-search' },
37
46
  { id: 'migration', label: 'Migration', path: adminPath + '/migration', icon: 'ti-database-export' },
38
47
  { id: 'webhooks', label: 'Webhooks', path: adminPath + '/webhooks', icon: 'ti-webhook' },
39
48
  { id: 'coolify', label: 'Coolify Deploy', path: adminPath + '/coolify-deploy', icon: 'ti-rocket' },
@@ -44,6 +53,8 @@
44
53
  items: [
45
54
  { id: 'audit', label: 'Audit Logs', path: adminPath + '/audit', icon: 'ti-history' },
46
55
  { id: 'errors', label: 'Error Tracking', path: adminPath + '/errors', icon: 'ti-bug' },
56
+ { id: 'console-manager', label: 'Console Manager', path: adminPath + '/console-manager', icon: 'ti-terminal-2' },
57
+ { id: 'health-checks', label: 'Health Checks', path: adminPath + '/health-checks', icon: 'ti-heartbeat' },
47
58
  { id: 'metrics', label: 'Metrics', path: adminPath + '/metrics', icon: 'ti-chart-bar' },
48
59
  { id: 'llm', label: 'LLM/AI', path: adminPath + '/admin-llm', icon: 'ti-brain' },
49
60
  ]
@@ -59,6 +70,9 @@
59
70
  title: 'Automation',
60
71
  items: [
61
72
  { id: 'workflows', label: 'Workflows', path: adminPath + '/workflows/all', icon: 'ti-robot' },
73
+ { id: 'scripts', label: 'Scripts', path: adminPath + '/scripts', icon: 'ti-terminal-2' },
74
+ { id: 'crons', label: 'Crons', path: adminPath + '/crons', icon: 'ti-clock' },
75
+ { id: 'terminals', label: 'Terminals', path: adminPath + '/terminals', icon: 'ti-terminal' },
62
76
  ]
63
77
  }
64
78
  ];
@@ -0,0 +1,183 @@
1
+ <div class="min-w-[200px]">
2
+ <label class="block text-xs font-medium text-gray-600 mb-1"><%= (typeof providerLabel !== 'undefined' && providerLabel) ? providerLabel : 'LLM Provider Key' %></label>
3
+ <input id="<%= providerInputId %>" class="w-full border rounded px-2 py-2 text-sm" placeholder="e.g. openrouter" list="<%= providerInputId %>__datalist" />
4
+ <datalist id="<%= providerInputId %>__datalist"></datalist>
5
+ </div>
6
+
7
+ <div class="min-w-[260px]">
8
+ <div class="flex flex-wrap items-center justify-between gap-2 mb-1">
9
+ <label class="block text-xs font-medium text-gray-600"><%= (typeof modelLabel !== 'undefined' && modelLabel) ? modelLabel : 'Model' %></label>
10
+ <% if ((typeof showOpenRouterFetch === 'undefined') ? true : (showOpenRouterFetch !== false)) { %>
11
+ <button
12
+ type="button"
13
+ class="px-2 py-1 bg-gray-200 text-gray-800 rounded text-[11px] whitespace-nowrap"
14
+ onclick="window.__llmProviderModelPicker && window.__llmProviderModelPicker.fetchOpenRouterModels && window.__llmProviderModelPicker.fetchOpenRouterModels({ providerInputId: '<%= providerInputId %>', modelInputId: '<%= modelInputId %>' })"
15
+ >
16
+ Fetch OpenRouter models
17
+ </button>
18
+ <% } %>
19
+ </div>
20
+ <input id="<%= modelInputId %>" class="w-full border rounded px-2 py-2 text-sm" placeholder="e.g. google/gemini-2.5-flash-lite" list="<%= modelInputId %>__datalist" />
21
+ <datalist id="<%= modelInputId %>__datalist"></datalist>
22
+ </div>
23
+
24
+ <script>
25
+ (function () {
26
+ if (!window.__llmProviderModelPicker) {
27
+ window.__llmProviderModelPicker = { instances: {} };
28
+ }
29
+
30
+ function safeJsonParse(raw, fallback) {
31
+ try {
32
+ return JSON.parse(raw);
33
+ } catch (_) {
34
+ return fallback;
35
+ }
36
+ }
37
+
38
+ async function fetchJson(url) {
39
+ const res = await fetch(url);
40
+ const data = await res.json();
41
+ if (!res.ok) {
42
+ throw new Error(data?.error || 'Request failed');
43
+ }
44
+ return data;
45
+ }
46
+
47
+ function setDatalistOptions(datalistEl, items) {
48
+ datalistEl.innerHTML = '';
49
+ const uniq = Array.from(new Set((items || []).filter(Boolean)));
50
+ for (const item of uniq) {
51
+ const opt = document.createElement('option');
52
+ opt.value = String(item);
53
+ datalistEl.appendChild(opt);
54
+ }
55
+ }
56
+
57
+ function trim(v) {
58
+ return String(v || '').trim();
59
+ }
60
+
61
+ function isOpenRouterProvider({ providerKey, providerConfig }) {
62
+ const pk = String(providerKey || '').trim().toLowerCase();
63
+ if (pk === 'openrouter') return true;
64
+
65
+ const baseUrl = providerConfig && typeof providerConfig === 'object'
66
+ ? String(providerConfig.baseUrl || providerConfig.baseURL || '').trim().toLowerCase()
67
+ : '';
68
+
69
+ return Boolean(baseUrl && baseUrl.includes('openrouter'));
70
+ }
71
+
72
+ function getInstanceKey({ providerInputId, modelInputId }) {
73
+ return `${String(providerInputId || '').trim()}::${String(modelInputId || '').trim()}`;
74
+ }
75
+
76
+ function getOrCreateInstance(opts) {
77
+ const key = getInstanceKey(opts);
78
+ const existing = window.__llmProviderModelPicker.instances[key];
79
+ if (existing) return existing;
80
+
81
+ const inst = {
82
+ apiBase: opts.apiBase,
83
+ providerInputId: opts.providerInputId,
84
+ modelInputId: opts.modelInputId,
85
+ providers: {},
86
+ providerModels: {},
87
+ };
88
+
89
+ window.__llmProviderModelPicker.instances[key] = inst;
90
+ return inst;
91
+ }
92
+
93
+ async function loadConfig(inst) {
94
+ const data = await fetchJson(`${inst.apiBase}/api/admin/llm/config`);
95
+ inst.providers = data.providers || {};
96
+ inst.providerModels = data.providerModels || {};
97
+ return data;
98
+ }
99
+
100
+ function renderProviderOptions(inst) {
101
+ const providerInput = document.getElementById(inst.providerInputId);
102
+ const providerList = document.getElementById(`${inst.providerInputId}__datalist`);
103
+ if (!providerInput || !providerList) return;
104
+
105
+ const providerKeys = Object.keys(inst.providers || {}).sort();
106
+ setDatalistOptions(providerList, providerKeys);
107
+ }
108
+
109
+ function renderModelOptions(inst) {
110
+ const providerInput = document.getElementById(inst.providerInputId);
111
+ const modelList = document.getElementById(`${inst.modelInputId}__datalist`);
112
+ if (!providerInput || !modelList) return;
113
+
114
+ const providerKey = trim(providerInput.value);
115
+ const models = providerKey && inst.providerModels && typeof inst.providerModels === 'object'
116
+ ? inst.providerModels[providerKey]
117
+ : null;
118
+
119
+ setDatalistOptions(modelList, Array.isArray(models) ? models : []);
120
+ }
121
+
122
+ async function maybeAutoFetchOpenRouterModels(inst) {
123
+ try {
124
+ const providerInput = document.getElementById(inst.providerInputId);
125
+ if (!providerInput) return;
126
+
127
+ const providerKey = trim(providerInput.value);
128
+ const providerConfig = inst.providers && typeof inst.providers === 'object' ? inst.providers[providerKey] : null;
129
+ if (!isOpenRouterProvider({ providerKey, providerConfig })) return;
130
+
131
+ const existing = inst.providerModels && typeof inst.providerModels === 'object' ? inst.providerModels.openrouter : null;
132
+ if (Array.isArray(existing) && existing.length > 0) return;
133
+
134
+ await fetchOpenRouterModels({
135
+ apiBase: inst.apiBase,
136
+ providerInputId: inst.providerInputId,
137
+ modelInputId: inst.modelInputId,
138
+ });
139
+ } catch {
140
+ // ignore
141
+ }
142
+ }
143
+
144
+ async function fetchOpenRouterModels(opts) {
145
+ const inst = getOrCreateInstance(opts || {});
146
+ inst.apiBase = (opts && opts.apiBase) || inst.apiBase || window.__llmProviderModelPicker.defaultApiBase || null;
147
+ if (!inst.apiBase) return;
148
+
149
+ const data = await fetchJson(`${inst.apiBase}/api/admin/llm/openrouter/models`);
150
+ const models = Array.isArray(data?.models) ? data.models : [];
151
+
152
+ inst.providerModels = inst.providerModels && typeof inst.providerModels === 'object' ? inst.providerModels : {};
153
+ inst.providerModels.openrouter = models;
154
+ renderModelOptions(inst);
155
+ }
156
+
157
+ async function init(opts) {
158
+ const inst = getOrCreateInstance(opts || {});
159
+
160
+ if (opts && opts.apiBase) {
161
+ window.__llmProviderModelPicker.defaultApiBase = opts.apiBase;
162
+ }
163
+
164
+ await loadConfig(inst);
165
+ renderProviderOptions(inst);
166
+ renderModelOptions(inst);
167
+ await maybeAutoFetchOpenRouterModels(inst);
168
+
169
+ const providerInput = document.getElementById(inst.providerInputId);
170
+ if (providerInput) {
171
+ providerInput.addEventListener('change', async () => {
172
+ renderModelOptions(inst);
173
+ await maybeAutoFetchOpenRouterModels(inst);
174
+ });
175
+ providerInput.addEventListener('input', () => renderModelOptions(inst));
176
+ }
177
+ }
178
+
179
+ window.__llmProviderModelPicker.init = init;
180
+ window.__llmProviderModelPicker.fetchOpenRouterModels = fetchOpenRouterModels;
181
+ window.__llmProviderModelPicker._util = { safeJsonParse };
182
+ })();
183
+ </script>