aicodeman 1.43.0 → 1.45.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.
- package/README.md +9 -4
- package/dist/case-rename.d.ts +65 -0
- package/dist/case-rename.d.ts.map +1 -0
- package/dist/case-rename.js +198 -0
- package/dist/case-rename.js.map +1 -0
- package/dist/cli-agent.d.ts +63 -4
- package/dist/cli-agent.d.ts.map +1 -1
- package/dist/cli-agent.js +233 -19
- package/dist/cli-agent.js.map +1 -1
- package/dist/config/auth-config.d.ts +16 -1
- package/dist/config/auth-config.d.ts.map +1 -1
- package/dist/config/auth-config.js +16 -1
- package/dist/config/auth-config.js.map +1 -1
- package/dist/config/cli-registry/stock.d.ts.map +1 -1
- package/dist/config/cli-registry/stock.js +8 -3
- package/dist/config/cli-registry/stock.js.map +1 -1
- package/dist/config/cli-registry/types.d.ts +2 -2
- package/dist/config/map-limits.d.ts +13 -1
- package/dist/config/map-limits.d.ts.map +1 -1
- package/dist/config/map-limits.js +28 -1
- package/dist/config/map-limits.js.map +1 -1
- package/dist/config-backup.d.ts +279 -0
- package/dist/config-backup.d.ts.map +1 -0
- package/dist/config-backup.js +794 -0
- package/dist/config-backup.js.map +1 -0
- package/dist/cron/cron-service.d.ts +5 -5
- package/dist/cron/cron-service.js +6 -6
- package/dist/cron/cron-service.js.map +1 -1
- package/dist/custom-model-hosts.d.ts +16 -0
- package/dist/custom-model-hosts.d.ts.map +1 -1
- package/dist/custom-model-hosts.js.map +1 -1
- package/dist/hooks-config.d.ts +15 -0
- package/dist/hooks-config.d.ts.map +1 -1
- package/dist/hooks-config.js +35 -0
- package/dist/hooks-config.js.map +1 -1
- package/dist/host-sensors.d.ts +115 -0
- package/dist/host-sensors.d.ts.map +1 -0
- package/dist/host-sensors.js +457 -0
- package/dist/host-sensors.js.map +1 -0
- package/dist/intent-store.d.ts +2 -0
- package/dist/intent-store.d.ts.map +1 -1
- package/dist/intent-store.js +9 -0
- package/dist/intent-store.js.map +1 -1
- package/dist/mux-interface.d.ts +21 -4
- package/dist/mux-interface.d.ts.map +1 -1
- package/dist/prompt-cache-keepwarm.d.ts +193 -0
- package/dist/prompt-cache-keepwarm.d.ts.map +1 -0
- package/dist/prompt-cache-keepwarm.js +359 -0
- package/dist/prompt-cache-keepwarm.js.map +1 -0
- package/dist/remote-hosts.d.ts +7 -0
- package/dist/remote-hosts.d.ts.map +1 -1
- package/dist/remote-hosts.js +17 -3
- package/dist/remote-hosts.js.map +1 -1
- package/dist/remote-pane-history.d.ts.map +1 -1
- package/dist/remote-pane-history.js +2 -2
- package/dist/remote-pane-history.js.map +1 -1
- package/dist/respawn-controller.d.ts.map +1 -1
- package/dist/respawn-controller.js +6 -5
- package/dist/respawn-controller.js.map +1 -1
- package/dist/session-auto-ops.d.ts.map +1 -1
- package/dist/session-auto-ops.js +6 -1
- package/dist/session-auto-ops.js.map +1 -1
- package/dist/session-cli-registry-bridge.d.ts +13 -0
- package/dist/session-cli-registry-bridge.d.ts.map +1 -1
- package/dist/session-cli-registry-bridge.js +20 -3
- package/dist/session-cli-registry-bridge.js.map +1 -1
- package/dist/session-submit-verifier.d.ts +19 -0
- package/dist/session-submit-verifier.d.ts.map +1 -1
- package/dist/session-submit-verifier.js +88 -0
- package/dist/session-submit-verifier.js.map +1 -1
- package/dist/session.d.ts +61 -5
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +125 -7
- package/dist/session.js.map +1 -1
- package/dist/state-store.d.ts +10 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +35 -0
- package/dist/state-store.js.map +1 -1
- package/dist/tab-layout-service.d.ts +33 -0
- package/dist/tab-layout-service.d.ts.map +1 -1
- package/dist/tab-layout-service.js +75 -3
- package/dist/tab-layout-service.js.map +1 -1
- package/dist/tmux-manager.d.ts +57 -11
- package/dist/tmux-manager.d.ts.map +1 -1
- package/dist/tmux-manager.js +190 -42
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-prefix-table.d.ts +86 -0
- package/dist/tmux-prefix-table.d.ts.map +1 -0
- package/dist/tmux-prefix-table.js +172 -0
- package/dist/tmux-prefix-table.js.map +1 -0
- package/dist/tui/tui-app.d.ts +17 -2
- package/dist/tui/tui-app.d.ts.map +1 -1
- package/dist/tui/tui-app.js +70 -20
- package/dist/tui/tui-app.js.map +1 -1
- package/dist/tui/tui-client.d.ts +93 -5
- package/dist/tui/tui-client.d.ts.map +1 -1
- package/dist/tui/tui-client.js +198 -28
- package/dist/tui/tui-client.js.map +1 -1
- package/dist/types/app-state.d.ts +6 -0
- package/dist/types/app-state.d.ts.map +1 -1
- package/dist/types/app-state.js.map +1 -1
- package/dist/types/session.d.ts +40 -1
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/dist/usage-telemetry.d.ts +15 -0
- package/dist/usage-telemetry.d.ts.map +1 -1
- package/dist/usage-telemetry.js +45 -2
- package/dist/usage-telemetry.js.map +1 -1
- package/dist/utils/copilot-plan-usage.d.ts +46 -0
- package/dist/utils/copilot-plan-usage.d.ts.map +1 -0
- package/dist/utils/copilot-plan-usage.js +95 -0
- package/dist/utils/copilot-plan-usage.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/single-line.d.ts +22 -0
- package/dist/utils/single-line.d.ts.map +1 -0
- package/dist/utils/single-line.js +30 -0
- package/dist/utils/single-line.js.map +1 -0
- package/dist/web/agent-inbox-persistence.d.ts +52 -0
- package/dist/web/agent-inbox-persistence.d.ts.map +1 -0
- package/dist/web/agent-inbox-persistence.js +139 -0
- package/dist/web/agent-inbox-persistence.js.map +1 -0
- package/dist/web/agent-inbox.d.ts +140 -0
- package/dist/web/agent-inbox.d.ts.map +1 -0
- package/dist/web/agent-inbox.js +258 -0
- package/dist/web/agent-inbox.js.map +1 -0
- package/dist/web/approval-inbox.d.ts +6 -0
- package/dist/web/approval-inbox.d.ts.map +1 -1
- package/dist/web/approval-inbox.js +2 -2
- package/dist/web/approval-inbox.js.map +1 -1
- package/dist/web/middleware/auth.d.ts +2 -0
- package/dist/web/middleware/auth.d.ts.map +1 -1
- package/dist/web/middleware/auth.js +145 -52
- package/dist/web/middleware/auth.js.map +1 -1
- package/dist/web/plan-usage-latest.d.ts +2 -1
- package/dist/web/plan-usage-latest.d.ts.map +1 -1
- package/dist/web/plan-usage-latest.js +12 -2
- package/dist/web/plan-usage-latest.js.map +1 -1
- package/dist/web/ports/index.d.ts +1 -0
- package/dist/web/ports/index.d.ts.map +1 -1
- package/dist/web/ports/keep-warm-port.d.ts +9 -0
- package/dist/web/ports/keep-warm-port.d.ts.map +1 -0
- package/dist/web/ports/keep-warm-port.js +6 -0
- package/dist/web/ports/keep-warm-port.js.map +1 -0
- package/dist/web/ports/session-port.d.ts +3 -1
- package/dist/web/ports/session-port.d.ts.map +1 -1
- package/dist/web/public/admin-ui.js.gz +0 -0
- package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
- package/dist/web/public/app.42d0abae.js +43 -0
- package/dist/web/public/app.42d0abae.js.br +0 -0
- package/dist/web/public/app.42d0abae.js.gz +0 -0
- package/dist/web/public/approvals-ui.js.gz +0 -0
- package/dist/web/public/{constants.e5e588db.js → constants.0b3cdde9.js} +60 -1
- package/dist/web/public/constants.0b3cdde9.js.br +0 -0
- package/dist/web/public/constants.0b3cdde9.js.gz +0 -0
- package/dist/web/public/cron-ui.js.gz +0 -0
- package/dist/web/public/entrance-animations.js.gz +0 -0
- package/dist/web/public/git-status-ui.js.gz +0 -0
- package/dist/web/public/header-divider.eb2d2463.js +1 -0
- package/dist/web/public/header-divider.eb2d2463.js.br +0 -0
- package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
- package/dist/web/public/home-sessions.js.gz +0 -0
- package/dist/web/public/host-wake-ui.js.gz +0 -0
- package/dist/web/public/i18n.1e68220d.js +2 -0
- package/dist/web/public/i18n.1e68220d.js.br +0 -0
- package/dist/web/public/i18n.1e68220d.js.gz +0 -0
- package/dist/web/public/image-input.941386e7.js.gz +0 -0
- package/dist/web/public/index.html +162 -14
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
- package/dist/web/public/{keyboard-accessory.c283e827.js → keyboard-accessory.4e8b20ac.js} +23 -5
- package/dist/web/public/keyboard-accessory.4e8b20ac.js.br +0 -0
- package/dist/web/public/keyboard-accessory.4e8b20ac.js.gz +0 -0
- package/dist/web/public/{mobile-handlers.0ef9c311.js → mobile-handlers.1c0cbcd8.js} +50 -16
- package/dist/web/public/mobile-handlers.1c0cbcd8.js.br +0 -0
- package/dist/web/public/mobile-handlers.1c0cbcd8.js.gz +0 -0
- package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
- package/dist/web/public/mobile-overview.js.gz +0 -0
- package/dist/web/public/mobile.12fb18de.css +1 -0
- package/dist/web/public/mobile.12fb18de.css.br +0 -0
- package/dist/web/public/mobile.12fb18de.css.gz +0 -0
- package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
- package/dist/web/public/orchestrator-panel.js.gz +0 -0
- package/dist/web/public/{panels-ui.b7bfb9da.js → panels-ui.78c3defb.js} +19 -13
- package/dist/web/public/panels-ui.78c3defb.js.br +0 -0
- package/dist/web/public/panels-ui.78c3defb.js.gz +0 -0
- package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
- package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
- package/dist/web/public/readmymind-ui.js.gz +0 -0
- package/dist/web/public/reboot-restore-ui.js.gz +0 -0
- package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
- package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
- package/dist/web/public/session-lineage.js.gz +0 -0
- package/dist/web/public/{session-ui.be076c88.js → session-ui.ea6b1994.js} +33 -27
- package/dist/web/public/session-ui.ea6b1994.js.br +0 -0
- package/dist/web/public/session-ui.ea6b1994.js.gz +0 -0
- package/dist/web/public/settings-ui.085dd202.js +106 -0
- package/dist/web/public/settings-ui.085dd202.js.br +0 -0
- package/dist/web/public/settings-ui.085dd202.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
- package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
- package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
- package/dist/web/public/styles.3b59b41d.css +1 -0
- package/dist/web/public/styles.3b59b41d.css.br +0 -0
- package/dist/web/public/styles.3b59b41d.css.gz +0 -0
- package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
- package/dist/web/public/sw.js +2 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
- package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
- package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
- package/dist/web/public/terminal-split.js.gz +0 -0
- package/dist/web/public/terminal-tile.js +49 -23
- package/dist/web/public/terminal-tile.js.br +0 -0
- package/dist/web/public/terminal-tile.js.gz +0 -0
- package/dist/web/public/terminal-ui.fb2a27f5.js +2 -0
- package/dist/web/public/terminal-ui.fb2a27f5.js.br +0 -0
- package/dist/web/public/terminal-ui.fb2a27f5.js.gz +0 -0
- package/dist/web/public/tile-grid.js.gz +0 -0
- package/dist/web/public/ultracode-panel.js.gz +0 -0
- package/dist/web/public/ultracode-windows.js.gz +0 -0
- package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
- package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
- package/dist/web/public/vendor/fflate.min.js.gz +0 -0
- package/dist/web/public/vendor/marked.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
- package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
- package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
- package/dist/web/public/vendor/xterm.css.gz +0 -0
- package/dist/web/public/vendor/xterm.min.js.gz +0 -0
- package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
- package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
- package/dist/web/public/webview-tabs.js.gz +0 -0
- package/dist/web/public/whats-new.json +9 -9
- package/dist/web/route-helpers.d.ts +57 -6
- package/dist/web/route-helpers.d.ts.map +1 -1
- package/dist/web/route-helpers.js +72 -4
- package/dist/web/route-helpers.js.map +1 -1
- package/dist/web/routes/admin-routes.d.ts +2 -1
- package/dist/web/routes/admin-routes.d.ts.map +1 -1
- package/dist/web/routes/admin-routes.js +3 -0
- package/dist/web/routes/admin-routes.js.map +1 -1
- package/dist/web/routes/case-routes.d.ts +2 -2
- package/dist/web/routes/case-routes.d.ts.map +1 -1
- package/dist/web/routes/case-routes.js +263 -4
- package/dist/web/routes/case-routes.js.map +1 -1
- package/dist/web/routes/config-backup-routes.d.ts +54 -0
- package/dist/web/routes/config-backup-routes.d.ts.map +1 -0
- package/dist/web/routes/config-backup-routes.js +268 -0
- package/dist/web/routes/config-backup-routes.js.map +1 -0
- package/dist/web/routes/custom-model-routes.d.ts +17 -1
- package/dist/web/routes/custom-model-routes.d.ts.map +1 -1
- package/dist/web/routes/custom-model-routes.js +118 -13
- package/dist/web/routes/custom-model-routes.js.map +1 -1
- package/dist/web/routes/hook-event-routes.d.ts.map +1 -1
- package/dist/web/routes/hook-event-routes.js +26 -5
- package/dist/web/routes/hook-event-routes.js.map +1 -1
- package/dist/web/routes/inbox-routes.d.ts +43 -0
- package/dist/web/routes/inbox-routes.d.ts.map +1 -0
- package/dist/web/routes/inbox-routes.js +148 -0
- package/dist/web/routes/inbox-routes.js.map +1 -0
- package/dist/web/routes/index.d.ts +2 -0
- package/dist/web/routes/index.d.ts.map +1 -1
- package/dist/web/routes/index.js +2 -0
- package/dist/web/routes/index.js.map +1 -1
- package/dist/web/routes/ralph-routes.js +2 -2
- package/dist/web/routes/ralph-routes.js.map +1 -1
- package/dist/web/routes/reboot-restore-routes.d.ts.map +1 -1
- package/dist/web/routes/reboot-restore-routes.js +2 -0
- package/dist/web/routes/reboot-restore-routes.js.map +1 -1
- package/dist/web/routes/session-routes.d.ts +2 -2
- package/dist/web/routes/session-routes.d.ts.map +1 -1
- package/dist/web/routes/session-routes.js +110 -61
- package/dist/web/routes/session-routes.js.map +1 -1
- package/dist/web/routes/status-telemetry-routes.d.ts +2 -2
- package/dist/web/routes/status-telemetry-routes.d.ts.map +1 -1
- package/dist/web/routes/status-telemetry-routes.js +8 -4
- package/dist/web/routes/status-telemetry-routes.js.map +1 -1
- package/dist/web/routes/system-routes.d.ts +2 -2
- package/dist/web/routes/system-routes.d.ts.map +1 -1
- package/dist/web/routes/system-routes.js +30 -2
- package/dist/web/routes/system-routes.js.map +1 -1
- package/dist/web/routes/webview-routes.d.ts.map +1 -1
- package/dist/web/routes/webview-routes.js +1 -0
- package/dist/web/routes/webview-routes.js.map +1 -1
- package/dist/web/schemas.d.ts +45 -0
- package/dist/web/schemas.d.ts.map +1 -1
- package/dist/web/schemas.js +79 -2
- package/dist/web/schemas.js.map +1 -1
- package/dist/web/sensitive-path.d.ts.map +1 -1
- package/dist/web/sensitive-path.js +2 -0
- package/dist/web/sensitive-path.js.map +1 -1
- package/dist/web/server.d.ts +14 -2
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +176 -7
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-listener-wiring.d.ts +6 -0
- package/dist/web/session-listener-wiring.d.ts.map +1 -1
- package/dist/web/session-listener-wiring.js +10 -3
- package/dist/web/session-listener-wiring.js.map +1 -1
- package/dist/web/session-wait-registry.d.ts +48 -4
- package/dist/web/session-wait-registry.d.ts.map +1 -1
- package/dist/web/session-wait-registry.js +144 -6
- package/dist/web/session-wait-registry.js.map +1 -1
- package/dist/web/sse-events.d.ts +9 -3
- package/dist/web/sse-events.d.ts.map +1 -1
- package/dist/web/sse-events.js +9 -3
- package/dist/web/sse-events.js.map +1 -1
- package/dist/web/webview-proxy.d.ts +8 -3
- package/dist/web/webview-proxy.d.ts.map +1 -1
- package/dist/web/webview-proxy.js +23 -5
- package/dist/web/webview-proxy.js.map +1 -1
- package/package.json +1 -1
- package/skills/codeman/SKILL.md +27 -13
- package/skills/codeman/preamble.sh +15 -5
- package/skills/codeman/reference/endpoints.md +32 -10
- package/skills/codeman/reference/recipes.md +1 -1
- package/skills/codeman/reference/verbs.md +68 -13
- package/dist/web/public/app.ae50029a.js +0 -43
- package/dist/web/public/app.ae50029a.js.br +0 -0
- package/dist/web/public/app.ae50029a.js.gz +0 -0
- package/dist/web/public/constants.e5e588db.js.br +0 -0
- package/dist/web/public/constants.e5e588db.js.gz +0 -0
- package/dist/web/public/i18n.8f291f06.js +0 -2
- package/dist/web/public/i18n.8f291f06.js.br +0 -0
- package/dist/web/public/i18n.8f291f06.js.gz +0 -0
- package/dist/web/public/keyboard-accessory.c283e827.js.br +0 -0
- package/dist/web/public/keyboard-accessory.c283e827.js.gz +0 -0
- package/dist/web/public/mobile-handlers.0ef9c311.js.br +0 -0
- package/dist/web/public/mobile-handlers.0ef9c311.js.gz +0 -0
- package/dist/web/public/mobile.13514863.css +0 -1
- package/dist/web/public/mobile.13514863.css.br +0 -0
- package/dist/web/public/mobile.13514863.css.gz +0 -0
- package/dist/web/public/panels-ui.b7bfb9da.js.br +0 -0
- package/dist/web/public/panels-ui.b7bfb9da.js.gz +0 -0
- package/dist/web/public/session-ui.be076c88.js.br +0 -0
- package/dist/web/public/session-ui.be076c88.js.gz +0 -0
- package/dist/web/public/settings-ui.fc4cc50a.js +0 -97
- package/dist/web/public/settings-ui.fc4cc50a.js.br +0 -0
- package/dist/web/public/settings-ui.fc4cc50a.js.gz +0 -0
- package/dist/web/public/styles.dcfa907f.css +0 -1
- package/dist/web/public/styles.dcfa907f.css.br +0 -0
- package/dist/web/public/styles.dcfa907f.css.gz +0 -0
- package/dist/web/public/terminal-ui.ae01a7d1.js +0 -2
- package/dist/web/public/terminal-ui.ae01a7d1.js.br +0 -0
- package/dist/web/public/terminal-ui.ae01a7d1.js.gz +0 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Config backups (src/config-backup.ts).
|
|
3
|
+
*
|
|
4
|
+
* GET /api/config-backups — settings in effect, the folder, and the snapshots in it.
|
|
5
|
+
* POST /api/config-backups — snapshot now (a manual request always makes one).
|
|
6
|
+
* POST /api/config-backups/:id/restore — put a snapshot's files back (after a `pre-restore` snapshot).
|
|
7
|
+
* GET /api/config-backups/diff?from=&to= — what differs between two snapshots, or a snapshot and the
|
|
8
|
+
* running config (`current`, the default for `to`).
|
|
9
|
+
*
|
|
10
|
+
* The files include credentials (`.env`, `users.json`, endpoint keys), so in multi-user mode all four
|
|
11
|
+
* are admin only. Responses carry file NAMES and sizes, never file content, except the diff, which shows
|
|
12
|
+
* changed values but never a credential's (`.env`, `users.json`, `push-keys.json`, `webhook.json`, `intents.json` and any key that looks
|
|
13
|
+
* like a token/secret/password are reported as "changed" only). Restore replaces files the running server
|
|
14
|
+
* may have cached: `restartRequired` says which ones only apply after a restart. A restore that could not
|
|
15
|
+
* write every file answers 500 with the error envelope plus `data` (`restored`, `failed`, `untouched`,
|
|
16
|
+
* `safetySnapshot`, `restartRequired`): the files it wrote stay restored and are reloaded like a full one.
|
|
17
|
+
*/
|
|
18
|
+
import { readFile } from 'node:fs/promises';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import { ApiErrorCode, createErrorResponse, getErrorMessage } from '../../types.js';
|
|
21
|
+
import { getAuthUser, isAdmin, readJsonConfig, SETTINGS_PATH } from '../route-helpers.js';
|
|
22
|
+
import { appendAdminAudit } from '../admin-audit.js';
|
|
23
|
+
import { reloadCliRegistry } from '../../config/cli-registry/registry.js';
|
|
24
|
+
import { isMultiUserMode } from '../../config/multiuser.js';
|
|
25
|
+
import { getDataDir } from '../../config/instance.js';
|
|
26
|
+
import { intentStore } from '../../intent-store.js';
|
|
27
|
+
import { describeUnknownPath, probePathKind } from '../../utils/bounded-path-probe.js';
|
|
28
|
+
import { dirname } from 'node:path';
|
|
29
|
+
import { CONFIG_BACKUP_FILES, CONFIG_DIFF_CURRENT, ConfigBackupCorruptError, ConfigBackupNotFoundError, ConfigBackupPartialRestoreError, defaultConfigBackupDir, diffConfigBackups, listConfigBackups, readConfigBackupSettings, resolveConfigBackupDir, restoreConfigBackup, runConfigBackupPass, withRecordedRetention, readConfigBackupOrigin, } from '../../config-backup.js';
|
|
30
|
+
// Only recorded in each manifest; a missing package.json just leaves it blank.
|
|
31
|
+
const APP_VERSION = (() => {
|
|
32
|
+
try {
|
|
33
|
+
return createRequire(import.meta.url)('../../../package.json').version ?? '';
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
/** Files whose running-process copy only reloads on restart (the rest are read per request; `users.json`
|
|
40
|
+
* is re-read within a second, `clis.json` is reloaded below, `intents.json` by `reload()`). */
|
|
41
|
+
const RESTART_REQUIRED = new Set(['.env', 'push-keys.json', 'settings.json']);
|
|
42
|
+
/**
|
|
43
|
+
* The settings in effect and the folder they name. A custom folder is probed (bounded, #516) BEFORE
|
|
44
|
+
* the recorded-retention fallback reads it, so this throws `BackupDirUnavailableError` for a folder
|
|
45
|
+
* that does not answer and never lists it; `present` is false for a custom folder that does not exist yet.
|
|
46
|
+
*/
|
|
47
|
+
export async function readEffectiveBackupSettings() {
|
|
48
|
+
const raw = await readJsonConfig(SETTINGS_PATH, 'settings.json', {});
|
|
49
|
+
const base = readConfigBackupSettings(raw);
|
|
50
|
+
const dir = resolveConfigBackupDir(base, getDataDir());
|
|
51
|
+
const present = await probeBackupDir(base, dir);
|
|
52
|
+
// The same fallback the timer pass uses, so the form never shows 20 / 30 while a pass keeps 100 / 0.
|
|
53
|
+
const settings = present ? withRecordedRetention(base, raw, dir, readConfigBackupOrigin(getDataDir())) : base;
|
|
54
|
+
return { settings, dir, present };
|
|
55
|
+
}
|
|
56
|
+
/** A user-chosen backup folder that is not safe to touch (bounded probe, as for case paths, #516). */
|
|
57
|
+
export class BackupDirUnavailableError extends Error {
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Probe a custom backup folder without blocking the event loop on a dead mount. The default folder
|
|
61
|
+
* needs no probe. Returns whether the folder exists; throws when it did not answer, or is not a
|
|
62
|
+
* folder. A missing custom folder is only creatable when its parent is present.
|
|
63
|
+
*/
|
|
64
|
+
export async function probeBackupDir(settings, dir) {
|
|
65
|
+
if (!settings.dir || dir === defaultConfigBackupDir(getDataDir()))
|
|
66
|
+
return true;
|
|
67
|
+
const kind = await probePathKind(dir, { pastCap: true });
|
|
68
|
+
if (kind === 'unknown')
|
|
69
|
+
throw new BackupDirUnavailableError(describeUnknownPath('Backup folder', dir, { pastCap: true }));
|
|
70
|
+
if (kind === 'file')
|
|
71
|
+
throw new BackupDirUnavailableError(`Backup folder is a file, not a folder: ${dir}`);
|
|
72
|
+
if (kind === 'directory')
|
|
73
|
+
return true;
|
|
74
|
+
const parent = await probePathKind(dirname(dir), { pastCap: true });
|
|
75
|
+
if (parent === 'unknown') {
|
|
76
|
+
throw new BackupDirUnavailableError(describeUnknownPath('Backup folder', dirname(dir), { pastCap: true }));
|
|
77
|
+
}
|
|
78
|
+
if (parent !== 'directory')
|
|
79
|
+
throw new BackupDirUnavailableError(`Backup folder's parent is missing: ${dirname(dir)}`);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
async function readSettingsForBackup() {
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(await readFile(SETTINGS_PATH, 'utf8'));
|
|
85
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
86
|
+
? parsed
|
|
87
|
+
: 'unreadable';
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
return err.code === 'ENOENT' ? {} : 'unreadable';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Snapshot (when changed) and apply retention; shared by the timer and the manual route. */
|
|
94
|
+
export async function runConfigBackup(reason, opts = {}) {
|
|
95
|
+
// Read settings.json directly: readJsonConfig answers {} for ANY failure, so a syntax error or a read
|
|
96
|
+
// that lands inside PUT /api/settings's non-atomic write used to prune with the defaults (20 / 30
|
|
97
|
+
// days) and drop the longer history the user asked to keep.
|
|
98
|
+
const raw = await readSettingsForBackup();
|
|
99
|
+
const unreadable = raw === 'unreadable';
|
|
100
|
+
const fromFile = readConfigBackupSettings(unreadable ? {} : raw);
|
|
101
|
+
if (!fromFile.enabled && !opts.force)
|
|
102
|
+
return null;
|
|
103
|
+
const dir = resolveConfigBackupDir(fromFile, getDataDir());
|
|
104
|
+
await probeBackupDir(fromFile, dir);
|
|
105
|
+
return runConfigBackupPass({
|
|
106
|
+
dataDir: getDataDir(),
|
|
107
|
+
backupDir: dir,
|
|
108
|
+
reason,
|
|
109
|
+
appVersion: APP_VERSION,
|
|
110
|
+
force: opts.force,
|
|
111
|
+
settings: fromFile,
|
|
112
|
+
raw: unreadable ? {} : raw,
|
|
113
|
+
// An unreadable settings.json still gets its snapshot, but no pass deletes anything on its say-so.
|
|
114
|
+
prune: !unreadable,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
export function registerConfigBackupRoutes(app) {
|
|
118
|
+
const deny = (req, reply) => {
|
|
119
|
+
if (isMultiUserMode() && !isAdmin(req)) {
|
|
120
|
+
reply.code(403);
|
|
121
|
+
return createErrorResponse(ApiErrorCode.FORBIDDEN, 'Admin only in multi-user mode');
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
};
|
|
125
|
+
app.get('/api/config-backups', async (req, reply) => {
|
|
126
|
+
const denied = deny(req, reply);
|
|
127
|
+
if (denied)
|
|
128
|
+
return denied;
|
|
129
|
+
let eff;
|
|
130
|
+
try {
|
|
131
|
+
eff = await readEffectiveBackupSettings();
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
reply.code(503);
|
|
135
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
|
|
136
|
+
}
|
|
137
|
+
const { settings, dir, present } = eff;
|
|
138
|
+
// Read-only: a listing must not create config-backup-id, and without one nothing can be ours.
|
|
139
|
+
const origin = readConfigBackupOrigin(getDataDir());
|
|
140
|
+
return {
|
|
141
|
+
success: true,
|
|
142
|
+
data: {
|
|
143
|
+
settings,
|
|
144
|
+
dir,
|
|
145
|
+
defaultDir: defaultConfigBackupDir(getDataDir()),
|
|
146
|
+
tracked: CONFIG_BACKUP_FILES,
|
|
147
|
+
backups: present && origin ? listConfigBackups(dir, origin) : [],
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
app.post('/api/config-backups', async (req, reply) => {
|
|
152
|
+
const denied = deny(req, reply);
|
|
153
|
+
if (denied)
|
|
154
|
+
return denied;
|
|
155
|
+
try {
|
|
156
|
+
const out = await runConfigBackup('manual', { force: true });
|
|
157
|
+
return { success: true, data: { ...out.result, removed: out.removed } };
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
reply.code(err instanceof BackupDirUnavailableError ? 503 : 500);
|
|
161
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Backup failed: ${getErrorMessage(err)}`);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
app.get('/api/config-backups/diff', async (req, reply) => {
|
|
165
|
+
const denied = deny(req, reply);
|
|
166
|
+
if (denied)
|
|
167
|
+
return denied;
|
|
168
|
+
const q = req.query;
|
|
169
|
+
if (typeof q.from !== 'string' || !q.from) {
|
|
170
|
+
reply.code(400);
|
|
171
|
+
return createErrorResponse(ApiErrorCode.INVALID_INPUT, '`from` is required (a backup id or "current")');
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const { dir } = await readEffectiveBackupSettings();
|
|
175
|
+
const data = diffConfigBackups({
|
|
176
|
+
dataDir: getDataDir(),
|
|
177
|
+
backupDir: dir,
|
|
178
|
+
from: q.from,
|
|
179
|
+
to: typeof q.to === 'string' && q.to ? q.to : CONFIG_DIFF_CURRENT,
|
|
180
|
+
});
|
|
181
|
+
return { success: true, data };
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
if (err instanceof ConfigBackupNotFoundError) {
|
|
185
|
+
reply.code(404);
|
|
186
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, err.message);
|
|
187
|
+
}
|
|
188
|
+
if (err instanceof ConfigBackupCorruptError) {
|
|
189
|
+
reply.code(409);
|
|
190
|
+
return createErrorResponse(ApiErrorCode.CONFLICT, err.message);
|
|
191
|
+
}
|
|
192
|
+
reply.code(err instanceof BackupDirUnavailableError ? 503 : 500);
|
|
193
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Diff failed: ${getErrorMessage(err)}`);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
/**
|
|
197
|
+
* What a restore that wrote files must do, whether or not every file made it: reload what the running
|
|
198
|
+
* server caches, audit it, and name the files that only apply after a restart.
|
|
199
|
+
*/
|
|
200
|
+
const afterRestore = (req, id, restored, failed = []) => {
|
|
201
|
+
// IntentStore caches the file and rewrites it whole on the next prompt, which would undo the restore.
|
|
202
|
+
if (restored.includes('intents.json'))
|
|
203
|
+
intentStore.reload();
|
|
204
|
+
// restoreConfigBackup writes clis.json directly (the one writer besides mutateRegistryFile).
|
|
205
|
+
if (restored.includes('clis.json'))
|
|
206
|
+
reloadCliRegistry();
|
|
207
|
+
// A restore of users.json replaces accounts, password hashes, roles and bypass grants.
|
|
208
|
+
if (isMultiUserMode()) {
|
|
209
|
+
void appendAdminAudit({
|
|
210
|
+
admin: getAuthUser(req).username,
|
|
211
|
+
action: 'config_backup_restore',
|
|
212
|
+
target: id,
|
|
213
|
+
ip: req.ip,
|
|
214
|
+
detail: { restored, ...(failed.length > 0 && { failed }) },
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return restored.filter((n) => RESTART_REQUIRED.has(n));
|
|
218
|
+
};
|
|
219
|
+
app.post('/api/config-backups/:id/restore', async (req, reply) => {
|
|
220
|
+
const denied = deny(req, reply);
|
|
221
|
+
if (denied)
|
|
222
|
+
return denied;
|
|
223
|
+
const { id } = req.params;
|
|
224
|
+
try {
|
|
225
|
+
const { dir } = await readEffectiveBackupSettings();
|
|
226
|
+
const out = restoreConfigBackup({ dataDir: getDataDir(), backupDir: dir, id, appVersion: APP_VERSION });
|
|
227
|
+
return {
|
|
228
|
+
success: true,
|
|
229
|
+
data: {
|
|
230
|
+
restored: out.restored,
|
|
231
|
+
untouched: out.untouched,
|
|
232
|
+
safetySnapshot: out.safetySnapshot,
|
|
233
|
+
restartRequired: afterRestore(req, id, out.restored),
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
if (err instanceof ConfigBackupNotFoundError) {
|
|
239
|
+
reply.code(404);
|
|
240
|
+
return createErrorResponse(ApiErrorCode.NOT_FOUND, err.message);
|
|
241
|
+
}
|
|
242
|
+
if (err instanceof ConfigBackupCorruptError) {
|
|
243
|
+
reply.code(409);
|
|
244
|
+
return createErrorResponse(ApiErrorCode.CONFLICT, `${err.message}. Nothing was changed.`);
|
|
245
|
+
}
|
|
246
|
+
if (err instanceof ConfigBackupPartialRestoreError) {
|
|
247
|
+
// Some files are the backup's now: the client must learn which (Settings re-reads a restored
|
|
248
|
+
// settings.json, or its next Save writes the old values back) and where the undo copy is.
|
|
249
|
+
const failedNames = err.failed.map((f) => f.name);
|
|
250
|
+
const restartRequired = afterRestore(req, id, err.restored, failedNames);
|
|
251
|
+
reply.code(500);
|
|
252
|
+
return {
|
|
253
|
+
...createErrorResponse(ApiErrorCode.OPERATION_FAILED, err.message),
|
|
254
|
+
data: {
|
|
255
|
+
restored: err.restored,
|
|
256
|
+
failed: err.failed,
|
|
257
|
+
untouched: err.untouched,
|
|
258
|
+
safetySnapshot: err.safetySnapshot,
|
|
259
|
+
restartRequired,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
reply.code(err instanceof BackupDirUnavailableError ? 503 : 500);
|
|
264
|
+
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, `Restore failed: ${getErrorMessage(err)}`);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=config-backup-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-backup-routes.js","sourceRoot":"","sources":["../../../src/web/routes/config-backup-routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -40,14 +40,30 @@ export declare const CLAUDE_MIN_SAFE_CONTEXT_TOKENS = 40000;
|
|
|
40
40
|
* with a small enough baseline of its own to never trip this would have no reason to
|
|
41
41
|
* declare the field in the first place, so the check simply never applies to it.
|
|
42
42
|
*/
|
|
43
|
-
export declare function exceedsSafeContextFloor(entry: Pick<CliEntry, 'capabilities'>, contextLength: number | undefined): boolean;
|
|
43
|
+
export declare function exceedsSafeContextFloor(entry: Pick<CliEntry, 'capabilities'>, contextLength: number | undefined, slotCount?: number): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The context one request can rely on when the server runs `slotCount` parallel slots.
|
|
46
|
+
* llama-server divides its context across `--parallel` slots (by default they share ONE
|
|
47
|
+
* unified KV pool), and Claude Code sends concurrent requests: the main session plus every
|
|
48
|
+
* parallel subagent, each carrying the full `CLAUDE_MIN_SAFE_CONTEXT_TOKENS` of its own
|
|
49
|
+
* system prompt and tools. With N busy slots each request therefore gets roughly
|
|
50
|
+
* `contextLength / N`, and once the pool is spent llama.cpp answers HTTP 500 "Context size
|
|
51
|
+
* has been exceeded" (measured: 4 slots, ~131k pool, four simultaneous 40k-token requests
|
|
52
|
+
* all failed; the same requests against one slot all succeeded). An unknown or single slot
|
|
53
|
+
* count leaves the context whole.
|
|
54
|
+
*/
|
|
55
|
+
export declare function perSlotContext(contextLength: number, slotCount: number | undefined): number;
|
|
44
56
|
export interface DiscoveryResult {
|
|
45
57
|
models: string[];
|
|
46
58
|
/** See `CustomModelHost.modelContextLengths` — only ever populated for models already loaded. */
|
|
47
59
|
contextLengths: Record<string, number>;
|
|
60
|
+
/** See `CustomModelHost.modelSlotCounts`; populated on the same loaded-models-only basis as `contextLengths`. */
|
|
61
|
+
slotCounts: Record<string, number>;
|
|
48
62
|
/** See `CustomModelHost.modelSizesGB` — populated for every model whose own listing states one. */
|
|
49
63
|
sizesGB: Record<string, number>;
|
|
50
64
|
}
|
|
65
|
+
/** Forget what was probed (tests only: a changed launch command already probes again). */
|
|
66
|
+
export declare function clearPropsProbeCache(): void;
|
|
51
67
|
export interface LlamaSwapRunningModel {
|
|
52
68
|
model: string;
|
|
53
69
|
state: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"custom-model-routes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/custom-model-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,SAAS,CAAC;AAQ/D,OAAO,EAA+C,KAAK,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAChH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AAMnE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,8BAA8B,QAAQ,CAAC;AAEpD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,IAAI,CAAC,QAAQ,EAAE,cAAc,CAAC,EACrC,aAAa,EAAE,MAAM,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"custom-model-routes.d.ts","sourceRoot":"","sources":["../../../src/web/routes/custom-model-routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -48,11 +48,30 @@ export const CLAUDE_MIN_SAFE_CONTEXT_TOKENS = 40000;
|
|
|
48
48
|
* with a small enough baseline of its own to never trip this would have no reason to
|
|
49
49
|
* declare the field in the first place, so the check simply never applies to it.
|
|
50
50
|
*/
|
|
51
|
-
export function exceedsSafeContextFloor(entry, contextLength) {
|
|
51
|
+
export function exceedsSafeContextFloor(entry, contextLength, slotCount) {
|
|
52
52
|
const cap = entry.capabilities.customModelInjection;
|
|
53
53
|
if (cap.kind !== 'env' || !cap.contextLengthVar)
|
|
54
54
|
return false;
|
|
55
|
-
|
|
55
|
+
if (typeof contextLength !== 'number')
|
|
56
|
+
return false;
|
|
57
|
+
if (contextLength < CLAUDE_MIN_SAFE_CONTEXT_TOKENS)
|
|
58
|
+
return true;
|
|
59
|
+
return perSlotContext(contextLength, slotCount) < CLAUDE_MIN_SAFE_CONTEXT_TOKENS;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The context one request can rely on when the server runs `slotCount` parallel slots.
|
|
63
|
+
* llama-server divides its context across `--parallel` slots (by default they share ONE
|
|
64
|
+
* unified KV pool), and Claude Code sends concurrent requests: the main session plus every
|
|
65
|
+
* parallel subagent, each carrying the full `CLAUDE_MIN_SAFE_CONTEXT_TOKENS` of its own
|
|
66
|
+
* system prompt and tools. With N busy slots each request therefore gets roughly
|
|
67
|
+
* `contextLength / N`, and once the pool is spent llama.cpp answers HTTP 500 "Context size
|
|
68
|
+
* has been exceeded" (measured: 4 slots, ~131k pool, four simultaneous 40k-token requests
|
|
69
|
+
* all failed; the same requests against one slot all succeeded). An unknown or single slot
|
|
70
|
+
* count leaves the context whole.
|
|
71
|
+
*/
|
|
72
|
+
export function perSlotContext(contextLength, slotCount) {
|
|
73
|
+
const slots = typeof slotCount === 'number' && Number.isFinite(slotCount) && slotCount > 1 ? Math.floor(slotCount) : 1;
|
|
74
|
+
return Math.floor(contextLength / slots);
|
|
56
75
|
}
|
|
57
76
|
function adminOnly(req, reply) {
|
|
58
77
|
if (!isMultiUserMode() || isAdmin(req))
|
|
@@ -96,7 +115,7 @@ function applyStoredApiKey(incoming, existing) {
|
|
|
96
115
|
return incoming.apiKey ? incoming : { ...incoming, apiKey: existing.apiKey };
|
|
97
116
|
}
|
|
98
117
|
/**
|
|
99
|
-
* `modelContextLengths`/`modelSizesGB` are server-populated by discovery, never
|
|
118
|
+
* `modelContextLengths`/`modelSlotCounts`/`modelSizesGB` are server-populated by discovery, never
|
|
100
119
|
* user-entered, and PUT replaces the whole record — so merge them back in from the
|
|
101
120
|
* stored host rather than trust whatever the editor's body carried (or omitted).
|
|
102
121
|
* The editor only ever sends `models`/`lastDiscoveredAt` verbatim from its cached
|
|
@@ -104,7 +123,12 @@ function applyStoredApiKey(incoming, existing) {
|
|
|
104
123
|
* future caller forgets, same class of bug `applyStoredApiKey` exists to prevent.
|
|
105
124
|
*/
|
|
106
125
|
function applyDiscoveredFields(incoming, existing) {
|
|
107
|
-
return {
|
|
126
|
+
return {
|
|
127
|
+
...incoming,
|
|
128
|
+
modelContextLengths: existing.modelContextLengths,
|
|
129
|
+
modelSlotCounts: existing.modelSlotCounts,
|
|
130
|
+
modelSizesGB: existing.modelSizesGB,
|
|
131
|
+
};
|
|
108
132
|
}
|
|
109
133
|
function authHeaders(host) {
|
|
110
134
|
const headers = {};
|
|
@@ -137,7 +161,7 @@ function parseSizeGB(description) {
|
|
|
137
161
|
}
|
|
138
162
|
/**
|
|
139
163
|
* Best-effort: fetches `GET /props?model=<id>` (llama.cpp-native, llama-swap-proxied) for
|
|
140
|
-
* ONE already-loaded model and pulls its
|
|
164
|
+
* ONE already-loaded model and pulls its `n_ctx` and `total_slots` out. Never called for a model that
|
|
141
165
|
* isn't already loaded — see the caller and `CustomModelHost.modelContextLengths` for why
|
|
142
166
|
* that's a hard safety requirement, not just a nicety: llama-swap treats this endpoint's
|
|
143
167
|
* `?model=` as a routing hint, and asking it about an unloaded model risks triggering an
|
|
@@ -153,17 +177,31 @@ function parseSizeGB(description) {
|
|
|
153
177
|
* (below), which reads the actual launch flag `/running` reports, is the primary source;
|
|
154
178
|
* this is only used when that parse comes up empty (no recognized flag in `cmd`, or `cmd`
|
|
155
179
|
* itself unavailable).
|
|
180
|
+
*
|
|
181
|
+
* ⚠️ `/props` only ever RAISES the slot warning, never skips it: its `n_ctx` is the context
|
|
182
|
+
* fallback when `cmd` states no context flag at all, and `total_slots` is the slot count when
|
|
183
|
+
* `cmd` has no `--parallel`. It never widens the pool the slot check divides. And it is not
|
|
184
|
+
* free: llama-swap routes it through the model's process handler, which refreshes the model's
|
|
185
|
+
* last-use time, so probing a model with a `ttl` on every 5-minute sweep would keep it loaded
|
|
186
|
+
* forever. `discoverModels` therefore probes once per launch command (`propsByCmd`).
|
|
187
|
+
*
|
|
188
|
+
* Returns `undefined` when the request itself failed (network error, timeout, unreadable body),
|
|
189
|
+
* which is retried next sweep; a non-2xx status, or a 2xx without the fields, gives an object with
|
|
190
|
+
* them unset, which `propsForLaunch` remembers.
|
|
156
191
|
*/
|
|
157
|
-
async function
|
|
192
|
+
async function fetchServerProps(host, modelId, headers) {
|
|
158
193
|
try {
|
|
159
194
|
const url = new URL(`${host.baseUrl.replace(/\/+$/, '')}/props`);
|
|
160
195
|
url.searchParams.set('model', modelId);
|
|
161
196
|
const res = await webviewFetch(url, { headers, signal: AbortSignal.timeout(PROPS_TIMEOUT_MS) });
|
|
197
|
+
// A non-2xx answer (an upstream that does not serve /props) is a real answer: empty, so it is
|
|
198
|
+
// remembered per launch command instead of being re-probed every sweep.
|
|
162
199
|
if (!res.ok)
|
|
163
|
-
return
|
|
200
|
+
return {};
|
|
164
201
|
const body = (await res.json());
|
|
165
202
|
const nCtx = body.n_ctx ?? body.default_generation_settings?.n_ctx;
|
|
166
|
-
|
|
203
|
+
const positive = (v) => typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : undefined;
|
|
204
|
+
return { nCtx: positive(nCtx), totalSlots: positive(body.total_slots) };
|
|
167
205
|
}
|
|
168
206
|
catch {
|
|
169
207
|
return undefined;
|
|
@@ -173,7 +211,7 @@ async function fetchContextLength(host, modelId, headers) {
|
|
|
173
211
|
* Parses the REAL configured context size out of llama-swap's own launch command for a
|
|
174
212
|
* model (`/running`'s `cmd` field, e.g. `"llama-server -m ... --fit-ctx 16384 ..."`) —
|
|
175
213
|
* the primary source for `modelContextLengths`, preferred over `/props`'s `n_ctx` (see
|
|
176
|
-
* `
|
|
214
|
+
* `fetchServerProps`'s own doc comment for why that field is unreliable here). Checks
|
|
177
215
|
* `--fit-ctx` first (llama-swap's own auto-fit flag), then the plain llama.cpp
|
|
178
216
|
* `-c`/`--ctx-size`/`--ctx_size` flags a hand-written launch command might use instead.
|
|
179
217
|
* Returns `undefined` when `cmd` has none of these — not every launch command needs to
|
|
@@ -189,6 +227,46 @@ function parseCtxFromCmd(cmd) {
|
|
|
189
227
|
const value = Number(match[1]);
|
|
190
228
|
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
191
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Parses an explicit `--parallel N` / `-np N` (or `--parallel=N`) out of a launch command.
|
|
232
|
+
* Returns `undefined` when the command states none, including `-np -1` (llama.cpp's "pick
|
|
233
|
+
* automatically"), because a llama-server started without it chooses the slot count itself
|
|
234
|
+
* (4 on the build this was measured on) and only `/props`'s `total_slots` can say what it chose.
|
|
235
|
+
*/
|
|
236
|
+
function parseSlotsFromCmd(cmd) {
|
|
237
|
+
if (typeof cmd !== 'string')
|
|
238
|
+
return undefined;
|
|
239
|
+
const match = /(?:^|\s)(?:-np|--parallel)(?:\s+|=)(\d+)(?=\s|$)/.exec(cmd);
|
|
240
|
+
if (!match)
|
|
241
|
+
return undefined;
|
|
242
|
+
const value = Number(match[1]);
|
|
243
|
+
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
244
|
+
}
|
|
245
|
+
/** The `/props` answer for the launch command a model was last probed under, by host and model id. */
|
|
246
|
+
const propsByCmd = new Map();
|
|
247
|
+
/** Forget what was probed (tests only: a changed launch command already probes again). */
|
|
248
|
+
export function clearPropsProbeCache() {
|
|
249
|
+
propsByCmd.clear();
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* `/props` for one loaded model, but only once per launch command: the probe refreshes the model's
|
|
253
|
+
* last-use time in llama-swap, so repeating it on every rediscovery sweep would stop a model with a
|
|
254
|
+
* `ttl` from ever unloading. A changed `cmd` (a reloaded or reconfigured model) probes again; a
|
|
255
|
+
* `cmd` that is not a string cannot be told apart, so it is probed each time; a probe that did not
|
|
256
|
+
* answer (network error, timeout) is not remembered; a non-2xx status is. Held in memory only, so a restart probes once more.
|
|
257
|
+
*/
|
|
258
|
+
async function propsForLaunch(host, modelId, cmd, headers) {
|
|
259
|
+
const key = `${host.baseUrl}\0${modelId}`;
|
|
260
|
+
if (typeof cmd === 'string') {
|
|
261
|
+
const cached = propsByCmd.get(key);
|
|
262
|
+
if (cached?.cmd === cmd)
|
|
263
|
+
return cached.props;
|
|
264
|
+
}
|
|
265
|
+
const props = await fetchServerProps(host, modelId, headers);
|
|
266
|
+
if (props && typeof cmd === 'string')
|
|
267
|
+
propsByCmd.set(key, { cmd, props });
|
|
268
|
+
return props;
|
|
269
|
+
}
|
|
192
270
|
async function discoverModels(host) {
|
|
193
271
|
const headers = authHeaders(host);
|
|
194
272
|
const res = await webviewFetch(new URL(`${host.baseUrl.replace(/\/+$/, '')}/v1/models`), {
|
|
@@ -214,6 +292,7 @@ async function discoverModels(host) {
|
|
|
214
292
|
// fetchContextLength must only ever run against a model this server itself calls loaded.
|
|
215
293
|
const hasStatusField = entries.some((m) => m && typeof m === 'object' && 'status' in m);
|
|
216
294
|
const contextLengths = {};
|
|
295
|
+
const slotCounts = {};
|
|
217
296
|
if (hasStatusField) {
|
|
218
297
|
const loadedIds = entries
|
|
219
298
|
.filter((m) => m.status && typeof m.status === 'object' && m.status.value === 'loaded')
|
|
@@ -227,14 +306,35 @@ async function discoverModels(host) {
|
|
|
227
306
|
const swapStatus = await getLlamaSwapStatus(host);
|
|
228
307
|
const cmdById = new Map(swapStatus.running.map((r) => [r.model, r.cmd]));
|
|
229
308
|
for (const id of loadedIds) {
|
|
230
|
-
const
|
|
231
|
-
|
|
309
|
+
const cmd = cmdById.get(id);
|
|
310
|
+
let ctx = parseCtxFromCmd(cmd);
|
|
311
|
+
const cmdSlots = parseSlotsFromCmd(cmd);
|
|
312
|
+
let slots = cmdSlots;
|
|
313
|
+
// /props is only needed for whatever the launch command did not answer. The context
|
|
314
|
+
// still comes from the command whenever it states one (see fetchServerProps's doc
|
|
315
|
+
// comment for why /props's n_ctx is only a fallback); the slot count usually cannot
|
|
316
|
+
// come from the command at all, since an unset --parallel is chosen by the server.
|
|
317
|
+
if (ctx === undefined || slots === undefined) {
|
|
318
|
+
const props = await propsForLaunch(host, id, cmd, headers);
|
|
319
|
+
const ctxFromProps = ctx === undefined && props?.nCtx !== undefined;
|
|
320
|
+
ctx ??= props?.nCtx;
|
|
321
|
+
slots ??= props?.totalSlots;
|
|
322
|
+
// With an explicit --parallel N, llama.cpp reports /props's n_ctx PER SLOT (the unified
|
|
323
|
+
// KV pool is only the default when the count is auto), so it is already the figure one
|
|
324
|
+
// request can use, so the count is 1: recording N would divide it a second time, and
|
|
325
|
+
// recording nothing would leave an earlier round's count in place (applyDiscoveredModels
|
|
326
|
+
// merges onto what was learned before).
|
|
327
|
+
if (cmdSlots !== undefined && ctxFromProps)
|
|
328
|
+
slots = 1;
|
|
329
|
+
}
|
|
232
330
|
if (ctx !== undefined)
|
|
233
331
|
contextLengths[id] = ctx;
|
|
332
|
+
if (slots !== undefined)
|
|
333
|
+
slotCounts[id] = slots;
|
|
234
334
|
}
|
|
235
335
|
}
|
|
236
336
|
}
|
|
237
|
-
return { models, contextLengths, sizesGB };
|
|
337
|
+
return { models, contextLengths, slotCounts, sizesGB };
|
|
238
338
|
}
|
|
239
339
|
/**
|
|
240
340
|
* undici reports every network failure as `TypeError('fetch failed', { cause })`, with the
|
|
@@ -506,7 +606,7 @@ export function triggerLlamaSwapLoad(host, modelId) {
|
|
|
506
606
|
});
|
|
507
607
|
}
|
|
508
608
|
function applyDiscoveredModels(host, result) {
|
|
509
|
-
const { models, contextLengths, sizesGB } = result;
|
|
609
|
+
const { models, contextLengths, slotCounts, sizesGB } = result;
|
|
510
610
|
const defaultModelId = host.defaultModelId && models.includes(host.defaultModelId) ? host.defaultModelId : undefined;
|
|
511
611
|
// Merge onto what's already known rather than replacing: a model not probed this round
|
|
512
612
|
// (not currently loaded) keeps whatever context length an earlier round already learned
|
|
@@ -514,6 +614,10 @@ function applyDiscoveredModels(host, result) {
|
|
|
514
614
|
const merged = { ...host.modelContextLengths, ...contextLengths };
|
|
515
615
|
const kept = Object.fromEntries(Object.entries(merged).filter(([id]) => models.includes(id)));
|
|
516
616
|
const modelContextLengths = Object.keys(kept).length > 0 ? kept : undefined;
|
|
617
|
+
// Slot counts follow the same loaded-only, keep-what-an-earlier-round-learned rule.
|
|
618
|
+
const mergedSlots = { ...host.modelSlotCounts, ...slotCounts };
|
|
619
|
+
const keptSlots = Object.fromEntries(Object.entries(mergedSlots).filter(([id]) => models.includes(id)));
|
|
620
|
+
const modelSlotCounts = Object.keys(keptSlots).length > 0 ? keptSlots : undefined;
|
|
517
621
|
// sizesGB, unlike contextLengths, is populated for every model in the SAME pass (no
|
|
518
622
|
// loaded-only restriction — see parseSizeGB), so this is closer to a plain replace, but
|
|
519
623
|
// still merges onto the previous round rather than dropping a size for a model whose
|
|
@@ -526,6 +630,7 @@ function applyDiscoveredModels(host, result) {
|
|
|
526
630
|
models,
|
|
527
631
|
defaultModelId,
|
|
528
632
|
modelContextLengths,
|
|
633
|
+
modelSlotCounts,
|
|
529
634
|
modelSizesGB,
|
|
530
635
|
lastDiscoveredAt: new Date().toISOString(),
|
|
531
636
|
};
|