mirror of
https://github.com/runebookai/tome.git
synced 2025-07-21 00:27:30 +03:00
Adds OpenAI support
Refactors the dispatch logic to be more engine-agnostic. Engines are a backend to an LLM provider, like Ollama, OpenAI, etc. Each Engine has a Client that is responsible for actually talking to the backend. Models now have an `id` in the format of `<engine>:<model-name>`. So for example, `ollama:qwen3:14b`. Models, in the model dropdown in chat, are now grouped and organized by Engine. So there's an Ollama section, an OpenAI section, etc.
This commit is contained in:
@@ -59,6 +59,8 @@
|
||||
"change-case": "^5.4.4",
|
||||
"marked": "^15.0.7",
|
||||
"moment": "^2.30.1",
|
||||
"ollama": "^0.5.15",
|
||||
"openai": "^4.98.0",
|
||||
"tailwind-merge": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
244
pnpm-lock.yaml
generated
244
pnpm-lock.yaml
generated
@@ -35,6 +35,12 @@ importers:
|
||||
moment:
|
||||
specifier: ^2.30.1
|
||||
version: 2.30.1
|
||||
ollama:
|
||||
specifier: ^0.5.15
|
||||
version: 0.5.15
|
||||
openai:
|
||||
specifier: ^4.98.0
|
||||
version: 4.98.0
|
||||
tailwind-merge:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
@@ -47,16 +53,16 @@ importers:
|
||||
version: 9.21.0
|
||||
'@sveltejs/adapter-auto':
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))
|
||||
version: 4.0.0(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))
|
||||
'@sveltejs/adapter-static':
|
||||
specifier: ^3.0.8
|
||||
version: 3.0.8(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))
|
||||
version: 3.0.8(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))
|
||||
'@sveltejs/kit':
|
||||
specifier: ^2.16.0
|
||||
version: 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
version: 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
'@sveltejs/vite-plugin-svelte':
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.3(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
version: 5.0.3(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.0.12
|
||||
version: 4.0.12
|
||||
@@ -65,7 +71,7 @@ importers:
|
||||
version: 0.5.16(tailwindcss@4.0.12)
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.0.9
|
||||
version: 4.0.9(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
version: 4.0.9(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
'@tauri-apps/cli':
|
||||
specifier: ^2.3.1
|
||||
version: 2.3.1
|
||||
@@ -125,7 +131,7 @@ importers:
|
||||
version: 8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)
|
||||
vite:
|
||||
specifier: ^6.0.0
|
||||
version: 6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)
|
||||
version: 6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -772,6 +778,12 @@ packages:
|
||||
'@types/json5@0.0.29':
|
||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||
|
||||
'@types/node-fetch@2.6.12':
|
||||
resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
|
||||
|
||||
'@types/node@18.19.100':
|
||||
resolution: {integrity: sha512-ojmMP8SZBKprc3qGrGk8Ujpo80AXkrP7G2tOT4VWr5jlr5DHjsJF+emXJz+Wm0glmy4Js62oKMdZZ6B9Y+tEcA==}
|
||||
|
||||
'@types/uuid4@2.0.3':
|
||||
resolution: {integrity: sha512-/fyn8jzKzeL/wci7GOaz8TPjKapD+WJUBUCr/ED2xcUcx5fA9rZH2fDZiV2Z/a+040mp5Zi3dgIi/Vey/uQBxw==}
|
||||
|
||||
@@ -822,6 +834,10 @@ packages:
|
||||
resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||
engines: {node: '>=6.5'}
|
||||
|
||||
acorn-jsx@5.3.2:
|
||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||
peerDependencies:
|
||||
@@ -837,6 +853,10 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
agentkeepalive@4.6.0:
|
||||
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
|
||||
ajv@6.12.6:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
|
||||
@@ -879,6 +899,9 @@ packages:
|
||||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -938,6 +961,10 @@ packages:
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
@@ -998,6 +1025,10 @@ packages:
|
||||
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
detect-libc@1.0.3:
|
||||
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
|
||||
engines: {node: '>=0.10'}
|
||||
@@ -1164,6 +1195,10 @@ packages:
|
||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
event-target-shim@5.0.1:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
@@ -1211,6 +1246,17 @@ packages:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
form-data-encoder@1.7.2:
|
||||
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
||||
|
||||
form-data@4.0.2:
|
||||
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
formdata-node@4.4.1:
|
||||
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
|
||||
engines: {node: '>= 12.20'}
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@@ -1295,6 +1341,9 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
humanize-ms@1.2.1:
|
||||
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||
|
||||
ignore@5.3.2:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -1561,6 +1610,14 @@ packages:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
@@ -1593,6 +1650,20 @@ packages:
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
|
||||
node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
|
||||
object-inspect@1.13.4:
|
||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1617,6 +1688,21 @@ packages:
|
||||
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
ollama@0.5.15:
|
||||
resolution: {integrity: sha512-TSaZSJyP7MQJFjSmmNsoJiriwa3U+/UJRw6+M8aucs5dTsaWNZsBIGpDb5rXnW6nXxJBB/z79gZY8IaiIQgelQ==}
|
||||
|
||||
openai@4.98.0:
|
||||
resolution: {integrity: sha512-TmDKur1WjxxMPQAtLG5sgBSCJmX7ynTsGmewKzoDwl1fRxtbLOsiR0FA/AOAAtYUmP6azal+MYQuOENfdU+7yg==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
ws: ^8.18.0
|
||||
zod: ^3.23.8
|
||||
peerDependenciesMeta:
|
||||
ws:
|
||||
optional: true
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -1951,6 +2037,9 @@ packages:
|
||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
ts-api-utils@2.0.1:
|
||||
resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==}
|
||||
engines: {node: '>=18.12'}
|
||||
@@ -1996,6 +2085,9 @@ packages:
|
||||
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
undici-types@5.26.5:
|
||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
@@ -2050,6 +2142,19 @@ packages:
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
web-streams-polyfill@4.0.0-beta.3:
|
||||
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
whatwg-fetch@3.6.20:
|
||||
resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2324,18 +2429,18 @@ snapshots:
|
||||
|
||||
'@rtsao/scc@1.1.0': {}
|
||||
|
||||
'@sveltejs/adapter-auto@4.0.0(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))':
|
||||
'@sveltejs/adapter-auto@4.0.0(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))':
|
||||
dependencies:
|
||||
'@sveltejs/kit': 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
'@sveltejs/kit': 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
import-meta-resolve: 4.1.0
|
||||
|
||||
'@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))':
|
||||
'@sveltejs/adapter-static@3.0.8(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))':
|
||||
dependencies:
|
||||
'@sveltejs/kit': 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
'@sveltejs/kit': 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
|
||||
'@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))':
|
||||
'@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))':
|
||||
dependencies:
|
||||
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
'@types/cookie': 0.6.0
|
||||
cookie: 0.6.0
|
||||
devalue: 5.1.1
|
||||
@@ -2348,27 +2453,27 @@ snapshots:
|
||||
set-cookie-parser: 2.7.1
|
||||
sirv: 3.0.1
|
||||
svelte: 5.21.0
|
||||
vite: 6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)
|
||||
vite: 6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)
|
||||
|
||||
'@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))':
|
||||
'@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))':
|
||||
dependencies:
|
||||
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
debug: 4.4.0
|
||||
svelte: 5.21.0
|
||||
vite: 6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)
|
||||
vite: 6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))':
|
||||
'@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))':
|
||||
dependencies:
|
||||
'@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
'@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)))(svelte@5.21.0)(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
debug: 4.4.0
|
||||
deepmerge: 4.3.1
|
||||
kleur: 4.1.5
|
||||
magic-string: 0.30.17
|
||||
svelte: 5.21.0
|
||||
vite: 6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)
|
||||
vitefu: 1.0.6(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
vite: 6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)
|
||||
vitefu: 1.0.6(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -2495,13 +2600,13 @@ snapshots:
|
||||
postcss-selector-parser: 6.0.10
|
||||
tailwindcss: 4.0.12
|
||||
|
||||
'@tailwindcss/vite@4.0.9(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))':
|
||||
'@tailwindcss/vite@4.0.9(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.0.9
|
||||
'@tailwindcss/oxide': 4.0.9
|
||||
lightningcss: 1.29.1
|
||||
tailwindcss: 4.0.9
|
||||
vite: 6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)
|
||||
vite: 6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)
|
||||
|
||||
'@tauri-apps/api@2.3.0': {}
|
||||
|
||||
@@ -2580,6 +2685,15 @@ snapshots:
|
||||
|
||||
'@types/json5@0.0.29': {}
|
||||
|
||||
'@types/node-fetch@2.6.12':
|
||||
dependencies:
|
||||
'@types/node': 18.19.100
|
||||
form-data: 4.0.2
|
||||
|
||||
'@types/node@18.19.100':
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
|
||||
'@types/uuid4@2.0.3': {}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2))(eslint@9.21.0(jiti@2.4.2))(typescript@5.8.2)':
|
||||
@@ -2659,6 +2773,10 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.26.0
|
||||
eslint-visitor-keys: 4.2.0
|
||||
|
||||
abort-controller@3.0.0:
|
||||
dependencies:
|
||||
event-target-shim: 5.0.1
|
||||
|
||||
acorn-jsx@5.3.2(acorn@8.14.0):
|
||||
dependencies:
|
||||
acorn: 8.14.0
|
||||
@@ -2669,6 +2787,10 @@ snapshots:
|
||||
|
||||
acorn@8.14.0: {}
|
||||
|
||||
agentkeepalive@4.6.0:
|
||||
dependencies:
|
||||
humanize-ms: 1.2.1
|
||||
|
||||
ajv@6.12.6:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
@@ -2734,6 +2856,8 @@ snapshots:
|
||||
|
||||
async-function@1.0.0: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.1.0
|
||||
@@ -2793,6 +2917,10 @@ snapshots:
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
||||
cookie@0.6.0: {}
|
||||
@@ -2847,6 +2975,8 @@ snapshots:
|
||||
has-property-descriptors: 1.0.2
|
||||
object-keys: 1.1.1
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
detect-libc@1.0.3: {}
|
||||
|
||||
devalue@5.1.1: {}
|
||||
@@ -3127,6 +3257,8 @@ snapshots:
|
||||
|
||||
esutils@2.0.3: {}
|
||||
|
||||
event-target-shim@5.0.1: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-glob@3.3.3:
|
||||
@@ -3173,6 +3305,20 @@ snapshots:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
|
||||
form-data-encoder@1.7.2: {}
|
||||
|
||||
form-data@4.0.2:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
mime-types: 2.1.35
|
||||
|
||||
formdata-node@4.4.1:
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 4.0.0-beta.3
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
@@ -3258,6 +3404,10 @@ snapshots:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
humanize-ms@1.2.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
@@ -3493,6 +3643,12 @@ snapshots:
|
||||
braces: 3.0.3
|
||||
picomatch: 2.3.1
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-types@2.1.35:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
|
||||
minimatch@3.1.2:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.11
|
||||
@@ -3515,6 +3671,12 @@ snapshots:
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
object-inspect@1.13.4: {}
|
||||
|
||||
object-keys@1.1.1: {}
|
||||
@@ -3548,6 +3710,22 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
ollama@0.5.15:
|
||||
dependencies:
|
||||
whatwg-fetch: 3.6.20
|
||||
|
||||
openai@4.98.0:
|
||||
dependencies:
|
||||
'@types/node': 18.19.100
|
||||
'@types/node-fetch': 2.6.12
|
||||
abort-controller: 3.0.0
|
||||
agentkeepalive: 4.6.0
|
||||
form-data-encoder: 1.7.2
|
||||
formdata-node: 4.4.1
|
||||
node-fetch: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@@ -3881,6 +4059,8 @@ snapshots:
|
||||
|
||||
totalist@3.0.1: {}
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
ts-api-utils@2.0.1(typescript@5.8.2):
|
||||
dependencies:
|
||||
typescript: 5.8.2
|
||||
@@ -3948,26 +4128,40 @@ snapshots:
|
||||
has-symbols: 1.1.0
|
||||
which-boxed-primitive: 1.1.1
|
||||
|
||||
undici-types@5.26.5: {}
|
||||
|
||||
uri-js@4.4.1:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0):
|
||||
vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0):
|
||||
dependencies:
|
||||
esbuild: 0.25.0
|
||||
postcss: 8.5.3
|
||||
rollup: 4.34.9
|
||||
optionalDependencies:
|
||||
'@types/node': 18.19.100
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.4.2
|
||||
lightningcss: 1.29.1
|
||||
yaml: 2.7.0
|
||||
|
||||
vitefu@1.0.6(vite@6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)):
|
||||
vitefu@1.0.6(vite@6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)):
|
||||
optionalDependencies:
|
||||
vite: 6.2.0(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)
|
||||
vite: 6.2.0(@types/node@18.19.100)(jiti@2.4.2)(lightningcss@1.29.1)(yaml@2.7.0)
|
||||
|
||||
web-streams-polyfill@4.0.0-beta.3: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
whatwg-fetch@3.6.20: {}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
dependencies:
|
||||
|
||||
@@ -59,7 +59,7 @@ log "chmod +x hermit"
|
||||
chmod +x ~/.config/runebook/hermit/bin/hermit
|
||||
|
||||
log "hermit install node >> "$LOGFILE""
|
||||
hermit install node >> "$LOGFILE"
|
||||
hermit install node@lts >> "$LOGFILE"
|
||||
|
||||
log "npx $*"
|
||||
npx "$@" || log "Error running npx $*"
|
||||
|
||||
@@ -7,16 +7,16 @@
|
||||
import Message from '$components/Message.svelte';
|
||||
import { dispatch } from '$lib/dispatch';
|
||||
import type { IMessage } from '$lib/models/message';
|
||||
import Model from '$lib/models/model.svelte';
|
||||
import type { IModel } from '$lib/models/model';
|
||||
import Session, { type ISession } from '$lib/models/session';
|
||||
|
||||
interface Props {
|
||||
session: ISession;
|
||||
model: string;
|
||||
model: IModel;
|
||||
onMessages?: (message: IMessage[]) => Promise<void>;
|
||||
}
|
||||
|
||||
const { session, model }: Props = $props();
|
||||
const { session, model = $bindable() }: Props = $props();
|
||||
|
||||
// DOM elements used via `bind:this`
|
||||
let input: HTMLTextAreaElement;
|
||||
@@ -91,7 +91,7 @@
|
||||
<Flex class="h-content w-full flex-col p-8 pr-2 pb-0">
|
||||
<Scrollable bind:ref={content} class="mb-8">
|
||||
<!-- Chat Log -->
|
||||
<div class:opacity-25={!Model.exists(model)} class="bg-medium relative h-full w-full px-2">
|
||||
<div class:opacity-25={false} class="bg-medium relative h-full w-full px-2">
|
||||
{#each messages as message (message.id)}
|
||||
<Flex id="messages" class="w-full flex-col items-start">
|
||||
<!-- Svelte hack: ensure chat is always scrolled to the bottom when a new message is added -->
|
||||
@@ -116,7 +116,7 @@
|
||||
bind:this={input}
|
||||
oninput={resize}
|
||||
onkeydown={onChatInput}
|
||||
disabled={!Model.exists(model)}
|
||||
disabled={false}
|
||||
placeholder="Message..."
|
||||
class="disabled:text-dark item bg-dark border-light focus:border-purple/15 mb-8
|
||||
h-auto w-[calc(100%-calc(var(--spacing)*6))] grow self-start rounded-xl border
|
||||
|
||||
68
src/components/ModelMenu.svelte
Normal file
68
src/components/ModelMenu.svelte
Normal file
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import Flex from '$components/Flex.svelte';
|
||||
import type { IEngine } from '$lib/models/engine';
|
||||
import Model, { type IModel } from '$lib/models/model';
|
||||
|
||||
interface Props {
|
||||
engines: IEngine[];
|
||||
value: string;
|
||||
onselect?: (model: IModel) => Promise<void>;
|
||||
}
|
||||
|
||||
let { engines, onselect, value = $bindable() }: Props = $props();
|
||||
let isOpen = $state(false);
|
||||
|
||||
const model = $derived(Model.find(value));
|
||||
|
||||
async function select(m: IModel) {
|
||||
close();
|
||||
value = m.name;
|
||||
await onselect?.(m);
|
||||
}
|
||||
|
||||
function toggle(e: Event) {
|
||||
e.stopPropagation();
|
||||
isOpen = isOpen ? false : true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
isOpen = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Flex class="bg-medium relative h-16 w-full hover:cursor-pointer">
|
||||
<Flex
|
||||
onclick={(e) => toggle(e)}
|
||||
class="border-light absolute top-0 left-0 w-full justify-between
|
||||
rounded-md border p-2 px-4"
|
||||
>
|
||||
<p>{model?.name}</p>
|
||||
<p>⏷</p>
|
||||
</Flex>
|
||||
|
||||
{#if isOpen}
|
||||
<Flex
|
||||
class="border-light bg-medium absolute top-12 left-0 z-50 -mt-[1px]
|
||||
max-h-[calc(100vh*0.7)] w-full flex-col items-start overflow-y-auto rounded-md
|
||||
rounded-t-none border"
|
||||
>
|
||||
{#each engines as engine (engine.id)}
|
||||
<p class="text-medium px-4 pt-4 pb-2 text-sm font-[500] uppercase">
|
||||
{engine.name}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
{#each engine.models as model (model.id)}
|
||||
<button
|
||||
onclick={async () => await select(model)}
|
||||
class="border-light w-full border-b p-2 px-4 pl-8 text-left
|
||||
first:border-t last:border-b-0 hover:cursor-pointer"
|
||||
>
|
||||
{model.name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</Flex>
|
||||
{/if}
|
||||
</Flex>
|
||||
@@ -6,8 +6,13 @@
|
||||
import Flex from '$components/Flex.svelte';
|
||||
import closables from '$lib/closables';
|
||||
|
||||
interface Option {
|
||||
display: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
options: string[];
|
||||
options: Option[];
|
||||
value?: string;
|
||||
onSelect: () => Promise<void>;
|
||||
}
|
||||
@@ -55,10 +60,10 @@
|
||||
>
|
||||
{#each options as option (option)}
|
||||
<button
|
||||
onclick={async () => await select(option)}
|
||||
onclick={async () => await select(option.value)}
|
||||
class="bg-dark border-b-light w-full border-b p-2 px-4 text-left last:border-b-0 hover:cursor-pointer"
|
||||
>
|
||||
{option}
|
||||
{option.display}
|
||||
</button>
|
||||
{/each}
|
||||
</Flex>
|
||||
|
||||
@@ -6,12 +6,11 @@ import { goto } from '$app/navigation';
|
||||
import { WELCOME_AGREED } from "$lib/const";
|
||||
import Config from "$lib/config";
|
||||
import { setupDeeplinks } from '$lib/deeplinks';
|
||||
import { OllamaClient } from '$lib/llm';
|
||||
import { info } from '$lib/logger';
|
||||
import App from "$lib/models/app";
|
||||
import Engine from '$lib/models/engine';
|
||||
import McpServer from "$lib/models/mcp-server";
|
||||
import Message from "$lib/models/message";
|
||||
import Model from '$lib/models/model.svelte';
|
||||
import Session from "$lib/models/session";
|
||||
import Setting from "$lib/models/setting";
|
||||
import startup, { StartupCheck } from "$lib/startup";
|
||||
@@ -29,21 +28,9 @@ export const init: ClientInit = async () => {
|
||||
await Message.sync();
|
||||
await McpServer.sync();
|
||||
await Setting.sync();
|
||||
await Engine.sync();
|
||||
info('[green]✔ database synced');
|
||||
|
||||
const client = new OllamaClient();
|
||||
|
||||
await startup.addCheck(
|
||||
StartupCheck.Ollama,
|
||||
async () => await client.connected(),
|
||||
);
|
||||
|
||||
await startup.addCheck(
|
||||
StartupCheck.MissingModels,
|
||||
async () => await client.hasModels(),
|
||||
async () => await Model.sync(),
|
||||
);
|
||||
|
||||
await startup.addCheck(
|
||||
StartupCheck.Agreement,
|
||||
async () => await Config.get(WELCOME_AGREED) === true,
|
||||
|
||||
6
src/lib/deeplinks.d.ts
vendored
6
src/lib/deeplinks.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
export interface VSCodeMcpInstallConfig {
|
||||
name: string;
|
||||
type: string;
|
||||
command: string;
|
||||
args: string[];
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
export * from '$lib/deeplinks.d';
|
||||
export interface VSCodeMcpInstallConfig {
|
||||
name: string;
|
||||
type: string;
|
||||
command: string;
|
||||
args: string[];
|
||||
}
|
||||
|
||||
export enum DeepLinks {
|
||||
InstallMcpServer = 'mcp/install',
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import { type LlmOptions, OllamaClient } from "./llm";
|
||||
|
||||
import type { Options } from "$lib/engines/types";
|
||||
import { error } from "$lib/logger";
|
||||
import App from "$lib/models/app";
|
||||
import Engine from "$lib/models/engine";
|
||||
import Message, { type IMessage } from "$lib/models/message";
|
||||
import type { IModel } from "$lib/models/model";
|
||||
import Session, { type ISession } from "$lib/models/session";
|
||||
|
||||
export async function dispatch(session: ISession, model: string, prompt?: string): Promise<IMessage> {
|
||||
export async function dispatch(session: ISession, model: IModel, prompt?: string): Promise<IMessage> {
|
||||
const app = App.find(session.appId as number);
|
||||
const client = new OllamaClient();
|
||||
const engine = Engine.fromModelId(model.id);
|
||||
|
||||
if (!engine) {
|
||||
error(`MissingEngineError`, model.id);
|
||||
throw `MissingEngineError: ${model.id}`;
|
||||
}
|
||||
|
||||
if (!app) {
|
||||
throw "Missing app";
|
||||
error(`MissingAppError`, session.appId);
|
||||
throw `MissingAppError: ${session.appId}`;
|
||||
}
|
||||
|
||||
if (prompt) {
|
||||
@@ -28,12 +36,12 @@ export async function dispatch(session: ISession, model: string, prompt?: string
|
||||
tool_calls: m.toolCalls,
|
||||
}));
|
||||
|
||||
const options: LlmOptions = {
|
||||
const options: Options = {
|
||||
num_ctx: session.config.contextWindow,
|
||||
temperature: session.config.temperature,
|
||||
};
|
||||
|
||||
const message = await client.chat(
|
||||
const message = await engine.client.chat(
|
||||
model,
|
||||
messages,
|
||||
await Session.tools(session),
|
||||
|
||||
84
src/lib/engines/ollama.ts
Normal file
84
src/lib/engines/ollama.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Ollama as OllamaClient } from 'ollama/browser';
|
||||
|
||||
import type { Client, Message, Options, Tool } from '$lib/engines/types';
|
||||
import type { IModel } from '$lib/models';
|
||||
import type { IMessage } from "$lib/models/message";
|
||||
import Setting from "$lib/models/setting";
|
||||
|
||||
export default class Ollama implements Client {
|
||||
client: OllamaClient;
|
||||
|
||||
constructor(host: string) {
|
||||
this.client = new OllamaClient({ host });
|
||||
}
|
||||
|
||||
async chat(model: IModel, messages: Message[], tools: Tool[] = [], options: Options = {}): Promise<IMessage> {
|
||||
const response = await this.client.chat({
|
||||
model: model.name,
|
||||
messages,
|
||||
tools,
|
||||
options,
|
||||
stream: false,
|
||||
});
|
||||
|
||||
let thought: string | undefined;
|
||||
let content: string = response
|
||||
.message
|
||||
.content
|
||||
.replace(/\.$/, '')
|
||||
.replace(/^"/, '')
|
||||
.replace(/"$/, '');
|
||||
|
||||
if (content.includes('<think>')) {
|
||||
[thought, content] = content.split('</think>');
|
||||
thought = thought.replace('<think>', '').trim();
|
||||
content = content.trim();
|
||||
}
|
||||
|
||||
return {
|
||||
model: model.name,
|
||||
role: 'assistant',
|
||||
content,
|
||||
thought,
|
||||
name: '',
|
||||
toolCalls: response.message.tool_calls || [],
|
||||
};
|
||||
}
|
||||
|
||||
async models(): Promise<IModel[]> {
|
||||
const models = (
|
||||
await this.client.list()
|
||||
).models;
|
||||
|
||||
return Promise.all(
|
||||
models.map(async (model) => (
|
||||
await this.info(model.name)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
async info(name: string): Promise<IModel> {
|
||||
const metadata = await this.client.show({ model: name });
|
||||
|
||||
// @ts-expect-error The Ollama SDK doesn't define the `capabilities`
|
||||
// property, but the API returns it.
|
||||
const capabilities = metadata.capabilities as string[];
|
||||
|
||||
return {
|
||||
id: `ollama:${name}`,
|
||||
name,
|
||||
metadata,
|
||||
supportsTools: capabilities.includes('tools'),
|
||||
};
|
||||
}
|
||||
|
||||
async connected(): Promise<boolean> {
|
||||
if (!Setting.OllamaUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
await fetch(Setting.OllamaUrl)
|
||||
).status == 200;
|
||||
}
|
||||
}
|
||||
96
src/lib/engines/openai.ts
Normal file
96
src/lib/engines/openai.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { OpenAI as OpenAIClient } from 'openai';
|
||||
|
||||
import type { Client, Message, Options, Tool, ToolCall } from '$lib/engines/types';
|
||||
import type { IMessage, IModel } from "$lib/models";
|
||||
|
||||
const SUPPORTED_MODELS = [
|
||||
'gpt-4o',
|
||||
'o4-mini',
|
||||
'gpt-4.5-preview',
|
||||
'gpt-4.1',
|
||||
'gpt-4.1-mini',
|
||||
];
|
||||
|
||||
export default class OpenAI implements Client {
|
||||
client: OpenAIClient;
|
||||
|
||||
constructor(apiKey: string) {
|
||||
this.client = new OpenAIClient({
|
||||
apiKey,
|
||||
dangerouslyAllowBrowser: true,
|
||||
});
|
||||
}
|
||||
|
||||
async chat(model: IModel, messages: Message[], tools: Tool[] = [], options: Options = {}): Promise<IMessage> {
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: model.name,
|
||||
// @ts-expect-error For some reason, the type of `messages` is marked
|
||||
// incorrect because of a difference in the `role`. Our `Role` type is
|
||||
// ripped straight from the OpenAI code, though, so :shrug:
|
||||
messages,
|
||||
tools,
|
||||
temperature: options.temperature,
|
||||
});
|
||||
|
||||
const {
|
||||
role,
|
||||
content,
|
||||
tool_calls,
|
||||
} = response.choices[0].message;
|
||||
|
||||
let toolCalls: ToolCall[] = [];
|
||||
|
||||
if (tool_calls) {
|
||||
toolCalls = tool_calls.map(tc => ({
|
||||
function: {
|
||||
name: tc.function.name,
|
||||
arguments: JSON.parse(tc.function.arguments),
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return {
|
||||
model: model.name,
|
||||
name: '',
|
||||
role,
|
||||
content: content || '',
|
||||
toolCalls,
|
||||
};
|
||||
}
|
||||
|
||||
async models(): Promise<IModel[]> {
|
||||
return (
|
||||
await this.client.models.list()
|
||||
)
|
||||
.data
|
||||
.filter(model => SUPPORTED_MODELS.includes(model.id))
|
||||
.map(model => {
|
||||
const { id, ...metadata } = model;
|
||||
return {
|
||||
id: `openai:${id}`,
|
||||
name: id,
|
||||
metadata,
|
||||
supportsTools: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async info(model: string): Promise<IModel> {
|
||||
const { id, ...metadata } = (
|
||||
await this.client.models.retrieve(model)
|
||||
);
|
||||
|
||||
return {
|
||||
id: `openai:${id}`,
|
||||
name: id,
|
||||
metadata,
|
||||
supportsTools: true,
|
||||
};
|
||||
}
|
||||
|
||||
async connected(): Promise<boolean> {
|
||||
return (
|
||||
await fetch('https://api.openai.com')
|
||||
).status == 200;
|
||||
}
|
||||
}
|
||||
55
src/lib/engines/types.ts
Normal file
55
src/lib/engines/types.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { IMessage, IModel } from "$lib/models";
|
||||
|
||||
export interface Client {
|
||||
chat(model: IModel, messages: Message[], tools?: Tool[], options?: Options): Promise<IMessage>;
|
||||
models(): Promise<IModel[]>;
|
||||
info(model: string): Promise<IModel>;
|
||||
connected(): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
num_ctx?: number;
|
||||
temperature?: number;
|
||||
}
|
||||
|
||||
export type Role =
|
||||
| 'developer'
|
||||
| 'system'
|
||||
| 'user'
|
||||
| 'assistant'
|
||||
| 'tool'
|
||||
| 'function';
|
||||
|
||||
export interface Message {
|
||||
role: Role;
|
||||
content: string;
|
||||
name: string;
|
||||
tool_calls?: ToolCall[];
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
function: {
|
||||
name: string;
|
||||
arguments: {
|
||||
[key: string]: any; // eslint-disable-line
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
type: 'function';
|
||||
function: {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: {
|
||||
type: string;
|
||||
required: string[];
|
||||
properties: Record<string, Property>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Property {
|
||||
type: string;
|
||||
description: string;
|
||||
}
|
||||
111
src/lib/llm.d.ts
vendored
111
src/lib/llm.d.ts
vendored
@@ -1,111 +0,0 @@
|
||||
export interface OllamaRequest {
|
||||
model: string;
|
||||
stream: boolean;
|
||||
tools: Tool[];
|
||||
options: Options;
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export interface OllamaResponse {
|
||||
created_at: string;
|
||||
done: boolean;
|
||||
done_reason: string;
|
||||
eval_count: number;
|
||||
eval_duration: number;
|
||||
load_duration: number;
|
||||
message: {
|
||||
content: string;
|
||||
role: LlmMessageRole;
|
||||
tool_calls: ToolCall[];
|
||||
};
|
||||
model: string;
|
||||
prompt_eval_count: number;
|
||||
prompt_eval_duration: number;
|
||||
total_duration: number;
|
||||
}
|
||||
|
||||
export interface LlmOptions {
|
||||
num_ctx?: number;
|
||||
temperature?: number;
|
||||
}
|
||||
|
||||
export interface OllamaModel {
|
||||
name: string;
|
||||
modifiedAt: string;
|
||||
size: number;
|
||||
digest: string;
|
||||
details: {
|
||||
parentModel: string;
|
||||
format: string;
|
||||
family: string;
|
||||
families: string[];
|
||||
parameter_size: string;
|
||||
quantization_level: string;
|
||||
};
|
||||
model_info: {
|
||||
"general.architecture": string;
|
||||
"general.file_type": number;
|
||||
"general.parameter_count": number;
|
||||
"general.quantization_version": number;
|
||||
"llama.attention.head_count": number;
|
||||
"llama.attention.head_count_kv": number;
|
||||
"llama.attention.layer_norm_rms_epsilon": number;
|
||||
"llama.block_count": number;
|
||||
"llama.context_length": number;
|
||||
"llama.embedding_length": number;
|
||||
"llama.feed_forward_length": number;
|
||||
"llama.rope.dimension_count": number;
|
||||
"llama.rope.freq_base": number;
|
||||
"llama.vocab_size": number;
|
||||
"tokenizer.ggml.bos_token_id": number;
|
||||
"tokenizer.ggml.eos_token_id": number;
|
||||
"tokenizer.ggml.merges": string[];
|
||||
"tokenizer.ggml.model": string;
|
||||
"tokenizer.ggml.pre": string;
|
||||
"tokenizer.ggml.token_type": string[];
|
||||
"tokenizer.ggml.tokens": string[];
|
||||
};
|
||||
capabilities?: string[];
|
||||
}
|
||||
|
||||
export type OllamaTag = Pick<OllamaModel, 'name' | 'modifiedAt' | 'size' | 'digest' | 'details'>;
|
||||
export type LlmMessageRole = 'system' | 'tool' | 'user' | 'assistant';
|
||||
|
||||
export interface OllamaTags {
|
||||
models: Tag[];
|
||||
}
|
||||
|
||||
export interface LlmMessage {
|
||||
role: LlmMessageRole;
|
||||
content: string;
|
||||
name: string;
|
||||
tool_calls?: Record<string, any>; // eslint-disable-line
|
||||
}
|
||||
|
||||
export interface LlmToolCall {
|
||||
function: {
|
||||
name: string;
|
||||
// We have no way of knowing what the LLM will pass
|
||||
// eslint-disable-next-line
|
||||
arguments: Record<string, any>;
|
||||
}
|
||||
}
|
||||
|
||||
export interface LlmTool {
|
||||
type: string;
|
||||
function: {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: {
|
||||
type: string;
|
||||
required: string[];
|
||||
properties: Record<string, Property>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface LlmProperty {
|
||||
type: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import type { LlmTool } from "$lib/llm";
|
||||
import type { Tool } from "$lib/engines/types";
|
||||
import type { McpTool } from "$lib/mcp.d";
|
||||
import type { ISession } from "$lib/models/session";
|
||||
|
||||
@@ -9,7 +9,7 @@ export * from '$lib/mcp.d';
|
||||
// Retrieve, and transform, tools from the MCP server, into `tools` object we
|
||||
// can send to the LLM.
|
||||
//
|
||||
export async function getMCPTools(session: ISession): Promise<LlmTool[]> {
|
||||
export async function getMCPTools(session: ISession): Promise<Tool[]> {
|
||||
return (
|
||||
await invoke<McpTool[]>('get_mcp_tools', { sessionId: session.id })
|
||||
).map(tool => {
|
||||
|
||||
@@ -181,7 +181,7 @@ export default function Model<Interface extends Obj, Row extends Obj>(table: str
|
||||
/**
|
||||
* Find the first occurence by a subset of the model's properties.
|
||||
*/
|
||||
static findBy(params: Partial<Interface>): Interface {
|
||||
static findBy(params: Partial<Interface>): Interface | undefined {
|
||||
return this.where(params)[0];
|
||||
}
|
||||
|
||||
@@ -490,3 +490,40 @@ export default function Model<Interface extends Obj, Row extends Obj>(table: str
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Model class NOT backed by a database
|
||||
*/
|
||||
export function BareModel<T extends Obj>() {
|
||||
let repo: T[] = $state([]);
|
||||
|
||||
return class BareModel {
|
||||
static reset(instances: T[] = []) {
|
||||
repo = instances;
|
||||
}
|
||||
|
||||
static add(instance: T) {
|
||||
repo.push(instance);
|
||||
}
|
||||
|
||||
static delete(instance: T) {
|
||||
repo = repo.filter(i => i !== instance);
|
||||
}
|
||||
|
||||
static all(): T[] {
|
||||
return repo;
|
||||
}
|
||||
|
||||
static find(id: string): T | undefined {
|
||||
return repo.findBy('id', id);
|
||||
}
|
||||
|
||||
static first(): T {
|
||||
return repo[0];
|
||||
}
|
||||
|
||||
static last(): T {
|
||||
return repo[repo.length - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
49
src/lib/models/engine.ts
Normal file
49
src/lib/models/engine.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import Ollama from '$lib/engines/ollama';
|
||||
import OpenAI from '$lib/engines/openai'
|
||||
import type { Client } from '$lib/engines/types';
|
||||
import { Setting } from '$lib/models';
|
||||
import { BareModel } from '$lib/models/base.svelte';
|
||||
import Model, { type IModel } from '$lib/models/model';
|
||||
|
||||
export interface IEngine {
|
||||
id: string;
|
||||
name: string;
|
||||
client: Client;
|
||||
models: IModel[];
|
||||
}
|
||||
|
||||
export default class Engine extends BareModel<IEngine>() {
|
||||
static fromModelId(id: string): IEngine | undefined {
|
||||
return this.find(id.split(':')[0]);
|
||||
}
|
||||
|
||||
static async sync() {
|
||||
this.reset();
|
||||
|
||||
if (Setting.OllamaUrl) {
|
||||
const client = new Ollama(Setting.OllamaUrl);
|
||||
|
||||
this.add({
|
||||
id: 'ollama',
|
||||
name: 'Ollama',
|
||||
client,
|
||||
models: (await client.models()).sortBy('name'),
|
||||
})
|
||||
}
|
||||
|
||||
if (Setting.OpenAIKey) {
|
||||
const client = new OpenAI(Setting.OpenAIKey);
|
||||
|
||||
this.add({
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
client,
|
||||
models: (await client.models()).sortBy('name'),
|
||||
});
|
||||
}
|
||||
|
||||
Model.reset(
|
||||
this.all().flatMap(engine => engine.models)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { info } from '$lib/logger';
|
||||
import App from '$lib/models/app';
|
||||
import Engine from '$lib/models/engine';
|
||||
import McpServer from '$lib/models/mcp-server';
|
||||
import Message from '$lib/models/message';
|
||||
import Model from '$lib/models/model.svelte';
|
||||
import Session from '$lib/models/session';
|
||||
import Setting from '$lib/models/setting';
|
||||
|
||||
export { default as App, type IApp } from '$lib/models/app';
|
||||
export { default as Engine, type IEngine } from '$lib/models/engine';
|
||||
export { type IMcpServer, default as McpServer } from '$lib/models/mcp-server';
|
||||
export { type IMessage, default as Message } from '$lib/models/message';
|
||||
export { type IModel, default as Model } from '$lib/models/model.svelte';
|
||||
export { type IModel, default as Model } from '$lib/models/model';
|
||||
export { type ISession, default as Session } from '$lib/models/session';
|
||||
export { type ISetting, default as Setting } from '$lib/models/setting';
|
||||
|
||||
@@ -19,6 +20,6 @@ export async function resync() {
|
||||
await Message.sync();
|
||||
await McpServer.sync();
|
||||
await Setting.sync();
|
||||
await Model.sync();
|
||||
await Engine.sync();
|
||||
info('[green]✔ resynced');
|
||||
}
|
||||
@@ -35,7 +35,7 @@ interface Metadata {
|
||||
|
||||
export default class McpServer extends Model<IMcpServer, Row>('mcp_servers') {
|
||||
static defaults = {
|
||||
name: 'Unknown',
|
||||
name: 'Installing...',
|
||||
command: '',
|
||||
metadata: {
|
||||
protocolVersion: '',
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import moment from "moment";
|
||||
|
||||
import type { LlmMessageRole } from '$lib/llm.d';
|
||||
import type { Role, ToolCall } from "$lib/engines/types";
|
||||
import Model, { type ToSqlRow } from '$lib/models/base.svelte';
|
||||
import Session, { type ISession } from "$lib/models/session";
|
||||
|
||||
export interface IMessage {
|
||||
id?: number;
|
||||
role: LlmMessageRole;
|
||||
role: Role;
|
||||
content: string;
|
||||
thought?: string;
|
||||
model: string;
|
||||
name: string;
|
||||
toolCalls: Record<string, any>[]; // eslint-disable-line
|
||||
toolCalls: ToolCall[];
|
||||
sessionId?: number;
|
||||
responseId?: number;
|
||||
created?: moment.Moment;
|
||||
@@ -54,7 +54,7 @@ export default class Message extends Model<IMessage, Row>('messages') {
|
||||
protected static async fromSql(row: Row): Promise<IMessage> {
|
||||
return {
|
||||
id: row.id,
|
||||
role: row.role as LlmMessageRole,
|
||||
role: row.role as Role,
|
||||
content: row.content,
|
||||
thought: row.thought,
|
||||
model: row.model,
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { OllamaClient, type OllamaModel } from "$lib/llm";
|
||||
|
||||
export type IModel = OllamaModel;
|
||||
|
||||
export interface Details {
|
||||
parentModel: string;
|
||||
format: string;
|
||||
family: string;
|
||||
families: string[];
|
||||
parameter_size: string;
|
||||
quantization_level: string;
|
||||
}
|
||||
|
||||
let repo: IModel[] = $state([]);
|
||||
|
||||
export default class Model {
|
||||
static async sync(): Promise<void> {
|
||||
const client = new OllamaClient();
|
||||
const models: IModel[] = await client.list();
|
||||
|
||||
repo = await Promise.all(
|
||||
models.map(async m => {
|
||||
const model = await client.info(m.name);
|
||||
return { ...m, ...model }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
static default(): IModel {
|
||||
return repo[0];
|
||||
}
|
||||
|
||||
static find(name: string): IModel {
|
||||
return repo.find(m => m.name == name) as IModel;
|
||||
}
|
||||
|
||||
static exists(name: string): boolean {
|
||||
return this.find(name) !== undefined;
|
||||
}
|
||||
|
||||
static first(): IModel {
|
||||
return repo[0];
|
||||
}
|
||||
|
||||
static last(): IModel {
|
||||
return repo[repo.length - 1];
|
||||
}
|
||||
|
||||
static all(): IModel[] {
|
||||
return repo;
|
||||
}
|
||||
|
||||
static supportsTools(model: IModel): boolean {
|
||||
return model && model.capabilities?.includes('tools') == true;
|
||||
}
|
||||
}
|
||||
21
src/lib/models/model.ts
Normal file
21
src/lib/models/model.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { BareModel } from "$lib/models/base.svelte";
|
||||
import Engine from "$lib/models/engine";
|
||||
|
||||
export interface IModel {
|
||||
id: string;
|
||||
name: string;
|
||||
supportsTools: boolean;
|
||||
metadata: {
|
||||
[key: string]: any; // eslint-disable-line
|
||||
}
|
||||
}
|
||||
|
||||
export default class Model extends BareModel<IModel>() {
|
||||
static default(): IModel {
|
||||
return Engine.first().models[0];
|
||||
}
|
||||
|
||||
static findOrDefault(id: string): IModel {
|
||||
return this.find(id) || this.default();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import moment from "moment";
|
||||
|
||||
import { type LlmTool, OllamaClient } from "$lib/llm";
|
||||
import Engine from "./engine";
|
||||
|
||||
import type { Tool } from "$lib/engines/types";
|
||||
import { getMCPTools } from '$lib/mcp';
|
||||
import App, { type IApp } from '$lib/models/app';
|
||||
import Base, { type ToSqlRow } from '$lib/models/base.svelte';
|
||||
import { type IMcpServer } from "$lib/models/mcp-server";
|
||||
import type { IMcpServer } from "$lib/models/mcp-server";
|
||||
import Message, { type IMessage } from "$lib/models/message";
|
||||
import Model from '$lib/models/model.svelte';
|
||||
import Model from '$lib/models/model';
|
||||
|
||||
export const DEFAULT_SUMMARY = 'Untitled';
|
||||
export interface ISession {
|
||||
@@ -36,7 +38,7 @@ export default class Session extends Base<ISession, Row>('sessions') {
|
||||
static defaults = () => ({
|
||||
summary: DEFAULT_SUMMARY,
|
||||
config: {
|
||||
model: Model.default().name,
|
||||
model: Model.default().id,
|
||||
contextWindow: 4096,
|
||||
temperature: 0.8,
|
||||
enabledMcpServers: [],
|
||||
@@ -53,10 +55,8 @@ export default class Session extends Base<ISession, Row>('sessions') {
|
||||
return Message.where({ sessionId: session.id });
|
||||
}
|
||||
|
||||
static async tools(session: ISession): Promise<LlmTool[]> {
|
||||
const model = Model.find(session.config.model);
|
||||
|
||||
if (!Model.supportsTools(model)) {
|
||||
static async tools(session: ISession): Promise<Tool[]> {
|
||||
if (!Model.find(session.config.model)?.supportsTools) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ export default class Session extends Base<ISession, Row>('sessions') {
|
||||
return await this.update(session);
|
||||
}
|
||||
|
||||
static async summarize(session: ISession, model: string) {
|
||||
static async summarize(session: ISession, modelId: string) {
|
||||
if (!session.id) {
|
||||
return;
|
||||
}
|
||||
@@ -101,9 +101,15 @@ export default class Session extends Base<ISession, Row>('sessions') {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = new OllamaClient();
|
||||
const message: IMessage = await client.chat(
|
||||
model,
|
||||
const engine = Engine.fromModelId(modelId);
|
||||
const model = Model.find(modelId);
|
||||
|
||||
if (!engine || !model) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message: IMessage = await engine.client.chat(
|
||||
model.name,
|
||||
[
|
||||
...this.messages(session),
|
||||
{
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { OllamaClient } from '$lib/llm';
|
||||
import Ollama from '$lib/engines/ollama';
|
||||
import Model, { type ToSqlRow } from '$lib/models/base.svelte';
|
||||
import LLMModel from '$lib/models/model.svelte';
|
||||
import Engine from '$lib/models/engine';
|
||||
|
||||
// OpenAI API Key
|
||||
const OPENAI_API_KEY = 'openai-api-key';
|
||||
|
||||
// Ollama URL
|
||||
export const OLLAMA_URL_CONFIG_KEY = 'ollama-url';
|
||||
@@ -20,23 +23,25 @@ interface Row {
|
||||
}
|
||||
|
||||
export default class Setting extends Model<ISetting, Row>('settings') {
|
||||
static get OllamaUrl(): string {
|
||||
return this.findBy({ key: OLLAMA_URL_CONFIG_KEY }).value as string;
|
||||
static get OllamaUrl(): string | undefined {
|
||||
return this.findBy({ key: OLLAMA_URL_CONFIG_KEY })?.value as string;
|
||||
}
|
||||
|
||||
static get OpenAIKey(): string | undefined {
|
||||
return this.findBy({ key: OPENAI_API_KEY })?.value as string;
|
||||
}
|
||||
|
||||
static async validate(setting: ISetting): Promise<boolean> {
|
||||
if (setting.key == OLLAMA_URL_CONFIG_KEY) {
|
||||
const client = new OllamaClient({ url: setting.value as string })
|
||||
const client = new Ollama(setting.value as string);
|
||||
return await client.connected();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static async afterUpdate(setting: ISetting): Promise<ISetting> {
|
||||
if (setting.key == OLLAMA_URL_CONFIG_KEY) {
|
||||
await LLMModel.sync();
|
||||
}
|
||||
|
||||
// Resync models in case a Provider key/url was updated.
|
||||
await Engine.sync();
|
||||
return setting;
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from '$lib/smithery.d';
|
||||
@@ -1,7 +1,5 @@
|
||||
import { HttpClient } from "$lib/http";
|
||||
import type { CompactServer, Server, ServerList } from '$lib/smithery.d';
|
||||
|
||||
export * from '$lib/smithery.d';
|
||||
import type { CompactServer, Server, ServerList } from '$lib/smithery/types';
|
||||
|
||||
export class Client extends HttpClient {
|
||||
options: RequestInit = {
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
import ts from 'typescript';
|
||||
|
||||
import type { Server } from '$lib/smithery';
|
||||
|
||||
const IS_CONFIG = /^(\s*config\.)(.+)$/;
|
||||
|
||||
// A Node is an object that knows how to be evaluated using a `config` object
|
||||
// which may contain values to be interpolated.
|
||||
//
|
||||
abstract class Node {
|
||||
text: string;
|
||||
abstract eval(config: Record<string, string>): string;
|
||||
|
||||
constructor(text: string) {
|
||||
this.text = text.trim().replaceAll("'", '');
|
||||
}
|
||||
}
|
||||
|
||||
// No interpolation, just a regular string.
|
||||
//
|
||||
// `text` is the value.
|
||||
//
|
||||
class StringNode extends Node {
|
||||
eval(_: Record<string, string>) {
|
||||
return this.text;
|
||||
}
|
||||
}
|
||||
|
||||
// Gets its value from the config object passed in.
|
||||
//
|
||||
// `text` is the name of the key in `config`.
|
||||
//
|
||||
class ConfigNode extends Node {
|
||||
eval(config: Record<string, string>): string {
|
||||
return config[this.text];
|
||||
}
|
||||
}
|
||||
|
||||
// Parent object that wraps a sets of `Node`s and can give us the final
|
||||
// `Config` using those `Node`s.
|
||||
//
|
||||
export class Config {
|
||||
command: StringNode;
|
||||
args: Node[];
|
||||
env: Record<string, Node>;
|
||||
|
||||
static from(server: Server) {
|
||||
return parse(server);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.command = new StringNode('');
|
||||
this.args = [];
|
||||
this.env = {};
|
||||
}
|
||||
|
||||
eval(config: Record<string, string>) {
|
||||
return {
|
||||
command: this.command.eval(config),
|
||||
args: this.args.map(a => a.eval(config)),
|
||||
env: Object.fromEntries(
|
||||
Object.entries(this.env).map(([k, v]) => [k, v.eval(config)])
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse out the start command of an MCP server, from Smithery's configuration
|
||||
// file syntax.
|
||||
//
|
||||
// Smithery's JSON schema for MCP servers involves specifying a JS function
|
||||
// that evaluates into the final config object used to start the server
|
||||
// process.
|
||||
//
|
||||
// ```
|
||||
// startCommand:
|
||||
// ...
|
||||
// commandFunction:
|
||||
// |-
|
||||
// (config) => ({
|
||||
// "command" => "npx",
|
||||
// "args" => ["an-mcp-server", "--apiKey", config.apiKey],
|
||||
// "env": {
|
||||
// OTHER_THING: config.otherThing
|
||||
// }
|
||||
// })
|
||||
// ```
|
||||
//
|
||||
// This seems like a supremely bad idea, since it means `eval`ing arbitrary,
|
||||
// user-supplied, code. Likely on a server, since it's the parts necessary to
|
||||
// spawn a process. I could totally be missing something, though. so :shrug:
|
||||
//
|
||||
// In any case, we're not going to `eval` anything. Instead, we build an AST of
|
||||
// this code and parse out the nodes we care about. We build a `Config` object
|
||||
// that will then be "evaluated" later, when we have the values we need to
|
||||
// interpolate.
|
||||
//
|
||||
// This approach avoids any dangers of executing arbitrary code.
|
||||
//
|
||||
function parse(server: Server): Config {
|
||||
const stdio = server.connections.find(c => c.type == 'stdio');
|
||||
const cmd = stdio?.stdioFunction || '';
|
||||
const config: Config = new Config();
|
||||
|
||||
// Build an AST from the function declaration
|
||||
const source = ts.createSourceFile(
|
||||
"source.ts",
|
||||
cmd,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
);
|
||||
|
||||
// Relevant tokens to eval
|
||||
const tokens: string[] = [];
|
||||
|
||||
// Take the next token unconditionally.
|
||||
let take = false;
|
||||
|
||||
// Collect the tokens we care about from the AST
|
||||
visit(source);
|
||||
|
||||
// For each token, transform them into `Node`s.
|
||||
tokens.forEach((token, i) => {
|
||||
if (token === 'command') {
|
||||
config.command = command(tokens[i + 1]);
|
||||
} else if (token === 'args') {
|
||||
config.args = args(tokens[i + 1]);
|
||||
} else if (token === 'env') {
|
||||
config.env = env(tokens[i + 1]);
|
||||
}
|
||||
});
|
||||
|
||||
return config;
|
||||
|
||||
// Visit a Typescript AST node and track relevant ones.
|
||||
//
|
||||
// We only care about two types of nodes – `Identifier` (that has a text
|
||||
// value of "command", "args", or "env") and next node.
|
||||
//
|
||||
function visit(node: ts.Node) {
|
||||
// Previous token was an identifier that we care about. If that's the
|
||||
// case, also track this node as it's the value of one of those objects
|
||||
if (take) {
|
||||
tokens.push(node.getText());
|
||||
}
|
||||
|
||||
// Reset this so we only record N(Identifier)+1 nodes.
|
||||
take = false;
|
||||
|
||||
// If we care about this, track it.
|
||||
if (node.kind == ts.SyntaxKind.Identifier && isNeeded(node.getText())) {
|
||||
tokens.push(node.getText());
|
||||
take = true;
|
||||
}
|
||||
|
||||
// Keep on truck'n.
|
||||
ts.forEachChild(node, (c) => visit(c));
|
||||
}
|
||||
|
||||
// Command is always just a plain string.
|
||||
//
|
||||
function command(text: string) {
|
||||
return new StringNode(text);
|
||||
}
|
||||
|
||||
// Parse the list of `args` into a list of `Node`s.
|
||||
//
|
||||
// An `arg` can either just be a plain string – "--verbose" – which will end
|
||||
// up as a `StringNode`.
|
||||
//
|
||||
// Or it can be a `config` variable – "config.apiKey" – which will end up
|
||||
// as a `ConfigNode` and be evaluated later for the final value.
|
||||
//
|
||||
function args(text: string): Node[] {
|
||||
text = text.replace('[', '').replace(']', '');
|
||||
|
||||
if (text == '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return text
|
||||
.split(',')
|
||||
.map(a => (
|
||||
IS_CONFIG.test(a)
|
||||
? new ConfigNode(a.trim().replace('config.', ''))
|
||||
: new StringNode(a)
|
||||
))
|
||||
.compact() as Node[];
|
||||
}
|
||||
|
||||
// Parse the list of `env` vars into a list of `Node`s.
|
||||
//
|
||||
// `env` entries will always have a plain string key, but can have a string
|
||||
// or config value.
|
||||
//
|
||||
// Does the same as `args` and tracks the corresponding `Node` to be
|
||||
// evaluated later.
|
||||
//
|
||||
function env(text: string): Record<string, Node> {
|
||||
text = text.replace('{', '').replace('}', '');
|
||||
|
||||
if (text === '') {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
text
|
||||
.split(',')
|
||||
.map(kv => {
|
||||
let [k, v] = kv.split(':'); // eslint-disable-line
|
||||
let value: Node = new StringNode(v);
|
||||
|
||||
if (IS_CONFIG.test(v)) {
|
||||
value = new ConfigNode(v.replace('config.', ''));
|
||||
}
|
||||
|
||||
return [k, value];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// We only care about these three keys
|
||||
//
|
||||
function isNeeded(text: string) {
|
||||
return ['command', 'args', 'env'].includes(text);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import jsonSchema from 'json-schema';
|
||||
const { JSONSchema7 } = jsonSchema;
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export interface ServerList {
|
||||
servers: CompactServer[];
|
||||
@@ -52,7 +51,7 @@ export interface ConfigSchema {
|
||||
properties: {
|
||||
[key: string]: {
|
||||
type: 'string';
|
||||
default?: any; // eslint-disable-line
|
||||
default?: any;
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
@@ -65,3 +64,4 @@ export interface Config {
|
||||
value: string;
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
@@ -10,25 +10,26 @@
|
||||
import Layout from '$components/Layouts/Default.svelte';
|
||||
import Link from '$components/Link.svelte';
|
||||
import Menu, { type MenuItem } from '$components/Menu.svelte';
|
||||
import Select from '$components/Select.svelte';
|
||||
import ModelMenu from '$components/ModelMenu.svelte';
|
||||
import Svg from '$components/Svg.svelte';
|
||||
import Toggle from '$components/Toggle.svelte';
|
||||
import Engine, { type IEngine } from '$lib/models/engine';
|
||||
import McpServer, { type IMcpServer } from '$lib/models/mcp-server';
|
||||
import Message from '$lib/models/message';
|
||||
import Model, { type IModel } from '$lib/models/model.svelte';
|
||||
import Model, { type IModel } from '$lib/models/model';
|
||||
import Session, { type ISession } from '$lib/models/session';
|
||||
|
||||
const session: ISession = $derived(Session.find(page.params.session_id));
|
||||
const model: IModel = $derived(Model.find(session.config.model));
|
||||
const model: IModel = $derived(Model.findOrDefault(session.config.model));
|
||||
|
||||
const sessions: ISession[] = $derived(Session.all());
|
||||
const mcpServers: IMcpServer[] = $derived(McpServer.all());
|
||||
const models: IModel[] = $derived(Model.all());
|
||||
const engines: IEngine[] = $derived(Engine.all());
|
||||
|
||||
let advancedIsOpen = $state(false);
|
||||
|
||||
async function modelDidUpdate() {
|
||||
// `session.config.model` has been updated, so save.
|
||||
async function modelDidUpdate(model: IModel) {
|
||||
session.config.model = model.id;
|
||||
await Session.update(session);
|
||||
}
|
||||
|
||||
@@ -91,7 +92,7 @@
|
||||
});
|
||||
|
||||
afterNavigate(async () => {
|
||||
if (model && Model.supportsTools(model)) {
|
||||
if (model.supportsTools) {
|
||||
await startMcpServers(session);
|
||||
}
|
||||
});
|
||||
@@ -137,20 +138,21 @@
|
||||
{#if session}
|
||||
<Flex class="bg-medium h-full w-[calc(100%-600px)] grow items-start">
|
||||
{#key session.id}
|
||||
<Chat {session} model={session.config.model} />
|
||||
<Chat {session} {model} />
|
||||
{/key}
|
||||
</Flex>
|
||||
|
||||
<Flex class="bg-medium border-light h-full w-[300px] flex-col items-start border-l p-4">
|
||||
<Select
|
||||
onSelect={modelDidUpdate}
|
||||
bind:value={session.config.model}
|
||||
class="text-light z-50 mb-8 w-full"
|
||||
options={models.map((m) => m.name).sort()}
|
||||
/>
|
||||
{#key session.config.model}
|
||||
<ModelMenu
|
||||
{engines}
|
||||
bind:value={session.config.model}
|
||||
onselect={modelDidUpdate}
|
||||
/>
|
||||
{/key}
|
||||
|
||||
{#if model && !Model.supportsTools(model)}
|
||||
<Flex class="text-red mb-8 w-full justify-start gap-2 pl-3">
|
||||
{#if model && !model.supportsTools}
|
||||
<Flex class="text-red w-full justify-start gap-2 pl-3">
|
||||
<Svg class="h-6 w-6" name="Warning" />
|
||||
Model doesn't support MCP
|
||||
</Flex>
|
||||
@@ -163,20 +165,22 @@
|
||||
</Flex>
|
||||
{/if}
|
||||
|
||||
{#each mcpServers as server (server.id)}
|
||||
<Flex class="text-light z-0 mb-4 ml-2">
|
||||
<Toggle
|
||||
label={server.name}
|
||||
value={Session.hasMcpServer(session, server.name) &&
|
||||
Model.supportsTools(model)
|
||||
? 'on'
|
||||
: 'off'}
|
||||
disabled={!Model.supportsTools(model)}
|
||||
onEnable={() => startMcpServer(server)}
|
||||
onDisable={() => stopMcpServer(server)}
|
||||
/>
|
||||
</Flex>
|
||||
{/each}
|
||||
<div class="mt-4">
|
||||
{#each mcpServers as server (server.id)}
|
||||
<Flex class="text-light z-0 mb-4 ml-2">
|
||||
<Toggle
|
||||
label={server.name}
|
||||
value={Session.hasMcpServer(session, server.name) &&
|
||||
model.supportsTools
|
||||
? 'on'
|
||||
: 'off'}
|
||||
disabled={!model.supportsTools}
|
||||
onEnable={() => startMcpServer(server)}
|
||||
onDisable={() => stopMcpServer(server)}
|
||||
/>
|
||||
</Flex>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<Flex class="mt-8 w-full flex-col items-start">
|
||||
<button
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
import type { CompactServer } from '$lib/smithery';
|
||||
import { Client } from '$lib/smithery/client';
|
||||
import type { CompactServer } from '$lib/smithery/types';
|
||||
|
||||
interface Response {
|
||||
servers: CompactServer[];
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import Box from '$components/Box.svelte';
|
||||
import Flex from '$components/Flex.svelte';
|
||||
import Layout from '$components/Layouts/Default.svelte';
|
||||
import Model, { type IModel } from '$lib/models/model.svelte';
|
||||
import Model, { type IModel } from '$lib/models/model';
|
||||
|
||||
const models: IModel[] = Model.all().sortBy('name');
|
||||
</script>
|
||||
@@ -32,9 +32,9 @@
|
||||
<Box class="border-b-light text-light w-full border-b px-8 py-4">
|
||||
<h2 class="mr-2">{model.name}</h2>
|
||||
<div class="grow"></div>
|
||||
{@render label(model.details.format, 'bg-[#7aa2f7]')}
|
||||
{@render label(model.details.parameter_size, 'bg-[#ff9e64]')}
|
||||
{@render label(model.details.quantization_level, 'bg-[#41a6b5]')}
|
||||
{@render label(model.metadata.details.format, 'bg-[#7aa2f7]')}
|
||||
{@render label(model.metadata.details.parameter_size, 'bg-[#ff9e64]')}
|
||||
{@render label(model.metadata.details.quantization_level, 'bg-[#41a6b5]')}
|
||||
</Box>
|
||||
{/each}
|
||||
</Flex>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
import Model from "$lib/models/model.svelte";
|
||||
import { Engine } from "$lib/models";
|
||||
|
||||
export const load: PageLoad = async (): Promise<void> => {
|
||||
await Model.sync();
|
||||
await Engine.sync();
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"resolveJsonModule": true,
|
||||
"inlineSourceMap": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user