Compare commits
671 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
201a987ff9 | ||
![]() |
2d8125042a | ||
![]() |
776e7bb5e4 | ||
![]() |
b8d7ca1a7b | ||
![]() |
2bed62926e | ||
![]() |
aad8d128a0 | ||
![]() |
ec1acbb867 | ||
![]() |
e4859c4563 | ||
![]() |
8e30eb26bd | ||
![]() |
0b5c589ca2 | ||
![]() |
65fadddc85 | ||
![]() |
ed5fb088c4 | ||
![]() |
f81f308118 | ||
![]() |
b1390a7b37 | ||
![]() |
11d83386a5 | ||
![]() |
bb31def011 | ||
![]() |
41e03ede95 | ||
![]() |
7fea1ecdf6 | ||
![]() |
054894271d | ||
![]() |
6fef042f0b | ||
![]() |
5c0c2d1d09 | ||
![]() |
37f9c8ad99 | ||
![]() |
2a80f55e2a | ||
![]() |
421c878a2d | ||
![]() |
36666c2142 | ||
![]() |
85801317d1 | ||
![]() |
2ed0d65948 | ||
![]() |
d459dc4ad1 | ||
![]() |
40bc4622ef | ||
![]() |
c0f818a07a | ||
![]() |
8671fdeda6 | ||
![]() |
2619850fb4 | ||
![]() |
8feb97dc0d | ||
![]() |
4e1ff6dcbb | ||
![]() |
8589d752ac | ||
![]() |
de4ded68b0 | ||
![]() |
9b5a3c5991 | ||
![]() |
00b0699c75 | ||
![]() |
993cf8bf55 | ||
![]() |
7bb7cb8a60 | ||
![]() |
b123be5b71 | ||
![]() |
ddf5c09a9b | ||
![]() |
5f73c08729 | ||
![]() |
f503a848c2 | ||
![]() |
36a6daccab | ||
![]() |
ceb0e26e5e | ||
![]() |
284e02bed0 | ||
![]() |
3450a57d4a | ||
![]() |
592dae31c8 | ||
![]() |
2010cbc5fa | ||
![]() |
ac0801eced | ||
![]() |
ad66e5b060 | ||
![]() |
ade4b55520 | ||
![]() |
a6d62e0617 | ||
![]() |
6e76348df7 | ||
![]() |
0d6687f84c | ||
![]() |
74d2a9ef9a | ||
![]() |
14476d48cc | ||
![]() |
ce8ce82567 | ||
![]() |
4dc4f1be34 | ||
![]() |
16b52331a4 | ||
![]() |
5445aaa94e | ||
![]() |
2ac3dd6853 | ||
![]() |
d8851cb7a0 | ||
![]() |
058f6cd2cc | ||
![]() |
790cf34d17 | ||
![]() |
928d844896 | ||
![]() |
939d6a8606 | ||
![]() |
58888a74bc | ||
![]() |
cc5a71e0e3 | ||
![]() |
e83bcf7f9a | ||
![]() |
5690e5ce99 | ||
![]() |
f2ea8470e5 | ||
![]() |
34b9db5afc | ||
![]() |
8711d03df7 | ||
![]() |
ee448deaba | ||
![]() |
6e8db04716 | ||
![]() |
658e60cf73 | ||
![]() |
4c78f028f8 | ||
![]() |
435cc866a3 | ||
![]() |
c7d3a558f6 | ||
![]() |
089cdb2877 | ||
![]() |
ea1e9aa36b | ||
![]() |
d0d28ef90d | ||
![]() |
6654186a7c | ||
![]() |
aa72281eae | ||
![]() |
74bcbf828f | ||
![]() |
fe39147e64 | ||
![]() |
fad00a85e5 | ||
![]() |
9c0db4cc83 | ||
![]() |
62be2050dd | ||
![]() |
56f8aa6912 | ||
![]() |
e6f9bfc0e8 | ||
![]() |
6f18297b3a | ||
![]() |
15016413de | ||
![]() |
440b7190ed | ||
![]() |
8d1995c625 | ||
![]() |
fd01fbf038 | ||
![]() |
0408205c1c | ||
![]() |
63a7edd771 | ||
![]() |
554ffdcce3 | ||
![]() |
9850a4ce08 | ||
![]() |
3934c15895 | ||
![]() |
fd048f1367 | ||
![]() |
8645076a71 | ||
![]() |
05e9424824 | ||
![]() |
52ebe67a98 | ||
![]() |
889b31ab78 | ||
![]() |
3cf483fe48 | ||
![]() |
8dca03173d | ||
![]() |
85bdf14b56 | ||
![]() |
d524e5ef5e | ||
![]() |
52f5370c48 | ||
![]() |
da8a0c7657 | ||
![]() |
1b42b4b59a | ||
![]() |
7c000ec3ed | ||
![]() |
c8afe7168c | ||
![]() |
28d3cd0148 | ||
![]() |
eb5554232a | ||
![]() |
ea4c284a48 | ||
![]() |
2bdc320216 | ||
![]() |
32561aed09 | ||
![]() |
71548d9829 | ||
![]() |
8aec92fa6d | ||
![]() |
a8b9b930b4 | ||
![]() |
9755cf9173 | ||
![]() |
70261b9bb6 | ||
![]() |
9df6c85c3a | ||
![]() |
e74163af4c | ||
![]() |
fb9580df85 | ||
![]() |
26df674785 | ||
![]() |
7c9792a6e0 | ||
![]() |
7afb2e125a | ||
![]() |
41a272de9f | ||
![]() |
f335722275 | ||
![]() |
6d53b67c2c | ||
![]() |
969238b19e | ||
![]() |
949d7832cf | ||
![]() |
99d227c9db | ||
![]() |
a27e419b47 | ||
![]() |
e4d0db5a97 | ||
![]() |
ba460802c2 | ||
![]() |
e54a3c7fcd | ||
![]() |
9f8691c6c8 | ||
![]() |
a0b8a32eb4 | ||
![]() |
7027f264fb | ||
![]() |
9bee3b63b1 | ||
![]() |
309aef7fee | ||
![]() |
08655170aa | ||
![]() |
2b341069a7 | ||
![]() |
c00fee6936 | ||
![]() |
c2d813bdc3 | ||
![]() |
786f3a1c44 | ||
![]() |
3397eff0cd | ||
![]() |
0efb7931c7 | ||
![]() |
42f2cc408e | ||
![]() |
9446b795b5 | ||
![]() |
62f8cda3b3 | ||
![]() |
6a1de23175 | ||
![]() |
a7b431e743 | ||
![]() |
5a25f93522 | ||
![]() |
7e33a017c0 | ||
![]() |
8b2c10061c | ||
![]() |
c5c451ca3b | ||
![]() |
2b4ca6cf36 | ||
![]() |
ad90b9ab3d | ||
![]() |
4340f8eba4 | ||
![]() |
4c7db6b7e9 | ||
![]() |
c03f0e3c3d | ||
![]() |
c5ff443b9f | ||
![]() |
01114b4526 | ||
![]() |
1524f323a3 | ||
![]() |
fccf3eecaa | ||
![]() |
c77d45d836 | ||
![]() |
5ec12cec6c | ||
![]() |
d9578d2bad | ||
![]() |
cb8352d6b4 | ||
![]() |
fc6558f47f | ||
![]() |
9502e5661f | ||
![]() |
e1c9a2a00f | ||
![]() |
1341ee1b56 | ||
![]() |
63efa075a0 | ||
![]() |
cb03fc9571 | ||
![]() |
a5ec9cfc0f | ||
![]() |
be517e491c | ||
![]() |
fc8e108642 | ||
![]() |
c5d5c4a96c | ||
![]() |
dfe330fa1c | ||
![]() |
01f77ae25d | ||
![]() |
483b81a863 | ||
![]() |
36bd967722 | ||
![]() |
b0e7d35db8 | ||
![]() |
aeb1fb5192 | ||
![]() |
a2e60ebcaf | ||
![]() |
883ec4d1ef | ||
![]() |
4de0126719 | ||
![]() |
9768e2dc75 | ||
![]() |
08600d5bec | ||
![]() |
a624e672d2 | ||
![]() |
e4a7e5b2ca | ||
![]() |
a0a15cfd5b | ||
![]() |
12e923e158 | ||
![]() |
cd135317d2 | ||
![]() |
4f895d633f | ||
![]() |
7d05a6ee8f | ||
![]() |
464d817824 | ||
![]() |
531324a9be | ||
![]() |
6589eb8a8c | ||
![]() |
90f071c658 | ||
![]() |
a039e383cd | ||
![]() |
80163ebcb5 | ||
![]() |
a57818d93e | ||
![]() |
841adda157 | ||
![]() |
0035e31af8 | ||
![]() |
c863c6a96d | ||
![]() |
1f11b52511 | ||
![]() |
526d4eb204 | ||
![]() |
0a74cb31d5 | ||
![]() |
10ed1b6292 | ||
![]() |
4fec5816d6 | ||
![]() |
0a0e9f3e0f | ||
![]() |
58d95cc9bd | ||
![]() |
3b6a9154dd | ||
![]() |
d6dd2ff839 | ||
![]() |
e57a6ba89f | ||
![]() |
12ec2346ef | ||
![]() |
1ec0df1069 | ||
![]() |
91b3e4d282 | ||
![]() |
d338d70492 | ||
![]() |
011bb67351 | ||
![]() |
d124627202 | ||
![]() |
b0a8246a69 | ||
![]() |
e6fb39c182 | ||
![]() |
e1f1c374ea | ||
![]() |
06a1508bfe | ||
![]() |
5a5efee46b | ||
![]() |
97ae517fbf | ||
![]() |
44b813e459 | ||
![]() |
539043f5e0 | ||
![]() |
dbcace6847 | ||
![]() |
c91a4ebcff | ||
![]() |
b79c7e4528 | ||
![]() |
035b274b70 | ||
![]() |
9c6a254945 | ||
![]() |
f31f2bedf4 | ||
![]() |
756c257553 | ||
![]() |
5255d0af8a | ||
![]() |
af8a8a6b59 | ||
![]() |
461ad25015 | ||
![]() |
8838ae787d | ||
![]() |
db75402ade | ||
![]() |
1e85a140a3 | ||
![]() |
c363282fdc | ||
![]() |
5b0c48d29e | ||
![]() |
913306f4fd | ||
![]() |
f5ca7f8c8e | ||
![]() |
856b8ec131 | ||
![]() |
1b272d5bcd | ||
![]() |
29715dbca7 | ||
![]() |
54a028d07f | ||
![]() |
f83e4db365 | ||
![]() |
3b5866a233 | ||
![]() |
b8c2be6142 | ||
![]() |
e0319bd78d | ||
![]() |
b31ed7f031 | ||
![]() |
5dacc1ebe8 | ||
![]() |
c2712b5566 | ||
![]() |
8091ef2eeb | ||
![]() |
f38b705dc7 | ||
![]() |
560be5e0b6 | ||
![]() |
4a1c76b3aa | ||
![]() |
28a64e23ca | ||
![]() |
92d74e2f59 | ||
![]() |
6f8f57dd1d | ||
![]() |
b2fa68b0ea | ||
![]() |
3767d5ef0d | ||
![]() |
9fed85bc8b | ||
![]() |
4501bc0913 | ||
![]() |
57ba519e63 | ||
![]() |
d98d322d24 | ||
![]() |
0c3ec74cf1 | ||
![]() |
42ae8359fa | ||
![]() |
e4b76dfb76 | ||
![]() |
2c56517494 | ||
![]() |
cfbc1b152b | ||
![]() |
9305ac1b2e | ||
![]() |
45d6292959 | ||
![]() |
22921a3969 | ||
![]() |
7b6cbc10ec | ||
![]() |
dfc6721b20 | ||
![]() |
acfa2b9422 | ||
![]() |
2c390a73ac | ||
![]() |
3e30c75f3e | ||
![]() |
7e430ff352 | ||
![]() |
1784113ef5 | ||
![]() |
949b6c01e0 | ||
![]() |
38daf0a252 | ||
![]() |
43799532c1 | ||
![]() |
d8fdbfd8da | ||
![]() |
a5ba0fcf78 | ||
![]() |
3a30bf56dc | ||
![]() |
a1c0a48524 | ||
![]() |
74788b487c | ||
![]() |
7ed3e94105 | ||
![]() |
2297ad39da | ||
![]() |
01cff6136d | ||
![]() |
3c4ad0ecab | ||
![]() |
22f326464e | ||
![]() |
e95ffc7448 | ||
![]() |
2dce1ab40b | ||
![]() |
f4b31c2d53 | ||
![]() |
ab3456207b | ||
![]() |
6ad414f31e | ||
![]() |
052b5a3b77 | ||
![]() |
d4c10df2b0 | ||
![]() |
540f4af45f | ||
![]() |
6ce37e4d96 | ||
![]() |
703684a82a | ||
![]() |
6459377ae0 | ||
![]() |
8546dd3d72 | ||
![]() |
87100be5e0 | ||
![]() |
e87c780ff9 | ||
![]() |
291c663865 | ||
![]() |
da20786e3e | ||
![]() |
5ce997a7b9 | ||
![]() |
672ffe9b7d | ||
![]() |
47cfe58af5 | ||
![]() |
c1a81c6fe3 | ||
![]() |
152ab524c2 | ||
![]() |
e72c567cfd | ||
![]() |
3e22611200 | ||
![]() |
a54d4a28dc | ||
![]() |
82b0c7c27e | ||
![]() |
ba7cf7fb66 | ||
![]() |
2f804068bd | ||
![]() |
85129d3a32 | ||
![]() |
9ac6440da3 | ||
![]() |
0085297928 | ||
![]() |
34d00f90b1 | ||
![]() |
b53229a2ed | ||
![]() |
53c107e20e | ||
![]() |
51578d8573 | ||
![]() |
b5fcd9d3aa | ||
![]() |
b80661e8c7 | ||
![]() |
6d3adfbea2 | ||
![]() |
369eda65f5 | ||
![]() |
f878e91070 | ||
![]() |
0d651478e4 | ||
![]() |
9ea492f1ce | ||
![]() |
bc13da2bfe | ||
![]() |
41b00b9856 | ||
![]() |
c2a8ed48e7 | ||
![]() |
3dc1bb6a35 | ||
![]() |
7865a6996a | ||
![]() |
00ec269321 | ||
![]() |
908005d90b | ||
![]() |
cdf65e793f | ||
![]() |
82ca694d68 | ||
![]() |
5017a15bcb | ||
![]() |
e11668aa07 | ||
![]() |
0bd0f4a29c | ||
![]() |
1ffb1e2874 | ||
![]() |
0a7844413c | ||
![]() |
f9cd55c70b | ||
![]() |
0fdebb34a9 | ||
![]() |
ac64cd4ef9 | ||
![]() |
4a5c9b8035 | ||
![]() |
efe5617b64 | ||
![]() |
5b3fad9636 | ||
![]() |
bfec2c6e10 | ||
![]() |
5c143af726 | ||
![]() |
6c0af2599e | ||
![]() |
fc8c044584 | ||
![]() |
ecc133d843 | ||
![]() |
76bdebbadf | ||
![]() |
18979ad4a1 | ||
![]() |
8e0ef931d8 | ||
![]() |
280da44522 | ||
![]() |
0cebc79cba | ||
![]() |
0e4669b04f | ||
![]() |
b886bec3f9 | ||
![]() |
fc06205971 | ||
![]() |
2ada81e068 | ||
![]() |
b1e74d4fda | ||
![]() |
f678f5c5c3 | ||
![]() |
2cb74e23fb | ||
![]() |
69f0227813 | ||
![]() |
3c8df3808b | ||
![]() |
7d564835c2 | ||
![]() |
72431031d9 | ||
![]() |
6041abb5b2 | ||
![]() |
6c5ccb11f9 | ||
![]() |
2e20110e50 | ||
![]() |
82ddc3e441 | ||
![]() |
d481fb3cc8 | ||
![]() |
23ee633252 | ||
![]() |
23ebe8fe11 | ||
![]() |
2c017ca441 | ||
![]() |
be330174dd | ||
![]() |
0ded7fdc4b | ||
![]() |
2103a5073c | ||
![]() |
ce9f7c4674 | ||
![]() |
e5596c1944 | ||
![]() |
9bc3fee694 | ||
![]() |
21347e1ed6 | ||
![]() |
3b4bab3dc5 | ||
![]() |
cbd6e3b38e | ||
![]() |
b830afa716 | ||
![]() |
bd1d8b0d14 | ||
![]() |
25c2912120 | ||
![]() |
0e19476b56 | ||
![]() |
fa2f2b3563 | ||
![]() |
cbf4970e0f | ||
![]() |
74468513bd | ||
![]() |
794a916a72 | ||
![]() |
76e5d9ec88 | ||
![]() |
076237b8ea | ||
![]() |
53d694c67f | ||
![]() |
5aa6bfea94 | ||
![]() |
1cde63dd64 | ||
![]() |
98e0b7e94f | ||
![]() |
061e8f6abc | ||
![]() |
a189810df6 | ||
![]() |
e95b896790 | ||
![]() |
1f087c4d26 | ||
![]() |
5d7ea6616f | ||
![]() |
2a4b128ae3 | ||
![]() |
fc483274ad | ||
![]() |
fd10a2ad4b | ||
![]() |
b291f63188 | ||
![]() |
f58856bf6f | ||
![]() |
275ea01587 | ||
![]() |
8782dd5628 | ||
![]() |
11bfff8ee1 | ||
![]() |
7c0167a8f6 | ||
![]() |
74d898e37d | ||
![]() |
c6e8b00718 | ||
![]() |
be9980ef13 | ||
![]() |
646a0dedb9 | ||
![]() |
7f964d938c | ||
![]() |
e6b8a139ff | ||
![]() |
bdc0ea1ba5 | ||
![]() |
7fab7918cc | ||
![]() |
74c1bdba0d | ||
![]() |
f983ef7f5f | ||
![]() |
1ae1c33651 | ||
![]() |
084d846621 | ||
![]() |
6a4b994433 | ||
![]() |
bea007deb7 | ||
![]() |
074934be03 | ||
![]() |
0de12368a0 | ||
![]() |
917bd61084 | ||
![]() |
efe040f8c0 | ||
![]() |
2a7553ce09 | ||
![]() |
10af6070a9 | ||
![]() |
92423b0600 | ||
![]() |
b3eac61cac | ||
![]() |
287ba11500 | ||
![]() |
63861f58cc | ||
![]() |
f0425d3de9 | ||
![]() |
210b65268e | ||
![]() |
949d7b1c48 | ||
![]() |
897b213468 | ||
![]() |
4613a080e7 | ||
![]() |
ace2cdf1c6 | ||
![]() |
eed92bc19a | ||
![]() |
e0a2f46466 | ||
![]() |
01ff2e14db | ||
![]() |
199e79ec0c | ||
![]() |
8125ce4cb6 | ||
![]() |
636d6eea99 | ||
![]() |
df56f1ee5e | ||
![]() |
0b6c6c9092 | ||
![]() |
cb60389de7 | ||
![]() |
ce0c95d097 | ||
![]() |
a9bc1e1c37 | ||
![]() |
62c71f4cb1 | ||
![]() |
41aca5c2d0 | ||
![]() |
753724d867 | ||
![]() |
e4576c2ee1 | ||
![]() |
9a7a4b9533 | ||
![]() |
2653191222 | ||
![]() |
b338c0635f | ||
![]() |
4fcbf1cde6 | ||
![]() |
9220b4fa91 | ||
![]() |
fc39a6cd7a | ||
![]() |
1e23e82324 | ||
![]() |
f9fd08040b | ||
![]() |
4318e35ee3 | ||
![]() |
9754c6d9d8 | ||
![]() |
a497235a55 | ||
![]() |
df6dc4fd96 | ||
![]() |
88622847c6 | ||
![]() |
9774663013 | ||
![]() |
a468ae0459 | ||
![]() |
c3e62ba38a | ||
![]() |
117369aa73 | ||
![]() |
1ba734de67 | ||
![]() |
5208cf09b1 | ||
![]() |
bb9de6037c | ||
![]() |
272e53a1f5 | ||
![]() |
db2a9ad1fe | ||
![]() |
c9ab1aead3 | ||
![]() |
4a10e7a7fa | ||
![]() |
86808f80a8 | ||
![]() |
4240b045e6 | ||
![]() |
e547378893 | ||
![]() |
fd77dbec4d | ||
![]() |
fefb3e77d1 | ||
![]() |
ed5489a96e | ||
![]() |
76113742cf | ||
![]() |
57e60c836f | ||
![]() |
622b1f3e67 | ||
![]() |
7ad9844ac0 | ||
![]() |
e43648afe5 | ||
![]() |
823a520266 | ||
![]() |
66ef308abd | ||
![]() |
29e90cc13b | ||
![]() |
f397e0e988 | ||
![]() |
9da9e8fb72 | ||
![]() |
42e77e2a69 | ||
![]() |
9241a29336 | ||
![]() |
f7231ad9ad | ||
![]() |
6920964b87 | ||
![]() |
2f9ed52bbd | ||
![]() |
caf2b13c10 | ||
![]() |
1d263449ff | ||
![]() |
48a273f80b | ||
![]() |
939c60473f | ||
![]() |
f76ca04f9e | ||
![]() |
76b8728f0c | ||
![]() |
1f9078d6ae | ||
![]() |
6d84f07505 | ||
![]() |
26b13fc33c | ||
![]() |
1c8435ffa9 | ||
![]() |
6680761596 | ||
![]() |
42b797ed9c | ||
![]() |
336aa43f3c | ||
![]() |
69f392c9b7 | ||
![]() |
a1dfab43b9 | ||
![]() |
a0a199b108 | ||
![]() |
ab0d37fde4 | ||
![]() |
14e71350c8 | ||
![]() |
453f572f83 | ||
![]() |
c9dfa6e571 | ||
![]() |
3dcbcd367d | ||
![]() |
e805ac1d59 | ||
![]() |
b9229ffca5 | ||
![]() |
46c847c4ad | ||
![]() |
92b1a21f79 | ||
![]() |
de76b95dd4 | ||
![]() |
59ec837ef6 | ||
![]() |
f06b99a461 | ||
![]() |
128fce5495 | ||
![]() |
27aa2d4a19 | ||
![]() |
b9f91a0b36 | ||
![]() |
b538dc3858 | ||
![]() |
f0e9496c85 | ||
![]() |
09a6f76f4c | ||
![]() |
e135167484 | ||
![]() |
38296ab352 | ||
![]() |
f43dea68d1 | ||
![]() |
e1f50377f4 | ||
![]() |
7913104527 | ||
![]() |
bfbf2f7cf7 | ||
![]() |
fe3cbd014f | ||
![]() |
3d6f48507a | ||
![]() |
f3761405c8 | ||
![]() |
e49dc9f3d8 | ||
![]() |
d125510b4b | ||
![]() |
1ca386aa9e | ||
![]() |
fb56988014 | ||
![]() |
d046bee790 | ||
![]() |
f11bf0740b | ||
![]() |
8450bf66e6 | ||
![]() |
b4e11be8ef | ||
![]() |
a896079705 | ||
![]() |
583950c828 | ||
![]() |
8ac08a0eec | ||
![]() |
60f47be64c | ||
![]() |
6e56077ada | ||
![]() |
98ae9467bb | ||
![]() |
b7a24af083 | ||
![]() |
c8b1f2369e | ||
![]() |
72b12c3be7 | ||
![]() |
0632dff3f8 | ||
![]() |
509e2dec8a | ||
![]() |
78a48de804 | ||
![]() |
e7dbb00331 | ||
![]() |
c3f9538636 | ||
![]() |
2e06ed01d5 | ||
![]() |
4072b5879b | ||
![]() |
15562e887d | ||
![]() |
f2245c7c77 | ||
![]() |
e4b9b72f2a | ||
![]() |
311f8e0c3f | ||
![]() |
f07f8b7a9e | ||
![]() |
4c4c730a0a | ||
![]() |
e02ecfb6c8 | ||
![]() |
c8059b4dcf | ||
![]() |
59d87127f5 | ||
![]() |
b5cf31b460 | ||
![]() |
cc4915e262 | ||
![]() |
667a2ba18a | ||
![]() |
e054ebe059 | ||
![]() |
9d3dcfd0ec | ||
![]() |
6e0ea5ecc8 | ||
![]() |
a47d8b2557 | ||
![]() |
30c43c285c | ||
![]() |
23a7ea593b | ||
![]() |
75c44aa319 | ||
![]() |
9d7b5d6c91 | ||
![]() |
5d9c4a5f5a | ||
![]() |
197e420a97 | ||
![]() |
a34e1ad3cf | ||
![]() |
2ae0556292 | ||
![]() |
5be9bdd444 | ||
![]() |
b706794905 | ||
![]() |
a8c5413d06 | ||
![]() |
5580de4571 | ||
![]() |
946431d5b0 | ||
![]() |
0610126049 | ||
![]() |
3ebd6a83fc | ||
![]() |
a64570dcae | ||
![]() |
7c40a67841 | ||
![]() |
e64b5b07a2 | ||
![]() |
9e1e295cdc | ||
![]() |
6eb3cddcb6 | ||
![]() |
a4564232a4 | ||
![]() |
a643823f86 | ||
![]() |
8e5d359a03 | ||
![]() |
a170888dd4 | ||
![]() |
cd22855ef8 | ||
![]() |
013fd07139 | ||
![]() |
f63dc2db5c | ||
![]() |
eaa5a396d9 | ||
![]() |
8ed22f5d72 | ||
![]() |
987c16b2f7 | ||
![]() |
950f636d64 | ||
![]() |
4458efb73a | ||
![]() |
ceea599494 | ||
![]() |
3005ec74b3 | ||
![]() |
0759d8996e | ||
![]() |
0f5b843319 | ||
![]() |
ffaf52e1e9 | ||
![]() |
940b10b036 | ||
![]() |
3bc28736cd | ||
![]() |
93a756266c | ||
![]() |
a0a829bf7a | ||
![]() |
730dcfcc7a | ||
![]() |
27a2d5af54 | ||
![]() |
5f81a33f43 | ||
![]() |
6225fde046 | ||
![]() |
069184562b | ||
![]() |
5576bb2348 | ||
![]() |
2738837786 | ||
![]() |
ec3764538d | ||
![]() |
df54c723ae | ||
![]() |
fa8c990e58 | ||
![]() |
da72235ebf | ||
![]() |
89c4aee29e | ||
![]() |
a447a083f2 | ||
![]() |
f32ea81b21 | ||
![]() |
681a914990 | ||
![]() |
4c54f0ddeb | ||
![]() |
c08dfaa23d | ||
![]() |
3b76e736ae | ||
![]() |
552db98bf1 | ||
![]() |
fdcdfef620 | ||
![]() |
6a042438af | ||
![]() |
27331ae3a8 |
@@ -1,8 +1,9 @@
|
|||||||
.vscode
|
.vscode
|
||||||
ollama
|
ollama
|
||||||
app
|
app
|
||||||
|
macapp
|
||||||
dist
|
dist
|
||||||
llm/llama.cpp
|
llm/llama.cpp
|
||||||
.env
|
.env
|
||||||
.cache
|
.cache
|
||||||
test_data
|
test_data
|
||||||
|
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
llm/ext_server/* linguist-vendored
|
60
.github/ISSUE_TEMPLATE/10_bug_report.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Bug report
|
||||||
|
labels: [bug]
|
||||||
|
description: Something isn't working right.
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: What is the issue?
|
||||||
|
description: What happened? What did you expect to happen?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: OS
|
||||||
|
description: Which operating system are you using?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Linux
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
- Docker
|
||||||
|
- WSL2
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: dropdown
|
||||||
|
id: gpu
|
||||||
|
attributes:
|
||||||
|
label: GPU
|
||||||
|
description: Which GPU are you using?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Nvidia
|
||||||
|
- AMD
|
||||||
|
- Intel
|
||||||
|
- Apple
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: dropdown
|
||||||
|
id: cpu
|
||||||
|
attributes:
|
||||||
|
label: CPU
|
||||||
|
description: Which CPU are you using?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Intel
|
||||||
|
- AMD
|
||||||
|
- Apple
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Ollama version
|
||||||
|
description: What version of Ollama are you using? (`ollama --version`)
|
||||||
|
placeholder: e.g., 0.1.32
|
||||||
|
validations:
|
||||||
|
required: false
|
6
.github/ISSUE_TEMPLATE/20_feature_request.md
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Request a new feature
|
||||||
|
labels: feature request
|
||||||
|
---
|
||||||
|
|
5
.github/ISSUE_TEMPLATE/30_model_request.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
name: Model request
|
||||||
|
about: Request support for a new model to be added to Ollama
|
||||||
|
labels: model request
|
||||||
|
---
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: Help
|
||||||
|
url: https://discord.com/invite/ollama
|
||||||
|
about: Please join our Discord server for help using Ollama
|
||||||
|
- name: Troubleshooting
|
||||||
|
url: https://github.com/ollama/ollama/blob/main/docs/faq.md#faq
|
||||||
|
about: See the FAQ for common issues and solutions
|
24
.github/workflows/latest.yaml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: latest
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [released]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-latest:
|
||||||
|
environment: release
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ vars.DOCKER_USER }}
|
||||||
|
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||||
|
- name: Tag images as latest
|
||||||
|
env:
|
||||||
|
PUSH: "1"
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
export "VERSION=${GITHUB_REF_NAME#v}"
|
||||||
|
./scripts/tag_latest.sh
|
473
.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Full build of the Mac assets
|
||||||
|
build-darwin:
|
||||||
|
runs-on: macos-12
|
||||||
|
environment: release
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set Version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
||||||
|
echo "RELEASE_VERSION=$(echo ${GITHUB_REF_NAME} | cut -f1 -d-)" >> $GITHUB_ENV
|
||||||
|
- name: key
|
||||||
|
env:
|
||||||
|
MACOS_SIGNING_KEY: ${{ secrets.MACOS_SIGNING_KEY }}
|
||||||
|
MACOS_SIGNING_KEY_PASSWORD: ${{ secrets.MACOS_SIGNING_KEY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
echo $MACOS_SIGNING_KEY | base64 --decode > certificate.p12
|
||||||
|
security create-keychain -p password build.keychain
|
||||||
|
security default-keychain -s build.keychain
|
||||||
|
security unlock-keychain -p password build.keychain
|
||||||
|
security import certificate.p12 -k build.keychain -P $MACOS_SIGNING_KEY_PASSWORD -T /usr/bin/codesign
|
||||||
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k password build.keychain
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
- name: Build Darwin
|
||||||
|
env:
|
||||||
|
APPLE_IDENTITY: ${{ secrets.APPLE_IDENTITY }}
|
||||||
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
|
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
||||||
|
APPLE_ID: ${{ vars.APPLE_ID }}
|
||||||
|
SDKROOT: /Applications/Xcode_13.4.1.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
|
||||||
|
DEVELOPER_DIR: /Applications/Xcode_13.4.1.app/Contents/Developer
|
||||||
|
run: |
|
||||||
|
./scripts/build_darwin.sh
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dist-darwin
|
||||||
|
path: |
|
||||||
|
dist/*arwin*
|
||||||
|
!dist/*-cov
|
||||||
|
|
||||||
|
# Windows builds take a long time to both install the dependencies and build, so parallelize
|
||||||
|
# CPU generation step
|
||||||
|
generate-windows-cpu:
|
||||||
|
environment: release
|
||||||
|
runs-on: windows
|
||||||
|
env:
|
||||||
|
KEY_CONTAINER: ${{ vars.KEY_CONTAINER }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set Version
|
||||||
|
shell: bash
|
||||||
|
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
||||||
|
- uses: 'google-github-actions/auth@v2'
|
||||||
|
with:
|
||||||
|
project_id: 'ollama'
|
||||||
|
credentials_json: '${{ secrets.GOOGLE_SIGNING_CREDENTIALS }}'
|
||||||
|
- run: echo "${{ vars.OLLAMA_CERT }}" > ollama_inc.crt
|
||||||
|
- name: install Windows SDK 8.1 to get signtool
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
write-host "downloading SDK"
|
||||||
|
Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=323507" -OutFile "${env:RUNNER_TEMP}\sdksetup.exe"
|
||||||
|
Start-Process "${env:RUNNER_TEMP}\sdksetup.exe" -ArgumentList @("/q") -NoNewWindow -Wait
|
||||||
|
write-host "Win SDK 8.1 installed"
|
||||||
|
gci -path 'C:\Program Files (x86)\Windows Kits\' -r -fi 'signtool.exe'
|
||||||
|
- name: install signing plugin
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
write-host "downloading plugin"
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/cng-v1.0/kmscng-1.0-windows-amd64.zip" -OutFile "${env:RUNNER_TEMP}\plugin.zip"
|
||||||
|
Expand-Archive -Path "${env:RUNNER_TEMP}\plugin.zip" -DestinationPath ${env:RUNNER_TEMP}\plugin\
|
||||||
|
write-host "Installing plugin"
|
||||||
|
& "${env:RUNNER_TEMP}\plugin\*\kmscng.msi" /quiet
|
||||||
|
write-host "plugin installed"
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
- run: go get ./...
|
||||||
|
- run: |
|
||||||
|
$gopath=(get-command go).source | split-path -parent
|
||||||
|
& "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Launch-VsDevShell.ps1"
|
||||||
|
cd $env:GITHUB_WORKSPACE
|
||||||
|
$env:CMAKE_SYSTEM_VERSION="10.0.22621.0"
|
||||||
|
$env:PATH="$gopath;$env:PATH"
|
||||||
|
go generate -x ./...
|
||||||
|
name: go generate
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: generate-windows-cpu
|
||||||
|
path: |
|
||||||
|
llm/build/**/bin/*
|
||||||
|
llm/build/**/*.a
|
||||||
|
dist/windows-amd64/**
|
||||||
|
|
||||||
|
# ROCm generation step
|
||||||
|
generate-windows-rocm:
|
||||||
|
environment: release
|
||||||
|
runs-on: windows
|
||||||
|
env:
|
||||||
|
KEY_CONTAINER: ${{ vars.KEY_CONTAINER }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set Version
|
||||||
|
shell: bash
|
||||||
|
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
||||||
|
- uses: 'google-github-actions/auth@v2'
|
||||||
|
with:
|
||||||
|
project_id: 'ollama'
|
||||||
|
credentials_json: '${{ secrets.GOOGLE_SIGNING_CREDENTIALS }}'
|
||||||
|
- run: echo "${{ vars.OLLAMA_CERT }}" > ollama_inc.crt
|
||||||
|
- name: install Windows SDK 8.1 to get signtool
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
write-host "downloading SDK"
|
||||||
|
Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=323507" -OutFile "${env:RUNNER_TEMP}\sdksetup.exe"
|
||||||
|
Start-Process "${env:RUNNER_TEMP}\sdksetup.exe" -ArgumentList @("/q") -NoNewWindow -Wait
|
||||||
|
write-host "Win SDK 8.1 installed"
|
||||||
|
gci -path 'C:\Program Files (x86)\Windows Kits\' -r -fi 'signtool.exe'
|
||||||
|
- name: install signing plugin
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
write-host "downloading plugin"
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/cng-v1.0/kmscng-1.0-windows-amd64.zip" -OutFile "${env:RUNNER_TEMP}\plugin.zip"
|
||||||
|
Expand-Archive -Path "${env:RUNNER_TEMP}\plugin.zip" -DestinationPath ${env:RUNNER_TEMP}\plugin\
|
||||||
|
write-host "Installing plugin"
|
||||||
|
& "${env:RUNNER_TEMP}\plugin\*\kmscng.msi" /quiet
|
||||||
|
write-host "plugin installed"
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
- name: 'Install ROCm'
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
write-host "downloading AMD HIP Installer"
|
||||||
|
Invoke-WebRequest -Uri "https://download.amd.com/developer/eula/rocm-hub/AMD-Software-PRO-Edition-23.Q4-WinSvr2022-For-HIP.exe" -OutFile "${env:RUNNER_TEMP}\rocm-install.exe"
|
||||||
|
write-host "Installing AMD HIP"
|
||||||
|
Start-Process "${env:RUNNER_TEMP}\rocm-install.exe" -ArgumentList '-install' -NoNewWindow -Wait
|
||||||
|
write-host "Completed AMD HIP"
|
||||||
|
- name: 'Verify ROCm'
|
||||||
|
run: |
|
||||||
|
& 'C:\Program Files\AMD\ROCm\*\bin\clang.exe' --version
|
||||||
|
- run: go get ./...
|
||||||
|
- run: |
|
||||||
|
$gopath=(get-command go).source | split-path -parent
|
||||||
|
& "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Launch-VsDevShell.ps1"
|
||||||
|
cd $env:GITHUB_WORKSPACE
|
||||||
|
$env:CMAKE_SYSTEM_VERSION="10.0.22621.0"
|
||||||
|
$env:PATH="$gopath;$env:PATH"
|
||||||
|
$env:OLLAMA_SKIP_CPU_GENERATE="1"
|
||||||
|
$env:HIP_PATH=$(Resolve-Path 'C:\Program Files\AMD\ROCm\*\bin\clang.exe' | split-path | split-path)
|
||||||
|
go generate -x ./...
|
||||||
|
name: go generate
|
||||||
|
- name: 'gather rocm dependencies'
|
||||||
|
run: |
|
||||||
|
$HIP_PATH=$(Resolve-Path 'C:\Program Files\AMD\ROCm\*\bin\clang.exe' | split-path | split-path)
|
||||||
|
md "dist\deps\bin\rocblas\library"
|
||||||
|
cp "${HIP_PATH}\bin\hipblas.dll" "dist\deps\bin\"
|
||||||
|
cp "${HIP_PATH}\bin\rocblas.dll" "dist\deps\bin\"
|
||||||
|
cp "${HIP_PATH}\bin\rocblas\library\*" "dist\deps\bin\rocblas\library\"
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: generate-windows-rocm
|
||||||
|
path: |
|
||||||
|
llm/build/**/bin/*
|
||||||
|
dist/windows-amd64/**
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows-rocm-deps
|
||||||
|
path: dist/deps/*
|
||||||
|
|
||||||
|
# CUDA generation step
|
||||||
|
generate-windows-cuda:
|
||||||
|
environment: release
|
||||||
|
runs-on: windows
|
||||||
|
env:
|
||||||
|
KEY_CONTAINER: ${{ vars.KEY_CONTAINER }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set Version
|
||||||
|
shell: bash
|
||||||
|
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
||||||
|
- uses: 'google-github-actions/auth@v2'
|
||||||
|
with:
|
||||||
|
project_id: 'ollama'
|
||||||
|
credentials_json: '${{ secrets.GOOGLE_SIGNING_CREDENTIALS }}'
|
||||||
|
- run: echo "${{ vars.OLLAMA_CERT }}" > ollama_inc.crt
|
||||||
|
- name: install Windows SDK 8.1 to get signtool
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
write-host "downloading SDK"
|
||||||
|
Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=323507" -OutFile "${env:RUNNER_TEMP}\sdksetup.exe"
|
||||||
|
Start-Process "${env:RUNNER_TEMP}\sdksetup.exe" -ArgumentList @("/q") -NoNewWindow -Wait
|
||||||
|
write-host "Win SDK 8.1 installed"
|
||||||
|
gci -path 'C:\Program Files (x86)\Windows Kits\' -r -fi 'signtool.exe'
|
||||||
|
- name: install signing plugin
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
write-host "downloading plugin"
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/cng-v1.0/kmscng-1.0-windows-amd64.zip" -OutFile "${env:RUNNER_TEMP}\plugin.zip"
|
||||||
|
Expand-Archive -Path "${env:RUNNER_TEMP}\plugin.zip" -DestinationPath ${env:RUNNER_TEMP}\plugin\
|
||||||
|
write-host "Installing plugin"
|
||||||
|
& "${env:RUNNER_TEMP}\plugin\*\kmscng.msi" /quiet
|
||||||
|
write-host "plugin installed"
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
- name: 'Install CUDA'
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
write-host "downloading CUDA Installer"
|
||||||
|
Invoke-WebRequest -Uri "https://developer.download.nvidia.com/compute/cuda/11.3.1/local_installers/cuda_11.3.1_465.89_win10.exe" -OutFile "${env:RUNNER_TEMP}\cuda-install.exe"
|
||||||
|
write-host "Installing CUDA"
|
||||||
|
Start-Process "${env:RUNNER_TEMP}\cuda-install.exe" -ArgumentList '-s' -NoNewWindow -Wait
|
||||||
|
write-host "Completed CUDA"
|
||||||
|
$cudaPath=((resolve-path "c:\Program Files\NVIDIA*\CUDA\v*\bin\nvcc.exe")[0].path | split-path | split-path)
|
||||||
|
$cudaVer=($cudaPath | split-path -leaf ) -replace 'v(\d+).(\d+)', '$1_$2'
|
||||||
|
echo "$cudaPath\bin" >> $env:GITHUB_PATH
|
||||||
|
echo "CUDA_PATH=$cudaPath" >> $env:GITHUB_ENV
|
||||||
|
echo "CUDA_PATH_V${cudaVer}=$cudaPath" >> $env:GITHUB_ENV
|
||||||
|
echo "CUDA_PATH_VX_Y=CUDA_PATH_V${cudaVer}" >> $env:GITHUB_ENV
|
||||||
|
- name: 'Verify CUDA'
|
||||||
|
run: nvcc -V
|
||||||
|
- run: go get ./...
|
||||||
|
- name: go generate
|
||||||
|
run: |
|
||||||
|
$gopath=(get-command go).source | split-path -parent
|
||||||
|
$cudabin=(get-command nvcc).source | split-path
|
||||||
|
& "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Launch-VsDevShell.ps1"
|
||||||
|
cd $env:GITHUB_WORKSPACE
|
||||||
|
$env:CMAKE_SYSTEM_VERSION="10.0.22621.0"
|
||||||
|
$env:PATH="$gopath;$cudabin;$env:PATH"
|
||||||
|
$env:OLLAMA_SKIP_CPU_GENERATE="1"
|
||||||
|
go generate -x ./...
|
||||||
|
- name: 'gather cuda dependencies'
|
||||||
|
run: |
|
||||||
|
$NVIDIA_DIR=(resolve-path 'C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\*\bin\')[0]
|
||||||
|
md "dist\deps"
|
||||||
|
cp "${NVIDIA_DIR}\cudart64_*.dll" "dist\deps\"
|
||||||
|
cp "${NVIDIA_DIR}\cublas64_*.dll" "dist\deps\"
|
||||||
|
cp "${NVIDIA_DIR}\cublasLt64_*.dll" "dist\deps\"
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: generate-windows-cuda
|
||||||
|
path: |
|
||||||
|
llm/build/**/bin/*
|
||||||
|
dist/windows-amd64/**
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows-cuda-deps
|
||||||
|
path: dist/deps/*
|
||||||
|
|
||||||
|
# Import the prior generation steps and build the final windows assets
|
||||||
|
build-windows:
|
||||||
|
environment: release
|
||||||
|
runs-on: windows
|
||||||
|
needs:
|
||||||
|
- generate-windows-cuda
|
||||||
|
- generate-windows-rocm
|
||||||
|
- generate-windows-cpu
|
||||||
|
env:
|
||||||
|
KEY_CONTAINER: ${{ vars.KEY_CONTAINER }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- name: Set Version
|
||||||
|
shell: bash
|
||||||
|
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
||||||
|
- uses: 'google-github-actions/auth@v2'
|
||||||
|
with:
|
||||||
|
project_id: 'ollama'
|
||||||
|
credentials_json: '${{ secrets.GOOGLE_SIGNING_CREDENTIALS }}'
|
||||||
|
- run: echo "${{ vars.OLLAMA_CERT }}" > ollama_inc.crt
|
||||||
|
- name: install Windows SDK 8.1 to get signtool
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
write-host "downloading SDK"
|
||||||
|
Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=323507" -OutFile "${env:RUNNER_TEMP}\sdksetup.exe"
|
||||||
|
Start-Process "${env:RUNNER_TEMP}\sdksetup.exe" -ArgumentList @("/q") -NoNewWindow -Wait
|
||||||
|
write-host "Win SDK 8.1 installed"
|
||||||
|
gci -path 'C:\Program Files (x86)\Windows Kits\' -r -fi 'signtool.exe'
|
||||||
|
- name: install signing plugin
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
write-host "downloading plugin"
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/cng-v1.0/kmscng-1.0-windows-amd64.zip" -OutFile "${env:RUNNER_TEMP}\plugin.zip"
|
||||||
|
Expand-Archive -Path "${env:RUNNER_TEMP}\plugin.zip" -DestinationPath ${env:RUNNER_TEMP}\plugin\
|
||||||
|
write-host "Installing plugin"
|
||||||
|
& "${env:RUNNER_TEMP}\plugin\*\kmscng.msi" /quiet
|
||||||
|
write-host "plugin installed"
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
- run: go get
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: generate-windows-cpu
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: generate-windows-cuda
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows-cuda-deps
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows-rocm-deps
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: generate-windows-rocm
|
||||||
|
- run: dir llm/build
|
||||||
|
- run: |
|
||||||
|
$gopath=(get-command go).source | split-path -parent
|
||||||
|
& "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Launch-VsDevShell.ps1"
|
||||||
|
cd $env:GITHUB_WORKSPACE
|
||||||
|
$env:CMAKE_SYSTEM_VERSION="10.0.22621.0"
|
||||||
|
$env:PATH="$gopath;$env:PATH"
|
||||||
|
$env:OLLAMA_SKIP_GENERATE="1"
|
||||||
|
& .\scripts\build_windows.ps1
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dist-windows
|
||||||
|
path: |
|
||||||
|
dist/OllamaSetup.exe
|
||||||
|
dist/ollama-windows-*.zip
|
||||||
|
|
||||||
|
# Linux x86 assets built using the container based build
|
||||||
|
build-linux-amd64:
|
||||||
|
environment: release
|
||||||
|
runs-on: linux
|
||||||
|
env:
|
||||||
|
OLLAMA_SKIP_MANIFEST_CREATE: '1'
|
||||||
|
BUILD_ARCH: amd64
|
||||||
|
PUSH: '1'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- name: Set Version
|
||||||
|
shell: bash
|
||||||
|
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ vars.DOCKER_USER }}
|
||||||
|
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||||
|
- run: |
|
||||||
|
./scripts/build_linux.sh
|
||||||
|
./scripts/build_docker.sh
|
||||||
|
mv dist/deps/* dist/
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dist-linux-amd64
|
||||||
|
path: |
|
||||||
|
dist/*linux*
|
||||||
|
!dist/*-cov
|
||||||
|
|
||||||
|
# Linux ARM assets built using the container based build
|
||||||
|
# (at present, docker isn't pre-installed on arm ubunutu images)
|
||||||
|
build-linux-arm64:
|
||||||
|
environment: release
|
||||||
|
runs-on: linux-arm64
|
||||||
|
env:
|
||||||
|
OLLAMA_SKIP_MANIFEST_CREATE: '1'
|
||||||
|
BUILD_ARCH: arm64
|
||||||
|
PUSH: '1'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- name: Set Version
|
||||||
|
shell: bash
|
||||||
|
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
||||||
|
- name: 'Install Docker'
|
||||||
|
run: |
|
||||||
|
# Add Docker's official GPG key:
|
||||||
|
env
|
||||||
|
uname -a
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y ca-certificates curl
|
||||||
|
sudo install -m 0755 -d /etc/apt/keyrings
|
||||||
|
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||||
|
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||||
|
|
||||||
|
# Add the repository to Apt sources:
|
||||||
|
echo \
|
||||||
|
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
|
||||||
|
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
|
||||||
|
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
sudo apt-get install acl
|
||||||
|
sudo setfacl --modify user:$USER:rw /var/run/docker.sock
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ vars.DOCKER_USER }}
|
||||||
|
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||||
|
- run: |
|
||||||
|
./scripts/build_linux.sh
|
||||||
|
./scripts/build_docker.sh
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dist-linux-arm64
|
||||||
|
path: |
|
||||||
|
dist/*linux*
|
||||||
|
!dist/*-cov
|
||||||
|
|
||||||
|
# Aggregate all the assets and ship a release
|
||||||
|
release:
|
||||||
|
needs:
|
||||||
|
- build-darwin
|
||||||
|
- build-windows
|
||||||
|
- build-linux-amd64
|
||||||
|
- build-linux-arm64
|
||||||
|
runs-on: linux
|
||||||
|
environment: release
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
env:
|
||||||
|
OLLAMA_SKIP_IMAGE_BUILD: '1'
|
||||||
|
PUSH: '1'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set Version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
||||||
|
echo "RELEASE_VERSION=$(echo ${GITHUB_REF_NAME} | cut -f1 -d-)" >> $GITHUB_ENV
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ vars.DOCKER_USER }}
|
||||||
|
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||||
|
- run: ./scripts/build_docker.sh
|
||||||
|
- name: Retrieve built artifact
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: dist
|
||||||
|
pattern: dist-*
|
||||||
|
merge-multiple: true
|
||||||
|
- run: |
|
||||||
|
ls -lh dist/
|
||||||
|
(cd dist; sha256sum * > sha256sum.txt)
|
||||||
|
cat dist/sha256sum.txt
|
||||||
|
- uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
name: ${{ env.RELEASE_VERSION }}
|
||||||
|
allowUpdates: true
|
||||||
|
artifacts: 'dist/*'
|
||||||
|
draft: true
|
||||||
|
prerelease: true
|
||||||
|
omitBodyDuringUpdate: true
|
||||||
|
generateReleaseNotes: true
|
||||||
|
omitDraftDuringUpdate: true
|
||||||
|
omitPrereleaseDuringUpdate: true
|
||||||
|
replacesArtifacts: true
|
289
.github/workflows/test.yaml
vendored
@@ -1,18 +1,59 @@
|
|||||||
name: test
|
name: test
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
# For PRs, later CI runs preempt previous ones. e.g. a force push on a PR
|
||||||
|
# cancels running CI jobs and starts all new ones.
|
||||||
|
#
|
||||||
|
# For non-PR pushes, concurrency.group needs to be unique for every distinct
|
||||||
|
# CI run we want to have happen. Use run_id, which in practice means all
|
||||||
|
# non-PR CI runs will be allowed to run without preempting each other.
|
||||||
|
group: ${{ github.workflow }}-$${{ github.pull_request.number || github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**/*'
|
||||||
|
- '!docs/**'
|
||||||
|
- '!README.md'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
changes:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
GENERATE: ${{ steps.changes.outputs.GENERATE }}
|
||||||
|
GENERATE_CUDA: ${{ steps.changes.outputs.GENERATE_CUDA }}
|
||||||
|
GENERATE_ROCM: ${{ steps.changes.outputs.GENERATE_ROCM }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- id: changes
|
||||||
|
run: |
|
||||||
|
changed() {
|
||||||
|
git diff-tree -r --no-commit-id --name-only \
|
||||||
|
$(git merge-base ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}) \
|
||||||
|
${{ github.event.pull_request.head.sha }} \
|
||||||
|
| xargs python3 -c "import sys; print(any([x.startswith('$1') for x in sys.argv[1:]]))"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
echo GENERATE=$(changed llm/)
|
||||||
|
echo GENERATE_CUDA=$(changed llm/)
|
||||||
|
echo GENERATE_ROCM=$(changed llm/)
|
||||||
|
} >>$GITHUB_OUTPUT
|
||||||
|
|
||||||
generate:
|
generate:
|
||||||
|
needs: [changes]
|
||||||
|
if: ${{ needs.changes.outputs.GENERATE == 'True' }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-2019]
|
||||||
arch: [amd64, arm64]
|
arch: [amd64, arm64]
|
||||||
exclude:
|
exclude:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
arch: arm64
|
arch: arm64
|
||||||
- os: windows-latest
|
- os: windows-2019
|
||||||
arch: arm64
|
arch: arm64
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
env:
|
env:
|
||||||
@@ -21,86 +62,258 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
- if: ${{ startsWith(matrix.os, 'windows-') }}
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$path = vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
|
|
||||||
if ($path) {
|
|
||||||
$path = join-path $path 'Common7\Tools\vsdevcmd.bat'
|
|
||||||
if (test-path $path) {
|
|
||||||
cmd /s /c """$path"" $args && set" | where { $_ -match '(\w+)=(.*)' } | foreach {
|
|
||||||
echo "$($Matches[1])=$($Matches[2])" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "C:\Program Files\Git\usr\bin" | Out-File -FilePath $Env:GITHUB_PATH -Encoding utf8 -Append
|
|
||||||
- run: go get ./...
|
- run: go get ./...
|
||||||
|
- run: |
|
||||||
|
$gopath=(get-command go).source | split-path -parent
|
||||||
|
$gccpath=(get-command gcc).source | split-path -parent
|
||||||
|
& "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Launch-VsDevShell.ps1"
|
||||||
|
cd $env:GITHUB_WORKSPACE
|
||||||
|
$env:CMAKE_SYSTEM_VERSION="10.0.22621.0"
|
||||||
|
$env:PATH="$gopath;$gccpath;$env:PATH"
|
||||||
|
echo $env:PATH
|
||||||
|
go generate -x ./...
|
||||||
|
if: ${{ startsWith(matrix.os, 'windows-') }}
|
||||||
|
name: 'Windows Go Generate'
|
||||||
- run: go generate -x ./...
|
- run: go generate -x ./...
|
||||||
|
if: ${{ ! startsWith(matrix.os, 'windows-') }}
|
||||||
|
name: 'Unix Go Generate'
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-${{ matrix.arch }}-libraries
|
name: ${{ matrix.os }}-${{ matrix.arch }}-libraries
|
||||||
path: |
|
path: |
|
||||||
llm/llama.cpp/build/**/lib/*
|
llm/build/**/bin/*
|
||||||
lint:
|
llm/build/**/*.a
|
||||||
needs: generate
|
generate-cuda:
|
||||||
|
needs: [changes]
|
||||||
|
if: ${{ needs.changes.outputs.GENERATE_CUDA == 'True' }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
cuda-version:
|
||||||
|
- '11.8.0'
|
||||||
|
runs-on: linux
|
||||||
|
container: nvidia/cuda:${{ matrix.cuda-version }}-devel-ubuntu20.04
|
||||||
|
steps:
|
||||||
|
- run: |
|
||||||
|
apt-get update && apt-get install -y git build-essential curl
|
||||||
|
curl -fsSL https://github.com/Kitware/CMake/releases/download/v3.28.1/cmake-3.28.1-linux-x86_64.tar.gz \
|
||||||
|
| tar -zx -C /usr --strip-components 1
|
||||||
|
env:
|
||||||
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
- run: go get ./...
|
||||||
|
- run: |
|
||||||
|
git config --global --add safe.directory /__w/ollama/ollama
|
||||||
|
go generate -x ./...
|
||||||
|
env:
|
||||||
|
OLLAMA_SKIP_CPU_GENERATE: '1'
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: cuda-${{ matrix.cuda-version }}-libraries
|
||||||
|
path: |
|
||||||
|
llm/build/**/bin/*
|
||||||
|
dist/windows-amd64/**
|
||||||
|
generate-rocm:
|
||||||
|
needs: [changes]
|
||||||
|
if: ${{ needs.changes.outputs.GENERATE_ROCM == 'True' }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
rocm-version:
|
||||||
|
- '6.0.2'
|
||||||
|
runs-on: linux
|
||||||
|
container: rocm/dev-ubuntu-20.04:${{ matrix.rocm-version }}
|
||||||
|
steps:
|
||||||
|
- run: |
|
||||||
|
apt-get update && apt-get install -y git build-essential curl rocm-libs
|
||||||
|
curl -fsSL https://github.com/Kitware/CMake/releases/download/v3.28.1/cmake-3.28.1-linux-x86_64.tar.gz \
|
||||||
|
| tar -zx -C /usr --strip-components 1
|
||||||
|
env:
|
||||||
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
- run: go get ./...
|
||||||
|
- run: |
|
||||||
|
git config --global --add safe.directory /__w/ollama/ollama
|
||||||
|
go generate -x ./...
|
||||||
|
env:
|
||||||
|
OLLAMA_SKIP_CPU_GENERATE: '1'
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: rocm-${{ matrix.rocm-version }}-libraries
|
||||||
|
path: |
|
||||||
|
llm/build/**/bin/*
|
||||||
|
dist/windows-amd64/**
|
||||||
|
|
||||||
|
# ROCm generation step
|
||||||
|
generate-windows-rocm:
|
||||||
|
needs: [changes]
|
||||||
|
if: ${{ needs.changes.outputs.GENERATE_ROCM == 'True' }}
|
||||||
|
runs-on: windows
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
- name: 'Install ROCm'
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
write-host "downloading AMD HIP Installer"
|
||||||
|
Invoke-WebRequest -Uri "https://download.amd.com/developer/eula/rocm-hub/AMD-Software-PRO-Edition-23.Q4-WinSvr2022-For-HIP.exe" -OutFile "${env:RUNNER_TEMP}\rocm-install.exe"
|
||||||
|
write-host "Installing AMD HIP"
|
||||||
|
Start-Process "${env:RUNNER_TEMP}\rocm-install.exe" -ArgumentList '-install' -NoNewWindow -Wait
|
||||||
|
write-host "Completed AMD HIP"
|
||||||
|
- name: 'Verify ROCm'
|
||||||
|
run: |
|
||||||
|
& 'C:\Program Files\AMD\ROCm\*\bin\clang.exe' --version
|
||||||
|
- run: go get ./...
|
||||||
|
- run: |
|
||||||
|
$gopath=(get-command go).source | split-path -parent
|
||||||
|
& "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Launch-VsDevShell.ps1"
|
||||||
|
cd $env:GITHUB_WORKSPACE
|
||||||
|
$env:CMAKE_SYSTEM_VERSION="10.0.22621.0"
|
||||||
|
$env:PATH="$gopath;$env:PATH"
|
||||||
|
$env:OLLAMA_SKIP_CPU_GENERATE="1"
|
||||||
|
$env:HIP_PATH=$(Resolve-Path 'C:\Program Files\AMD\ROCm\*\bin\clang.exe' | split-path | split-path)
|
||||||
|
go generate -x ./...
|
||||||
|
name: go generate
|
||||||
|
env:
|
||||||
|
OLLAMA_SKIP_CPU_GENERATE: '1'
|
||||||
|
# TODO - do we need any artifacts?
|
||||||
|
|
||||||
|
# CUDA generation step
|
||||||
|
generate-windows-cuda:
|
||||||
|
needs: [changes]
|
||||||
|
if: ${{ needs.changes.outputs.GENERATE_CUDA == 'True' }}
|
||||||
|
runs-on: windows
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
cache: true
|
||||||
|
- name: 'Install CUDA'
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
write-host "downloading CUDA Installer"
|
||||||
|
Invoke-WebRequest -Uri "https://developer.download.nvidia.com/compute/cuda/11.3.1/local_installers/cuda_11.3.1_465.89_win10.exe" -OutFile "${env:RUNNER_TEMP}\cuda-install.exe"
|
||||||
|
write-host "Installing CUDA"
|
||||||
|
Start-Process "${env:RUNNER_TEMP}\cuda-install.exe" -ArgumentList '-s' -NoNewWindow -Wait
|
||||||
|
write-host "Completed CUDA"
|
||||||
|
$cudaPath=((resolve-path "c:\Program Files\NVIDIA*\CUDA\v*\bin\nvcc.exe")[0].path | split-path | split-path)
|
||||||
|
$cudaVer=($cudaPath | split-path -leaf ) -replace 'v(\d+).(\d+)', '$1_$2'
|
||||||
|
echo "$cudaPath\bin" >> $env:GITHUB_PATH
|
||||||
|
echo "CUDA_PATH=$cudaPath" >> $env:GITHUB_ENV
|
||||||
|
echo "CUDA_PATH_V${cudaVer}=$cudaPath" >> $env:GITHUB_ENV
|
||||||
|
echo "CUDA_PATH_VX_Y=CUDA_PATH_V${cudaVer}" >> $env:GITHUB_ENV
|
||||||
|
- name: 'Verify CUDA'
|
||||||
|
run: nvcc -V
|
||||||
|
- run: go get ./...
|
||||||
|
- name: go generate
|
||||||
|
run: |
|
||||||
|
$gopath=(get-command go).source | split-path -parent
|
||||||
|
$cudabin=(get-command nvcc).source | split-path
|
||||||
|
& "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Launch-VsDevShell.ps1"
|
||||||
|
cd $env:GITHUB_WORKSPACE
|
||||||
|
$env:CMAKE_SYSTEM_VERSION="10.0.22621.0"
|
||||||
|
$env:PATH="$gopath;$cudabin;$env:PATH"
|
||||||
|
$env:OLLAMA_SKIP_CPU_GENERATE="1"
|
||||||
|
go generate -x ./...
|
||||||
|
env:
|
||||||
|
OLLAMA_SKIP_CPU_GENERATE: '1'
|
||||||
|
# TODO - do we need any artifacts?
|
||||||
|
|
||||||
|
lint:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-2019]
|
||||||
arch: [amd64, arm64]
|
arch: [amd64, arm64]
|
||||||
exclude:
|
exclude:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
arch: arm64
|
arch: arm64
|
||||||
- os: windows-latest
|
- os: windows-2019
|
||||||
arch: arm64
|
arch: arm64
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
arch: amd64
|
arch: amd64
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
env:
|
env:
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
CGO_ENABLED: "1"
|
CGO_ENABLED: '1'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version-file: go.mod
|
||||||
cache: false
|
cache: false
|
||||||
- uses: actions/download-artifact@v4
|
- run: |
|
||||||
|
case ${{ matrix.arch }} in
|
||||||
|
amd64) echo ARCH=x86_64 ;;
|
||||||
|
arm64) echo ARCH=arm64 ;;
|
||||||
|
esac >>$GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
- run: |
|
||||||
|
mkdir -p llm/build/linux/$ARCH/stub/bin
|
||||||
|
touch llm/build/linux/$ARCH/stub/bin/ollama_llama_server
|
||||||
|
if: ${{ startsWith(matrix.os, 'ubuntu-') }}
|
||||||
|
- run: |
|
||||||
|
mkdir -p llm/build/darwin/$ARCH/stub/bin
|
||||||
|
touch llm/build/darwin/$ARCH/stub/bin/ollama_llama_server
|
||||||
|
if: ${{ startsWith(matrix.os, 'macos-') }}
|
||||||
|
- uses: golangci/golangci-lint-action@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-${{ matrix.arch }}-libraries
|
args: --timeout 8m0s -v
|
||||||
path: llm/llama.cpp/build
|
|
||||||
- uses: golangci/golangci-lint-action@v3
|
|
||||||
test:
|
test:
|
||||||
needs: generate
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-2019]
|
||||||
arch: [amd64]
|
arch: [amd64]
|
||||||
exclude:
|
exclude:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
arch: arm64
|
arch: arm64
|
||||||
- os: windows-latest
|
- os: windows-2019
|
||||||
arch: arm64
|
arch: arm64
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
env:
|
env:
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
CGO_ENABLED: "1"
|
CGO_ENABLED: '1'
|
||||||
|
OLLAMA_CPU_TARGET: 'static'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
- run: go get
|
- run: |
|
||||||
- uses: actions/download-artifact@v4
|
case ${{ matrix.arch }} in
|
||||||
with:
|
amd64) echo ARCH=x86_64 ;;
|
||||||
name: ${{ matrix.os }}-${{ matrix.arch }}-libraries
|
arm64) echo ARCH=arm64 ;;
|
||||||
path: llm/llama.cpp/build
|
esac >>$GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
- run: |
|
||||||
|
mkdir -p llm/build/linux/$ARCH/stub/bin
|
||||||
|
touch llm/build/linux/$ARCH/stub/bin/ollama_llama_server
|
||||||
|
if: ${{ startsWith(matrix.os, 'ubuntu-') }}
|
||||||
|
- run: |
|
||||||
|
mkdir -p llm/build/darwin/$ARCH/stub/bin
|
||||||
|
touch llm/build/darwin/$ARCH/stub/bin/ollama_llama_server
|
||||||
|
if: ${{ startsWith(matrix.os, 'macos-') }}
|
||||||
|
shell: bash
|
||||||
|
- run: go generate ./...
|
||||||
- run: go build
|
- run: go build
|
||||||
- run: go test -v ./...
|
- run: go test -v ./...
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.os }}-binaries
|
||||||
|
path: ollama
|
||||||
|
4
.gitignore
vendored
@@ -9,4 +9,6 @@ ggml-metal.metal
|
|||||||
.cache
|
.cache
|
||||||
*.exe
|
*.exe
|
||||||
.idea
|
.idea
|
||||||
test_data
|
test_data
|
||||||
|
*.crt
|
||||||
|
llm/build
|
||||||
|
@@ -15,13 +15,3 @@ linters:
|
|||||||
- misspell
|
- misspell
|
||||||
- nilerr
|
- nilerr
|
||||||
- unused
|
- unused
|
||||||
linters-settings:
|
|
||||||
errcheck:
|
|
||||||
# exclude the following functions since we don't generally
|
|
||||||
# need to be concerned with the returned errors
|
|
||||||
exclude-functions:
|
|
||||||
- encoding/binary.Read
|
|
||||||
- (*os.File).Seek
|
|
||||||
- (*bufio.Writer).WriteString
|
|
||||||
- (*github.com/spf13/pflag.FlagSet).Set
|
|
||||||
- (*github.com/jmorganca/ollama/llm.readSeekOffset).Seek
|
|
||||||
|
145
Dockerfile
@@ -1,29 +1,144 @@
|
|||||||
FROM nvidia/cuda:11.8.0-devel-ubuntu22.04
|
ARG GOLANG_VERSION=1.22.1
|
||||||
|
ARG CMAKE_VERSION=3.22.1
|
||||||
|
# this CUDA_VERSION corresponds with the one specified in docs/gpu.md
|
||||||
|
ARG CUDA_VERSION=11.3.1
|
||||||
|
ARG ROCM_VERSION=6.0.2
|
||||||
|
|
||||||
ARG TARGETARCH
|
# Copy the minimal context we need to run the generate scripts
|
||||||
ARG GOFLAGS="'-ldflags=-w -s'"
|
FROM scratch AS llm-code
|
||||||
|
COPY .git .git
|
||||||
|
COPY .gitmodules .gitmodules
|
||||||
|
COPY llm llm
|
||||||
|
|
||||||
WORKDIR /go/src/github.com/jmorganca/ollama
|
FROM --platform=linux/amd64 nvidia/cuda:$CUDA_VERSION-devel-centos7 AS cuda-build-amd64
|
||||||
RUN apt-get update && apt-get install -y git build-essential cmake
|
ARG CMAKE_VERSION
|
||||||
ADD https://dl.google.com/go/go1.21.3.linux-$TARGETARCH.tar.gz /tmp/go1.21.3.tar.gz
|
COPY ./scripts/rh_linux_deps.sh /
|
||||||
RUN mkdir -p /usr/local && tar xz -C /usr/local </tmp/go1.21.3.tar.gz
|
RUN CMAKE_VERSION=${CMAKE_VERSION} sh /rh_linux_deps.sh
|
||||||
|
ENV PATH /opt/rh/devtoolset-10/root/usr/bin:$PATH
|
||||||
|
COPY --from=llm-code / /go/src/github.com/ollama/ollama/
|
||||||
|
WORKDIR /go/src/github.com/ollama/ollama/llm/generate
|
||||||
|
ARG CGO_CFLAGS
|
||||||
|
RUN OLLAMA_SKIP_STATIC_GENERATE=1 OLLAMA_SKIP_CPU_GENERATE=1 sh gen_linux.sh
|
||||||
|
|
||||||
|
FROM --platform=linux/arm64 nvidia/cuda:$CUDA_VERSION-devel-rockylinux8 AS cuda-build-arm64
|
||||||
|
ARG CMAKE_VERSION
|
||||||
|
COPY ./scripts/rh_linux_deps.sh /
|
||||||
|
RUN CMAKE_VERSION=${CMAKE_VERSION} sh /rh_linux_deps.sh
|
||||||
|
ENV PATH /opt/rh/gcc-toolset-10/root/usr/bin:$PATH
|
||||||
|
COPY --from=llm-code / /go/src/github.com/ollama/ollama/
|
||||||
|
WORKDIR /go/src/github.com/ollama/ollama/llm/generate
|
||||||
|
ARG CGO_CFLAGS
|
||||||
|
RUN OLLAMA_SKIP_STATIC_GENERATE=1 OLLAMA_SKIP_CPU_GENERATE=1 sh gen_linux.sh
|
||||||
|
|
||||||
|
FROM --platform=linux/amd64 rocm/dev-centos-7:${ROCM_VERSION}-complete AS rocm-build-amd64
|
||||||
|
ARG CMAKE_VERSION
|
||||||
|
COPY ./scripts/rh_linux_deps.sh /
|
||||||
|
RUN CMAKE_VERSION=${CMAKE_VERSION} sh /rh_linux_deps.sh
|
||||||
|
ENV PATH /opt/rh/devtoolset-10/root/usr/bin:$PATH
|
||||||
|
ENV LIBRARY_PATH /opt/amdgpu/lib64
|
||||||
|
COPY --from=llm-code / /go/src/github.com/ollama/ollama/
|
||||||
|
WORKDIR /go/src/github.com/ollama/ollama/llm/generate
|
||||||
|
ARG CGO_CFLAGS
|
||||||
|
ARG AMDGPU_TARGETS
|
||||||
|
RUN OLLAMA_SKIP_STATIC_GENERATE=1 OLLAMA_SKIP_CPU_GENERATE=1 sh gen_linux.sh
|
||||||
|
RUN mkdir /tmp/scratch && \
|
||||||
|
for dep in $(zcat /go/src/github.com/ollama/ollama/llm/build/linux/x86_64/rocm*/bin/deps.txt.gz) ; do \
|
||||||
|
cp ${dep} /tmp/scratch/ || exit 1 ; \
|
||||||
|
done && \
|
||||||
|
(cd /opt/rocm/lib && tar cf - rocblas/library) | (cd /tmp/scratch/ && tar xf - ) && \
|
||||||
|
mkdir -p /go/src/github.com/ollama/ollama/dist/deps/ && \
|
||||||
|
(cd /tmp/scratch/ && tar czvf /go/src/github.com/ollama/ollama/dist/deps/ollama-linux-amd64-rocm.tgz . )
|
||||||
|
|
||||||
|
|
||||||
|
FROM --platform=linux/amd64 centos:7 AS cpu-builder-amd64
|
||||||
|
ARG CMAKE_VERSION
|
||||||
|
ARG GOLANG_VERSION
|
||||||
|
COPY ./scripts/rh_linux_deps.sh /
|
||||||
|
RUN CMAKE_VERSION=${CMAKE_VERSION} GOLANG_VERSION=${GOLANG_VERSION} sh /rh_linux_deps.sh
|
||||||
|
ENV PATH /opt/rh/devtoolset-10/root/usr/bin:$PATH
|
||||||
|
COPY --from=llm-code / /go/src/github.com/ollama/ollama/
|
||||||
|
ARG OLLAMA_CUSTOM_CPU_DEFS
|
||||||
|
ARG CGO_CFLAGS
|
||||||
|
WORKDIR /go/src/github.com/ollama/ollama/llm/generate
|
||||||
|
|
||||||
|
FROM --platform=linux/amd64 cpu-builder-amd64 AS static-build-amd64
|
||||||
|
RUN OLLAMA_CPU_TARGET="static" sh gen_linux.sh
|
||||||
|
FROM --platform=linux/amd64 cpu-builder-amd64 AS cpu-build-amd64
|
||||||
|
RUN OLLAMA_SKIP_STATIC_GENERATE=1 OLLAMA_CPU_TARGET="cpu" sh gen_linux.sh
|
||||||
|
FROM --platform=linux/amd64 cpu-builder-amd64 AS cpu_avx-build-amd64
|
||||||
|
RUN OLLAMA_SKIP_STATIC_GENERATE=1 OLLAMA_CPU_TARGET="cpu_avx" sh gen_linux.sh
|
||||||
|
FROM --platform=linux/amd64 cpu-builder-amd64 AS cpu_avx2-build-amd64
|
||||||
|
RUN OLLAMA_SKIP_STATIC_GENERATE=1 OLLAMA_CPU_TARGET="cpu_avx2" sh gen_linux.sh
|
||||||
|
|
||||||
|
FROM --platform=linux/arm64 centos:7 AS cpu-builder-arm64
|
||||||
|
ARG CMAKE_VERSION
|
||||||
|
ARG GOLANG_VERSION
|
||||||
|
COPY ./scripts/rh_linux_deps.sh /
|
||||||
|
RUN CMAKE_VERSION=${CMAKE_VERSION} GOLANG_VERSION=${GOLANG_VERSION} sh /rh_linux_deps.sh
|
||||||
|
ENV PATH /opt/rh/devtoolset-10/root/usr/bin:$PATH
|
||||||
|
COPY --from=llm-code / /go/src/github.com/ollama/ollama/
|
||||||
|
ARG OLLAMA_CUSTOM_CPU_DEFS
|
||||||
|
ARG CGO_CFLAGS
|
||||||
|
WORKDIR /go/src/github.com/ollama/ollama/llm/generate
|
||||||
|
|
||||||
|
FROM --platform=linux/arm64 cpu-builder-arm64 AS static-build-arm64
|
||||||
|
RUN OLLAMA_CPU_TARGET="static" sh gen_linux.sh
|
||||||
|
FROM --platform=linux/arm64 cpu-builder-arm64 AS cpu-build-arm64
|
||||||
|
RUN OLLAMA_SKIP_STATIC_GENERATE=1 OLLAMA_CPU_TARGET="cpu" sh gen_linux.sh
|
||||||
|
|
||||||
|
|
||||||
|
# Intermediate stage used for ./scripts/build_linux.sh
|
||||||
|
FROM --platform=linux/amd64 cpu-build-amd64 AS build-amd64
|
||||||
|
ENV CGO_ENABLED 1
|
||||||
|
WORKDIR /go/src/github.com/ollama/ollama
|
||||||
COPY . .
|
COPY . .
|
||||||
ENV GOARCH=$TARGETARCH
|
COPY --from=static-build-amd64 /go/src/github.com/ollama/ollama/llm/build/linux/ llm/build/linux/
|
||||||
ENV GOFLAGS=$GOFLAGS
|
COPY --from=cpu_avx-build-amd64 /go/src/github.com/ollama/ollama/llm/build/linux/ llm/build/linux/
|
||||||
RUN /usr/local/go/bin/go generate ./... \
|
COPY --from=cpu_avx2-build-amd64 /go/src/github.com/ollama/ollama/llm/build/linux/ llm/build/linux/
|
||||||
&& /usr/local/go/bin/go build .
|
COPY --from=cuda-build-amd64 /go/src/github.com/ollama/ollama/llm/build/linux/ llm/build/linux/
|
||||||
|
COPY --from=rocm-build-amd64 /go/src/github.com/ollama/ollama/llm/build/linux/ llm/build/linux/
|
||||||
|
COPY --from=rocm-build-amd64 /go/src/github.com/ollama/ollama/dist/deps/ ./dist/deps/
|
||||||
|
ARG GOFLAGS
|
||||||
|
ARG CGO_CFLAGS
|
||||||
|
RUN go build -trimpath .
|
||||||
|
|
||||||
FROM ubuntu:22.04
|
# Intermediate stage used for ./scripts/build_linux.sh
|
||||||
|
FROM --platform=linux/arm64 cpu-build-arm64 AS build-arm64
|
||||||
|
ENV CGO_ENABLED 1
|
||||||
|
ARG GOLANG_VERSION
|
||||||
|
WORKDIR /go/src/github.com/ollama/ollama
|
||||||
|
COPY . .
|
||||||
|
COPY --from=static-build-arm64 /go/src/github.com/ollama/ollama/llm/build/linux/ llm/build/linux/
|
||||||
|
COPY --from=cuda-build-arm64 /go/src/github.com/ollama/ollama/llm/build/linux/ llm/build/linux/
|
||||||
|
ARG GOFLAGS
|
||||||
|
ARG CGO_CFLAGS
|
||||||
|
RUN go build -trimpath .
|
||||||
|
|
||||||
|
# Runtime stages
|
||||||
|
FROM --platform=linux/amd64 ubuntu:22.04 as runtime-amd64
|
||||||
RUN apt-get update && apt-get install -y ca-certificates
|
RUN apt-get update && apt-get install -y ca-certificates
|
||||||
COPY --from=0 /go/src/github.com/jmorganca/ollama/ollama /bin/ollama
|
COPY --from=build-amd64 /go/src/github.com/ollama/ollama/ollama /bin/ollama
|
||||||
|
FROM --platform=linux/arm64 ubuntu:22.04 as runtime-arm64
|
||||||
|
RUN apt-get update && apt-get install -y ca-certificates
|
||||||
|
COPY --from=build-arm64 /go/src/github.com/ollama/ollama/ollama /bin/ollama
|
||||||
|
|
||||||
|
# Radeon images are much larger so we keep it distinct from the CPU/CUDA image
|
||||||
|
FROM --platform=linux/amd64 rocm/dev-centos-7:${ROCM_VERSION}-complete as runtime-rocm
|
||||||
|
RUN update-pciids
|
||||||
|
COPY --from=build-amd64 /go/src/github.com/ollama/ollama/ollama /bin/ollama
|
||||||
EXPOSE 11434
|
EXPOSE 11434
|
||||||
ENV OLLAMA_HOST 0.0.0.0
|
ENV OLLAMA_HOST 0.0.0.0
|
||||||
|
|
||||||
# set some environment variable for better NVIDIA compatibility
|
ENTRYPOINT ["/bin/ollama"]
|
||||||
ENV PATH=/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
CMD ["serve"]
|
||||||
|
|
||||||
|
FROM runtime-$TARGETARCH
|
||||||
|
EXPOSE 11434
|
||||||
|
ENV OLLAMA_HOST 0.0.0.0
|
||||||
|
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
ENV LD_LIBRARY_PATH=/usr/local/nvidia/lib:/usr/local/nvidia/lib64
|
ENV LD_LIBRARY_PATH=/usr/local/nvidia/lib:/usr/local/nvidia/lib64
|
||||||
ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||||
|
ENV NVIDIA_VISIBLE_DEVICES=all
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/ollama"]
|
ENTRYPOINT ["/bin/ollama"]
|
||||||
CMD ["serve"]
|
CMD ["serve"]
|
||||||
|
@@ -1,99 +0,0 @@
|
|||||||
ARG GOLANG_VERSION=1.21.3
|
|
||||||
ARG CMAKE_VERSION=3.22.1
|
|
||||||
ARG CUDA_VERSION=11.3.1
|
|
||||||
|
|
||||||
# Copy the minimal context we need to run the generate scripts
|
|
||||||
FROM scratch AS llm-code
|
|
||||||
COPY .git .git
|
|
||||||
COPY .gitmodules .gitmodules
|
|
||||||
COPY llm llm
|
|
||||||
|
|
||||||
FROM --platform=linux/amd64 nvidia/cuda:$CUDA_VERSION-devel-centos7 AS cuda-build-amd64
|
|
||||||
ARG CMAKE_VERSION
|
|
||||||
ARG CGO_CFLAGS
|
|
||||||
COPY ./scripts/rh_linux_deps.sh /
|
|
||||||
RUN CMAKE_VERSION=${CMAKE_VERSION} sh /rh_linux_deps.sh
|
|
||||||
ENV PATH /opt/rh/devtoolset-10/root/usr/bin:$PATH
|
|
||||||
COPY --from=llm-code / /go/src/github.com/jmorganca/ollama/
|
|
||||||
WORKDIR /go/src/github.com/jmorganca/ollama/llm/generate
|
|
||||||
RUN OLLAMA_SKIP_CPU_GENERATE=1 sh gen_linux.sh
|
|
||||||
|
|
||||||
FROM --platform=linux/arm64 nvidia/cuda:$CUDA_VERSION-devel-rockylinux8 AS cuda-build-arm64
|
|
||||||
ARG CMAKE_VERSION
|
|
||||||
ARG CGO_CFLAGS
|
|
||||||
COPY ./scripts/rh_linux_deps.sh /
|
|
||||||
RUN CMAKE_VERSION=${CMAKE_VERSION} sh /rh_linux_deps.sh
|
|
||||||
ENV PATH /opt/rh/gcc-toolset-10/root/usr/bin:$PATH
|
|
||||||
COPY --from=llm-code / /go/src/github.com/jmorganca/ollama/
|
|
||||||
WORKDIR /go/src/github.com/jmorganca/ollama/llm/generate
|
|
||||||
RUN OLLAMA_SKIP_CPU_GENERATE=1 sh gen_linux.sh
|
|
||||||
|
|
||||||
FROM --platform=linux/amd64 rocm/dev-centos-7:5.7.1-complete AS rocm-5-build-amd64
|
|
||||||
ARG CMAKE_VERSION
|
|
||||||
ARG CGO_CFLAGS
|
|
||||||
COPY ./scripts/rh_linux_deps.sh /
|
|
||||||
RUN CMAKE_VERSION=${CMAKE_VERSION} sh /rh_linux_deps.sh
|
|
||||||
ENV PATH /opt/rh/devtoolset-10/root/usr/bin:$PATH
|
|
||||||
ENV LIBRARY_PATH /opt/amdgpu/lib64
|
|
||||||
COPY --from=llm-code / /go/src/github.com/jmorganca/ollama/
|
|
||||||
WORKDIR /go/src/github.com/jmorganca/ollama/llm/generate
|
|
||||||
RUN OLLAMA_SKIP_CPU_GENERATE=1 sh gen_linux.sh
|
|
||||||
|
|
||||||
FROM --platform=linux/amd64 rocm/dev-centos-7:6.0-complete AS rocm-6-build-amd64
|
|
||||||
ARG CMAKE_VERSION
|
|
||||||
ARG CGO_CFLAGS
|
|
||||||
COPY ./scripts/rh_linux_deps.sh /
|
|
||||||
RUN CMAKE_VERSION=${CMAKE_VERSION} sh /rh_linux_deps.sh
|
|
||||||
ENV PATH /opt/rh/devtoolset-10/root/usr/bin:$PATH
|
|
||||||
ENV LIBRARY_PATH /opt/amdgpu/lib64
|
|
||||||
COPY --from=llm-code / /go/src/github.com/jmorganca/ollama/
|
|
||||||
WORKDIR /go/src/github.com/jmorganca/ollama/llm/generate
|
|
||||||
RUN OLLAMA_SKIP_CPU_GENERATE=1 sh gen_linux.sh
|
|
||||||
|
|
||||||
FROM --platform=linux/amd64 centos:7 AS cpu-build-amd64
|
|
||||||
ARG CMAKE_VERSION
|
|
||||||
ARG GOLANG_VERSION
|
|
||||||
ARG OLLAMA_CUSTOM_CPU_DEFS
|
|
||||||
ARG CGO_CFLAGS
|
|
||||||
COPY ./scripts/rh_linux_deps.sh /
|
|
||||||
RUN CMAKE_VERSION=${CMAKE_VERSION} GOLANG_VERSION=${GOLANG_VERSION} sh /rh_linux_deps.sh
|
|
||||||
ENV PATH /opt/rh/devtoolset-10/root/usr/bin:$PATH
|
|
||||||
COPY --from=llm-code / /go/src/github.com/jmorganca/ollama/
|
|
||||||
WORKDIR /go/src/github.com/jmorganca/ollama/llm/generate
|
|
||||||
RUN sh gen_linux.sh
|
|
||||||
|
|
||||||
FROM --platform=linux/arm64 centos:7 AS cpu-build-arm64
|
|
||||||
ARG CMAKE_VERSION
|
|
||||||
ARG GOLANG_VERSION
|
|
||||||
ARG OLLAMA_CUSTOM_CPU_DEFS
|
|
||||||
ARG CGO_CFLAGS
|
|
||||||
COPY ./scripts/rh_linux_deps.sh /
|
|
||||||
RUN CMAKE_VERSION=${CMAKE_VERSION} GOLANG_VERSION=${GOLANG_VERSION} sh /rh_linux_deps.sh
|
|
||||||
ENV PATH /opt/rh/devtoolset-10/root/usr/bin:$PATH
|
|
||||||
COPY --from=llm-code / /go/src/github.com/jmorganca/ollama/
|
|
||||||
WORKDIR /go/src/github.com/jmorganca/ollama/llm/generate
|
|
||||||
RUN sh gen_linux.sh
|
|
||||||
|
|
||||||
|
|
||||||
FROM --platform=linux/amd64 cpu-build-amd64 AS build-amd64
|
|
||||||
ENV CGO_ENABLED 1
|
|
||||||
ARG GOFLAGS
|
|
||||||
ARG CGO_CFLAGS
|
|
||||||
WORKDIR /go/src/github.com/jmorganca/ollama
|
|
||||||
COPY . .
|
|
||||||
COPY --from=cuda-build-amd64 /go/src/github.com/jmorganca/ollama/llm/llama.cpp/build/linux/ llm/llama.cpp/build/linux/
|
|
||||||
COPY --from=rocm-5-build-amd64 /go/src/github.com/jmorganca/ollama/llm/llama.cpp/build/linux/ llm/llama.cpp/build/linux/
|
|
||||||
COPY --from=rocm-6-build-amd64 /go/src/github.com/jmorganca/ollama/llm/llama.cpp/build/linux/ llm/llama.cpp/build/linux/
|
|
||||||
RUN go build .
|
|
||||||
|
|
||||||
FROM --platform=linux/arm64 cpu-build-arm64 AS build-arm64
|
|
||||||
ENV CGO_ENABLED 1
|
|
||||||
ARG GOLANG_VERSION
|
|
||||||
ARG GOFLAGS
|
|
||||||
ARG CGO_CFLAGS
|
|
||||||
WORKDIR /go/src/github.com/jmorganca/ollama
|
|
||||||
COPY . .
|
|
||||||
COPY --from=cuda-build-arm64 /go/src/github.com/jmorganca/ollama/llm/llama.cpp/build/linux/ llm/llama.cpp/build/linux/
|
|
||||||
RUN go build .
|
|
||||||
|
|
||||||
FROM build-$TARGETARCH
|
|
143
README.md
@@ -1,8 +1,5 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<picture>
|
<img alt="ollama" height="200px" src="https://github.com/ollama/ollama/assets/3325447/0d0b44e2-8f4a-4e99-9b52-a5c1c741c8f7">
|
||||||
<source media="(prefers-color-scheme: dark)" height="200px" srcset="https://github.com/jmorganca/ollama/assets/3325447/56ea1849-1284-4645-8970-956de6e51c3c">
|
|
||||||
<img alt="logo" height="200px" src="https://github.com/jmorganca/ollama/assets/3325447/0d0b44e2-8f4a-4e99-9b52-a5c1c741c8f7">
|
|
||||||
</picture>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
# Ollama
|
# Ollama
|
||||||
@@ -13,53 +10,57 @@ Get up and running with large language models locally.
|
|||||||
|
|
||||||
### macOS
|
### macOS
|
||||||
|
|
||||||
[Download](https://ollama.ai/download/Ollama-darwin.zip)
|
[Download](https://ollama.com/download/Ollama-darwin.zip)
|
||||||
|
|
||||||
### Windows
|
### Windows preview
|
||||||
|
|
||||||
Coming soon! For now, you can install Ollama on Windows via WSL2.
|
[Download](https://ollama.com/download/OllamaSetup.exe)
|
||||||
|
|
||||||
### Linux & WSL2
|
### Linux
|
||||||
|
|
||||||
```
|
```
|
||||||
curl https://ollama.ai/install.sh | sh
|
curl -fsSL https://ollama.com/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
[Manual install instructions](https://github.com/jmorganca/ollama/blob/main/docs/linux.md)
|
[Manual install instructions](https://github.com/ollama/ollama/blob/main/docs/linux.md)
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
The official [Ollama Docker image](https://hub.docker.com/r/ollama/ollama) `ollama/ollama` is available on Docker Hub.
|
The official [Ollama Docker image](https://hub.docker.com/r/ollama/ollama) `ollama/ollama` is available on Docker Hub.
|
||||||
|
|
||||||
|
### Libraries
|
||||||
|
|
||||||
|
- [ollama-python](https://github.com/ollama/ollama-python)
|
||||||
|
- [ollama-js](https://github.com/ollama/ollama-js)
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
To run and chat with [Llama 2](https://ollama.ai/library/llama2):
|
To run and chat with [Llama 3](https://ollama.com/library/llama3):
|
||||||
|
|
||||||
```
|
```
|
||||||
ollama run llama2
|
ollama run llama3
|
||||||
```
|
```
|
||||||
|
|
||||||
## Model library
|
## Model library
|
||||||
|
|
||||||
Ollama supports a list of open-source models available on [ollama.ai/library](https://ollama.ai/library 'ollama model library')
|
Ollama supports a list of models available on [ollama.com/library](https://ollama.com/library 'ollama model library')
|
||||||
|
|
||||||
Here are some example open-source models that can be downloaded:
|
Here are some example models that can be downloaded:
|
||||||
|
|
||||||
| Model | Parameters | Size | Download |
|
| Model | Parameters | Size | Download |
|
||||||
| ------------------ | ---------- | ----- | ------------------------------ |
|
| ------------------ | ---------- | ----- | ------------------------------ |
|
||||||
| Llama 2 | 7B | 3.8GB | `ollama run llama2` |
|
| Llama 3 | 8B | 4.7GB | `ollama run llama3` |
|
||||||
|
| Llama 3 | 70B | 40GB | `ollama run llama3:70b` |
|
||||||
|
| Phi-3 | 3,8B | 2.3GB | `ollama run phi3` |
|
||||||
| Mistral | 7B | 4.1GB | `ollama run mistral` |
|
| Mistral | 7B | 4.1GB | `ollama run mistral` |
|
||||||
| Dolphin Phi | 2.7B | 1.6GB | `ollama run dolphin-phi` |
|
|
||||||
| Phi-2 | 2.7B | 1.7GB | `ollama run phi` |
|
|
||||||
| Neural Chat | 7B | 4.1GB | `ollama run neural-chat` |
|
| Neural Chat | 7B | 4.1GB | `ollama run neural-chat` |
|
||||||
| Starling | 7B | 4.1GB | `ollama run starling-lm` |
|
| Starling | 7B | 4.1GB | `ollama run starling-lm` |
|
||||||
| Code Llama | 7B | 3.8GB | `ollama run codellama` |
|
| Code Llama | 7B | 3.8GB | `ollama run codellama` |
|
||||||
| Llama 2 Uncensored | 7B | 3.8GB | `ollama run llama2-uncensored` |
|
| Llama 2 Uncensored | 7B | 3.8GB | `ollama run llama2-uncensored` |
|
||||||
| Llama 2 13B | 13B | 7.3GB | `ollama run llama2:13b` |
|
|
||||||
| Llama 2 70B | 70B | 39GB | `ollama run llama2:70b` |
|
|
||||||
| Orca Mini | 3B | 1.9GB | `ollama run orca-mini` |
|
|
||||||
| Vicuna | 7B | 3.8GB | `ollama run vicuna` |
|
|
||||||
| LLaVA | 7B | 4.5GB | `ollama run llava` |
|
| LLaVA | 7B | 4.5GB | `ollama run llava` |
|
||||||
|
| Gemma | 2B | 1.4GB | `ollama run gemma:2b` |
|
||||||
|
| Gemma | 7B | 4.8GB | `ollama run gemma:7b` |
|
||||||
|
| Solar | 10.7B | 6.1GB | `ollama run solar` |
|
||||||
|
|
||||||
> Note: You should have at least 8 GB of RAM available to run the 7B models, 16 GB to run the 13B models, and 32 GB to run the 33B models.
|
> Note: You should have at least 8 GB of RAM available to run the 7B models, 16 GB to run the 13B models, and 32 GB to run the 33B models.
|
||||||
|
|
||||||
@@ -93,16 +94,16 @@ See the [guide](docs/import.md) on importing models for more information.
|
|||||||
|
|
||||||
### Customize a prompt
|
### Customize a prompt
|
||||||
|
|
||||||
Models from the Ollama library can be customized with a prompt. For example, to customize the `llama2` model:
|
Models from the Ollama library can be customized with a prompt. For example, to customize the `llama3` model:
|
||||||
|
|
||||||
```
|
```
|
||||||
ollama pull llama2
|
ollama pull llama3
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a `Modelfile`:
|
Create a `Modelfile`:
|
||||||
|
|
||||||
```
|
```
|
||||||
FROM llama2
|
FROM llama3
|
||||||
|
|
||||||
# set the temperature to 1 [higher is more creative, lower is more coherent]
|
# set the temperature to 1 [higher is more creative, lower is more coherent]
|
||||||
PARAMETER temperature 1
|
PARAMETER temperature 1
|
||||||
@@ -137,7 +138,7 @@ ollama create mymodel -f ./Modelfile
|
|||||||
### Pull a model
|
### Pull a model
|
||||||
|
|
||||||
```
|
```
|
||||||
ollama pull llama2
|
ollama pull llama3
|
||||||
```
|
```
|
||||||
|
|
||||||
> This command can also be used to update a local model. Only the diff will be pulled.
|
> This command can also be used to update a local model. Only the diff will be pulled.
|
||||||
@@ -145,13 +146,13 @@ ollama pull llama2
|
|||||||
### Remove a model
|
### Remove a model
|
||||||
|
|
||||||
```
|
```
|
||||||
ollama rm llama2
|
ollama rm llama3
|
||||||
```
|
```
|
||||||
|
|
||||||
### Copy a model
|
### Copy a model
|
||||||
|
|
||||||
```
|
```
|
||||||
ollama cp llama2 my-llama2
|
ollama cp llama3 my-model
|
||||||
```
|
```
|
||||||
|
|
||||||
### Multiline input
|
### Multiline input
|
||||||
@@ -175,7 +176,7 @@ The image features a yellow smiley face, which is likely the central focus of th
|
|||||||
### Pass in prompt as arguments
|
### Pass in prompt as arguments
|
||||||
|
|
||||||
```
|
```
|
||||||
$ ollama run llama2 "Summarize this file: $(cat README.md)"
|
$ ollama run llama3 "Summarize this file: $(cat README.md)"
|
||||||
Ollama is a lightweight, extensible framework for building and running language models on the local machine. It provides a simple API for creating, running, and managing models, as well as a library of pre-built models that can be easily used in a variety of applications.
|
Ollama is a lightweight, extensible framework for building and running language models on the local machine. It provides a simple API for creating, running, and managing models, as well as a library of pre-built models that can be easily used in a variety of applications.
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -198,18 +199,21 @@ brew install cmake go
|
|||||||
```
|
```
|
||||||
|
|
||||||
Then generate dependencies:
|
Then generate dependencies:
|
||||||
|
|
||||||
```
|
```
|
||||||
go generate ./...
|
go generate ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
Then build the binary:
|
Then build the binary:
|
||||||
|
|
||||||
```
|
```
|
||||||
go build .
|
go build .
|
||||||
```
|
```
|
||||||
|
|
||||||
More detailed instructions can be found in the [developer guide](https://github.com/jmorganca/ollama/blob/main/docs/development.md)
|
More detailed instructions can be found in the [developer guide](https://github.com/ollama/ollama/blob/main/docs/development.md)
|
||||||
|
|
||||||
|
|
||||||
### Running local builds
|
### Running local builds
|
||||||
|
|
||||||
Next, start the server:
|
Next, start the server:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -219,7 +223,7 @@ Next, start the server:
|
|||||||
Finally, in a separate shell, run a model:
|
Finally, in a separate shell, run a model:
|
||||||
|
|
||||||
```
|
```
|
||||||
./ollama run llama2
|
./ollama run llama3
|
||||||
```
|
```
|
||||||
|
|
||||||
## REST API
|
## REST API
|
||||||
@@ -230,7 +234,7 @@ Ollama has a REST API for running and managing models.
|
|||||||
|
|
||||||
```
|
```
|
||||||
curl http://localhost:11434/api/generate -d '{
|
curl http://localhost:11434/api/generate -d '{
|
||||||
"model": "llama2",
|
"model": "llama3",
|
||||||
"prompt":"Why is the sky blue?"
|
"prompt":"Why is the sky blue?"
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
@@ -239,7 +243,7 @@ curl http://localhost:11434/api/generate -d '{
|
|||||||
|
|
||||||
```
|
```
|
||||||
curl http://localhost:11434/api/chat -d '{
|
curl http://localhost:11434/api/chat -d '{
|
||||||
"model": "mistral",
|
"model": "llama3",
|
||||||
"messages": [
|
"messages": [
|
||||||
{ "role": "user", "content": "why is the sky blue?" }
|
{ "role": "user", "content": "why is the sky blue?" }
|
||||||
]
|
]
|
||||||
@@ -248,26 +252,49 @@ curl http://localhost:11434/api/chat -d '{
|
|||||||
|
|
||||||
See the [API documentation](./docs/api.md) for all endpoints.
|
See the [API documentation](./docs/api.md) for all endpoints.
|
||||||
|
|
||||||
## Integrations
|
|
||||||
|
|
||||||
- [ollama-python](https://github.com/jmorganca/ollama-python)
|
|
||||||
|
|
||||||
## Community Integrations
|
## Community Integrations
|
||||||
|
|
||||||
### Web & Desktop
|
### Web & Desktop
|
||||||
|
|
||||||
|
- [Open WebUI](https://github.com/open-webui/open-webui)
|
||||||
|
- [Enchanted (macOS native)](https://github.com/AugustDev/enchanted)
|
||||||
|
- [Lollms-Webui](https://github.com/ParisNeo/lollms-webui)
|
||||||
|
- [LibreChat](https://github.com/danny-avila/LibreChat)
|
||||||
- [Bionic GPT](https://github.com/bionic-gpt/bionic-gpt)
|
- [Bionic GPT](https://github.com/bionic-gpt/bionic-gpt)
|
||||||
- [HTML UI](https://github.com/rtcfirefly/ollama-ui)
|
- [HTML UI](https://github.com/rtcfirefly/ollama-ui)
|
||||||
|
- [Saddle](https://github.com/jikkuatwork/saddle)
|
||||||
- [Chatbot UI](https://github.com/ivanfioravanti/chatbot-ollama)
|
- [Chatbot UI](https://github.com/ivanfioravanti/chatbot-ollama)
|
||||||
|
- [Chatbot UI v2](https://github.com/mckaywrigley/chatbot-ui)
|
||||||
- [Typescript UI](https://github.com/ollama-interface/Ollama-Gui?tab=readme-ov-file)
|
- [Typescript UI](https://github.com/ollama-interface/Ollama-Gui?tab=readme-ov-file)
|
||||||
- [Minimalistic React UI for Ollama Models](https://github.com/richawo/minimal-llm-ui)
|
- [Minimalistic React UI for Ollama Models](https://github.com/richawo/minimal-llm-ui)
|
||||||
- [Web UI](https://github.com/ollama-webui/ollama-webui)
|
|
||||||
- [Ollamac](https://github.com/kevinhermawan/Ollamac)
|
- [Ollamac](https://github.com/kevinhermawan/Ollamac)
|
||||||
- [big-AGI](https://github.com/enricoros/big-agi/blob/main/docs/config-ollama.md)
|
- [big-AGI](https://github.com/enricoros/big-AGI/blob/main/docs/config-local-ollama.md)
|
||||||
- [Cheshire Cat assistant framework](https://github.com/cheshire-cat-ai/core)
|
- [Cheshire Cat assistant framework](https://github.com/cheshire-cat-ai/core)
|
||||||
- [Amica](https://github.com/semperai/amica)
|
- [Amica](https://github.com/semperai/amica)
|
||||||
- [chatd](https://github.com/BruceMacD/chatd)
|
- [chatd](https://github.com/BruceMacD/chatd)
|
||||||
- [Ollama-SwiftUI](https://github.com/kghandour/Ollama-SwiftUI)
|
- [Ollama-SwiftUI](https://github.com/kghandour/Ollama-SwiftUI)
|
||||||
|
- [Dify.AI](https://github.com/langgenius/dify)
|
||||||
|
- [MindMac](https://mindmac.app)
|
||||||
|
- [NextJS Web Interface for Ollama](https://github.com/jakobhoeg/nextjs-ollama-llm-ui)
|
||||||
|
- [Msty](https://msty.app)
|
||||||
|
- [Chatbox](https://github.com/Bin-Huang/Chatbox)
|
||||||
|
- [WinForm Ollama Copilot](https://github.com/tgraupmann/WinForm_Ollama_Copilot)
|
||||||
|
- [NextChat](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) with [Get Started Doc](https://docs.nextchat.dev/models/ollama)
|
||||||
|
- [Alpaca WebUI](https://github.com/mmo80/alpaca-webui)
|
||||||
|
- [OllamaGUI](https://github.com/enoch1118/ollamaGUI)
|
||||||
|
- [OpenAOE](https://github.com/InternLM/OpenAOE)
|
||||||
|
- [Odin Runes](https://github.com/leonid20000/OdinRunes)
|
||||||
|
- [LLM-X: Progressive Web App](https://github.com/mrdjohnson/llm-x)
|
||||||
|
- [AnythingLLM (Docker + MacOs/Windows/Linux native app)](https://github.com/Mintplex-Labs/anything-llm)
|
||||||
|
- [Ollama Basic Chat: Uses HyperDiv Reactive UI](https://github.com/rapidarchitect/ollama_basic_chat)
|
||||||
|
- [Ollama-chats RPG](https://github.com/drazdra/ollama-chats)
|
||||||
|
- [QA-Pilot: Chat with Code Repository](https://github.com/reid41/QA-Pilot)
|
||||||
|
- [ChatOllama: Open Source Chatbot based on Ollama with Knowledge Bases](https://github.com/sugarforever/chat-ollama)
|
||||||
|
- [CRAG Ollama Chat: Simple Web Search with Corrective RAG](https://github.com/Nagi-ovo/CRAG-Ollama-Chat)
|
||||||
|
- [RAGFlow: Open-source Retrieval-Augmented Generation engine based on deep document understanding](https://github.com/infiniflow/ragflow)
|
||||||
|
- [chat: chat web app for teams](https://github.com/swuecho/chat)
|
||||||
|
- [Lobe Chat](https://github.com/lobehub/lobe-chat) with [Integrating Doc](https://lobehub.com/docs/self-hosting/examples/ollama)
|
||||||
|
- [Ollama RAG Chatbot: Local Chat with multiples PDFs using Ollama and RAG.](https://github.com/datvodinh/rag-chatbot.git)
|
||||||
|
|
||||||
### Terminal
|
### Terminal
|
||||||
|
|
||||||
@@ -276,23 +303,36 @@ See the [API documentation](./docs/api.md) for all endpoints.
|
|||||||
- [Emacs client](https://github.com/zweifisch/ollama)
|
- [Emacs client](https://github.com/zweifisch/ollama)
|
||||||
- [gen.nvim](https://github.com/David-Kunz/gen.nvim)
|
- [gen.nvim](https://github.com/David-Kunz/gen.nvim)
|
||||||
- [ollama.nvim](https://github.com/nomnivore/ollama.nvim)
|
- [ollama.nvim](https://github.com/nomnivore/ollama.nvim)
|
||||||
|
- [ollero.nvim](https://github.com/marco-souza/ollero.nvim)
|
||||||
|
- [ollama-chat.nvim](https://github.com/gerazov/ollama-chat.nvim)
|
||||||
- [ogpt.nvim](https://github.com/huynle/ogpt.nvim)
|
- [ogpt.nvim](https://github.com/huynle/ogpt.nvim)
|
||||||
- [gptel Emacs client](https://github.com/karthink/gptel)
|
- [gptel Emacs client](https://github.com/karthink/gptel)
|
||||||
- [Oatmeal](https://github.com/dustinblackman/oatmeal)
|
- [Oatmeal](https://github.com/dustinblackman/oatmeal)
|
||||||
- [cmdh](https://github.com/pgibler/cmdh)
|
- [cmdh](https://github.com/pgibler/cmdh)
|
||||||
|
- [ooo](https://github.com/npahlfer/ooo)
|
||||||
|
- [shell-pilot](https://github.com/reid41/shell-pilot)
|
||||||
|
- [tenere](https://github.com/pythops/tenere)
|
||||||
|
- [llm-ollama](https://github.com/taketwo/llm-ollama) for [Datasette's LLM CLI](https://llm.datasette.io/en/stable/).
|
||||||
|
- [typechat-cli](https://github.com/anaisbetts/typechat-cli)
|
||||||
|
- [ShellOracle](https://github.com/djcopley/ShellOracle)
|
||||||
|
- [tlm](https://github.com/yusufcanb/tlm)
|
||||||
|
- [podman-ollama](https://github.com/ericcurtin/podman-ollama)
|
||||||
|
|
||||||
### Database
|
### Database
|
||||||
|
|
||||||
- [MindsDB](https://github.com/mindsdb/mindsdb/blob/staging/mindsdb/integrations/handlers/ollama_handler/README.md)
|
- [MindsDB](https://github.com/mindsdb/mindsdb/blob/staging/mindsdb/integrations/handlers/ollama_handler/README.md) (Connects Ollama models with nearly 200 data platforms and apps)
|
||||||
|
- [chromem-go](https://github.com/philippgille/chromem-go/blob/v0.5.0/embed_ollama.go) with [example](https://github.com/philippgille/chromem-go/tree/v0.5.0/examples/rag-wikipedia-ollama)
|
||||||
|
|
||||||
### Package managers
|
### Package managers
|
||||||
|
|
||||||
- [Pacman](https://archlinux.org/packages/extra/x86_64/ollama/)
|
- [Pacman](https://archlinux.org/packages/extra/x86_64/ollama/)
|
||||||
|
- [Helm Chart](https://artifacthub.io/packages/helm/ollama-helm/ollama)
|
||||||
|
|
||||||
### Libraries
|
### Libraries
|
||||||
|
|
||||||
- [LangChain](https://python.langchain.com/docs/integrations/llms/ollama) and [LangChain.js](https://js.langchain.com/docs/modules/model_io/models/llms/integrations/ollama) with [example](https://js.langchain.com/docs/use_cases/question_answering/local_retrieval_qa)
|
- [LangChain](https://python.langchain.com/docs/integrations/llms/ollama) and [LangChain.js](https://js.langchain.com/docs/modules/model_io/models/llms/integrations/ollama) with [example](https://js.langchain.com/docs/use_cases/question_answering/local_retrieval_qa)
|
||||||
- [LangChainGo](https://github.com/tmc/langchaingo/) with [example](https://github.com/tmc/langchaingo/tree/main/examples/ollama-completion-example)
|
- [LangChainGo](https://github.com/tmc/langchaingo/) with [example](https://github.com/tmc/langchaingo/tree/main/examples/ollama-completion-example)
|
||||||
|
- [LangChain4j](https://github.com/langchain4j/langchain4j) with [example](https://github.com/langchain4j/langchain4j-examples/tree/main/ollama-examples/src/main/java)
|
||||||
- [LlamaIndex](https://gpt-index.readthedocs.io/en/stable/examples/llm/ollama.html)
|
- [LlamaIndex](https://gpt-index.readthedocs.io/en/stable/examples/llm/ollama.html)
|
||||||
- [LiteLLM](https://github.com/BerriAI/litellm)
|
- [LiteLLM](https://github.com/BerriAI/litellm)
|
||||||
- [OllamaSharp for .NET](https://github.com/awaescher/OllamaSharp)
|
- [OllamaSharp for .NET](https://github.com/awaescher/OllamaSharp)
|
||||||
@@ -306,7 +346,11 @@ See the [API documentation](./docs/api.md) for all endpoints.
|
|||||||
- [LangChainDart](https://github.com/davidmigloz/langchain_dart)
|
- [LangChainDart](https://github.com/davidmigloz/langchain_dart)
|
||||||
- [Semantic Kernel - Python](https://github.com/microsoft/semantic-kernel/tree/main/python/semantic_kernel/connectors/ai/ollama)
|
- [Semantic Kernel - Python](https://github.com/microsoft/semantic-kernel/tree/main/python/semantic_kernel/connectors/ai/ollama)
|
||||||
- [Haystack](https://github.com/deepset-ai/haystack-integrations/blob/main/integrations/ollama.md)
|
- [Haystack](https://github.com/deepset-ai/haystack-integrations/blob/main/integrations/ollama.md)
|
||||||
|
- [Elixir LangChain](https://github.com/brainlid/langchain)
|
||||||
|
- [Ollama for R - rollama](https://github.com/JBGruber/rollama)
|
||||||
|
- [Ollama-ex for Elixir](https://github.com/lebrunel/ollama-ex)
|
||||||
|
- [Ollama Connector for SAP ABAP](https://github.com/b-tocs/abap_btocs_ollama)
|
||||||
|
- [Testcontainers](https://testcontainers.com/modules/ollama/)
|
||||||
|
|
||||||
### Mobile
|
### Mobile
|
||||||
|
|
||||||
@@ -320,6 +364,7 @@ See the [API documentation](./docs/api.md) for all endpoints.
|
|||||||
- [Continue](https://github.com/continuedev/continue)
|
- [Continue](https://github.com/continuedev/continue)
|
||||||
- [Obsidian Ollama plugin](https://github.com/hinterdupfinger/obsidian-ollama)
|
- [Obsidian Ollama plugin](https://github.com/hinterdupfinger/obsidian-ollama)
|
||||||
- [Logseq Ollama plugin](https://github.com/omagdy7/ollama-logseq)
|
- [Logseq Ollama plugin](https://github.com/omagdy7/ollama-logseq)
|
||||||
|
- [NotesOllama](https://github.com/andersrex/notesollama) (Apple Notes Ollama plugin)
|
||||||
- [Dagger Chatbot](https://github.com/samalba/dagger-chatbot)
|
- [Dagger Chatbot](https://github.com/samalba/dagger-chatbot)
|
||||||
- [Discord AI Bot](https://github.com/mekb-turtle/discord-ai-bot)
|
- [Discord AI Bot](https://github.com/mekb-turtle/discord-ai-bot)
|
||||||
- [Ollama Telegram Bot](https://github.com/ruecat/ollama-telegram)
|
- [Ollama Telegram Bot](https://github.com/ruecat/ollama-telegram)
|
||||||
@@ -327,4 +372,16 @@ See the [API documentation](./docs/api.md) for all endpoints.
|
|||||||
- [Rivet plugin](https://github.com/abrenneke/rivet-plugin-ollama)
|
- [Rivet plugin](https://github.com/abrenneke/rivet-plugin-ollama)
|
||||||
- [Llama Coder](https://github.com/ex3ndr/llama-coder) (Copilot alternative using Ollama)
|
- [Llama Coder](https://github.com/ex3ndr/llama-coder) (Copilot alternative using Ollama)
|
||||||
- [Obsidian BMO Chatbot plugin](https://github.com/longy2k/obsidian-bmo-chatbot)
|
- [Obsidian BMO Chatbot plugin](https://github.com/longy2k/obsidian-bmo-chatbot)
|
||||||
- [Open Interpreter](https://docs.openinterpreter.com/language-model-setup/local-models/ollama)
|
- [Cliobot](https://github.com/herval/cliobot) (Telegram bot with Ollama support)
|
||||||
|
- [Copilot for Obsidian plugin](https://github.com/logancyang/obsidian-copilot)
|
||||||
|
- [Obsidian Local GPT plugin](https://github.com/pfrankov/obsidian-local-gpt)
|
||||||
|
- [Open Interpreter](https://docs.openinterpreter.com/language-model-setup/local-models/ollama)
|
||||||
|
- [twinny](https://github.com/rjmacarthy/twinny) (Copilot and Copilot chat alternative using Ollama)
|
||||||
|
- [Wingman-AI](https://github.com/RussellCanfield/wingman-ai) (Copilot code and chat alternative using Ollama and HuggingFace)
|
||||||
|
- [Page Assist](https://github.com/n4ze3m/page-assist) (Chrome Extension)
|
||||||
|
- [AI Telegram Bot](https://github.com/tusharhero/aitelegrambot) (Telegram bot using Ollama in backend)
|
||||||
|
- [AI ST Completion](https://github.com/yaroslavyaroslav/OpenAI-sublime-text) (Sublime Text 4 AI assistant plugin with Ollama support)
|
||||||
|
- [Discord-Ollama Chat Bot](https://github.com/kevinthedang/discord-ollama) (Generalized TypeScript Discord Bot w/ Tuning Documentation)
|
||||||
|
|
||||||
|
### Supported backends
|
||||||
|
- [llama.cpp](https://github.com/ggerganov/llama.cpp) project founded by Georgi Gerganov.
|
||||||
|
@@ -1,3 +1,9 @@
|
|||||||
|
// Package api implements the client-side API for code wishing to interact
|
||||||
|
// with the ollama service. The methods of the [Client] type correspond to
|
||||||
|
// the ollama REST API as described in https://github.com/ollama/ollama/blob/main/docs/api.md
|
||||||
|
//
|
||||||
|
// The ollama command-line client itself uses this package to interact with
|
||||||
|
// the backend service.
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -5,7 +11,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@@ -15,13 +20,15 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jmorganca/ollama/format"
|
"github.com/ollama/ollama/format"
|
||||||
"github.com/jmorganca/ollama/version"
|
"github.com/ollama/ollama/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Client encapsulates client state for interacting with the ollama
|
||||||
|
// service. Use [ClientFromEnvironment] to create new Clients.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
base *url.URL
|
base *url.URL
|
||||||
http http.Client
|
http *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkError(resp *http.Response, body []byte) error {
|
func checkError(resp *http.Response, body []byte) error {
|
||||||
@@ -40,6 +47,15 @@ func checkError(resp *http.Response, body []byte) error {
|
|||||||
return apiError
|
return apiError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClientFromEnvironment creates a new [Client] using configuration from the
|
||||||
|
// environment variable OLLAMA_HOST, which points to the network host and
|
||||||
|
// port on which the ollama service is listenting. The format of this variable
|
||||||
|
// is:
|
||||||
|
//
|
||||||
|
// <scheme>://<host>:<port>
|
||||||
|
//
|
||||||
|
// If the variable is not specified, a default ollama host and port will be
|
||||||
|
// used.
|
||||||
func ClientFromEnvironment() (*Client, error) {
|
func ClientFromEnvironment() (*Client, error) {
|
||||||
defaultPort := "11434"
|
defaultPort := "11434"
|
||||||
|
|
||||||
@@ -66,30 +82,20 @@ func ClientFromEnvironment() (*Client, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client := Client{
|
return &Client{
|
||||||
base: &url.URL{
|
base: &url.URL{
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
Host: net.JoinHostPort(host, port),
|
Host: net.JoinHostPort(host, port),
|
||||||
},
|
},
|
||||||
}
|
http: http.DefaultClient,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
mockRequest, err := http.NewRequest(http.MethodHead, client.base.String(), nil)
|
func NewClient(base *url.URL, http *http.Client) *Client {
|
||||||
if err != nil {
|
return &Client{
|
||||||
return nil, err
|
base: base,
|
||||||
|
http: http,
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyURL, err := http.ProxyFromEnvironment(mockRequest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
client.http = http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyURL(proxyURL),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return &client, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) do(ctx context.Context, method, path string, reqData, respData any) error {
|
func (c *Client) do(ctx context.Context, method, path string, reqData, respData any) error {
|
||||||
@@ -208,8 +214,14 @@ func (c *Client) stream(ctx context.Context, method, path string, data any, fn f
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateResponseFunc is a function that [Client.Generate] invokes every time
|
||||||
|
// a response is received from the service. If this function returns an error,
|
||||||
|
// [Client.Generate] will stop generating and return this error.
|
||||||
type GenerateResponseFunc func(GenerateResponse) error
|
type GenerateResponseFunc func(GenerateResponse) error
|
||||||
|
|
||||||
|
// Generate generates a response for a given prompt. The req parameter should
|
||||||
|
// be populated with prompt details. fn is called for each response (there may
|
||||||
|
// be multiple responses, e.g. in case streaming is enabled).
|
||||||
func (c *Client) Generate(ctx context.Context, req *GenerateRequest, fn GenerateResponseFunc) error {
|
func (c *Client) Generate(ctx context.Context, req *GenerateRequest, fn GenerateResponseFunc) error {
|
||||||
return c.stream(ctx, http.MethodPost, "/api/generate", req, func(bts []byte) error {
|
return c.stream(ctx, http.MethodPost, "/api/generate", req, func(bts []byte) error {
|
||||||
var resp GenerateResponse
|
var resp GenerateResponse
|
||||||
@@ -221,8 +233,15 @@ func (c *Client) Generate(ctx context.Context, req *GenerateRequest, fn Generate
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChatResponseFunc is a function that [Client.Chat] invokes every time
|
||||||
|
// a response is received from the service. If this function returns an error,
|
||||||
|
// [Client.Chat] will stop generating and return this error.
|
||||||
type ChatResponseFunc func(ChatResponse) error
|
type ChatResponseFunc func(ChatResponse) error
|
||||||
|
|
||||||
|
// Chat generates the next message in a chat. [ChatRequest] may contain a
|
||||||
|
// sequence of messages which can be used to maintain chat history with a model.
|
||||||
|
// fn is called for each response (there may be multiple responses, e.g. if case
|
||||||
|
// streaming is enabled).
|
||||||
func (c *Client) Chat(ctx context.Context, req *ChatRequest, fn ChatResponseFunc) error {
|
func (c *Client) Chat(ctx context.Context, req *ChatRequest, fn ChatResponseFunc) error {
|
||||||
return c.stream(ctx, http.MethodPost, "/api/chat", req, func(bts []byte) error {
|
return c.stream(ctx, http.MethodPost, "/api/chat", req, func(bts []byte) error {
|
||||||
var resp ChatResponse
|
var resp ChatResponse
|
||||||
@@ -234,8 +253,14 @@ func (c *Client) Chat(ctx context.Context, req *ChatRequest, fn ChatResponseFunc
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PullProgressFunc is a function that [Client.Pull] invokes every time there
|
||||||
|
// is progress with a "pull" request sent to the service. If this function
|
||||||
|
// returns an error, [Client.Pull] will stop the process and return this error.
|
||||||
type PullProgressFunc func(ProgressResponse) error
|
type PullProgressFunc func(ProgressResponse) error
|
||||||
|
|
||||||
|
// Pull downloads a model from the ollama library. fn is called each time
|
||||||
|
// progress is made on the request and can be used to display a progress bar,
|
||||||
|
// etc.
|
||||||
func (c *Client) Pull(ctx context.Context, req *PullRequest, fn PullProgressFunc) error {
|
func (c *Client) Pull(ctx context.Context, req *PullRequest, fn PullProgressFunc) error {
|
||||||
return c.stream(ctx, http.MethodPost, "/api/pull", req, func(bts []byte) error {
|
return c.stream(ctx, http.MethodPost, "/api/pull", req, func(bts []byte) error {
|
||||||
var resp ProgressResponse
|
var resp ProgressResponse
|
||||||
@@ -318,18 +343,7 @@ func (c *Client) Embeddings(ctx context.Context, req *EmbeddingRequest) (*Embedd
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) CreateBlob(ctx context.Context, digest string, r io.Reader) error {
|
func (c *Client) CreateBlob(ctx context.Context, digest string, r io.Reader) error {
|
||||||
if err := c.do(ctx, http.MethodHead, fmt.Sprintf("/api/blobs/%s", digest), nil, nil); err != nil {
|
return c.do(ctx, http.MethodPost, fmt.Sprintf("/api/blobs/%s", digest), r, nil)
|
||||||
var statusError StatusError
|
|
||||||
if !errors.As(err, &statusError) || statusError.StatusCode != http.StatusNotFound {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.do(ctx, http.MethodPost, fmt.Sprintf("/api/blobs/%s", digest), r, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Version(ctx context.Context) (string, error) {
|
func (c *Client) Version(ctx context.Context) (string, error) {
|
||||||
|
147
api/types.go
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
@@ -33,25 +34,55 @@ func (e StatusError) Error() string {
|
|||||||
|
|
||||||
type ImageData []byte
|
type ImageData []byte
|
||||||
|
|
||||||
|
// GenerateRequest describes a request sent by [Client.Generate]. While you
|
||||||
|
// have to specify the Model and Prompt fields, all the other fields have
|
||||||
|
// reasonable defaults for basic uses.
|
||||||
type GenerateRequest struct {
|
type GenerateRequest struct {
|
||||||
Model string `json:"model"`
|
// Model is the model name; it should be a name familiar to Ollama from
|
||||||
Prompt string `json:"prompt"`
|
// the library at https://ollama.com/library
|
||||||
System string `json:"system"`
|
Model string `json:"model"`
|
||||||
Template string `json:"template"`
|
|
||||||
Context []int `json:"context,omitempty"`
|
|
||||||
Stream *bool `json:"stream,omitempty"`
|
|
||||||
Raw bool `json:"raw,omitempty"`
|
|
||||||
Format string `json:"format"`
|
|
||||||
Images []ImageData `json:"images,omitempty"`
|
|
||||||
|
|
||||||
|
// Prompt is the textual prompt to send to the model.
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
|
||||||
|
// System overrides the model's default system message/prompt.
|
||||||
|
System string `json:"system"`
|
||||||
|
|
||||||
|
// Template overrides the model's default prompt template.
|
||||||
|
Template string `json:"template"`
|
||||||
|
|
||||||
|
// Context is the context parameter returned from a previous call to
|
||||||
|
// Generate call. It can be used to keep a short conversational memory.
|
||||||
|
Context []int `json:"context,omitempty"`
|
||||||
|
|
||||||
|
// Stream specifies whether the response is streaming; it is true by default.
|
||||||
|
Stream *bool `json:"stream,omitempty"`
|
||||||
|
|
||||||
|
// Raw set to true means that no formatting will be applied to the prompt.
|
||||||
|
Raw bool `json:"raw,omitempty"`
|
||||||
|
|
||||||
|
// Format specifies the format to return a response in.
|
||||||
|
Format string `json:"format"`
|
||||||
|
|
||||||
|
// KeepAlive controls how long the model will stay loaded in memory following
|
||||||
|
// this request.
|
||||||
|
KeepAlive *Duration `json:"keep_alive,omitempty"`
|
||||||
|
|
||||||
|
// Images is an optional list of base64-encoded images accompanying this
|
||||||
|
// request, for multimodal models.
|
||||||
|
Images []ImageData `json:"images,omitempty"`
|
||||||
|
|
||||||
|
// Options lists model-specific options. For example, temperature can be
|
||||||
|
// set through this field, if the model supports it.
|
||||||
Options map[string]interface{} `json:"options"`
|
Options map[string]interface{} `json:"options"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatRequest struct {
|
type ChatRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Messages []Message `json:"messages"`
|
Messages []Message `json:"messages"`
|
||||||
Stream *bool `json:"stream,omitempty"`
|
Stream *bool `json:"stream,omitempty"`
|
||||||
Format string `json:"format"`
|
Format string `json:"format"`
|
||||||
|
KeepAlive *Duration `json:"keep_alive,omitempty"`
|
||||||
|
|
||||||
Options map[string]interface{} `json:"options"`
|
Options map[string]interface{} `json:"options"`
|
||||||
}
|
}
|
||||||
@@ -81,7 +112,7 @@ type Metrics struct {
|
|||||||
EvalDuration time.Duration `json:"eval_duration,omitempty"`
|
EvalDuration time.Duration `json:"eval_duration,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options specfied in GenerateRequest, if you add a new option here add it to the API docs also
|
// Options specified in GenerateRequest, if you add a new option here add it to the API docs also
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Runner
|
Runner
|
||||||
|
|
||||||
@@ -107,27 +138,30 @@ type Options struct {
|
|||||||
|
|
||||||
// Runner options which must be set when the model is loaded into memory
|
// Runner options which must be set when the model is loaded into memory
|
||||||
type Runner struct {
|
type Runner struct {
|
||||||
UseNUMA bool `json:"numa,omitempty"`
|
UseNUMA bool `json:"numa,omitempty"`
|
||||||
NumCtx int `json:"num_ctx,omitempty"`
|
NumCtx int `json:"num_ctx,omitempty"`
|
||||||
NumBatch int `json:"num_batch,omitempty"`
|
NumBatch int `json:"num_batch,omitempty"`
|
||||||
NumGQA int `json:"num_gqa,omitempty"`
|
NumGQA int `json:"num_gqa,omitempty"`
|
||||||
NumGPU int `json:"num_gpu,omitempty"`
|
NumGPU int `json:"num_gpu,omitempty"`
|
||||||
MainGPU int `json:"main_gpu,omitempty"`
|
MainGPU int `json:"main_gpu,omitempty"`
|
||||||
LowVRAM bool `json:"low_vram,omitempty"`
|
LowVRAM bool `json:"low_vram,omitempty"`
|
||||||
F16KV bool `json:"f16_kv,omitempty"`
|
F16KV bool `json:"f16_kv,omitempty"`
|
||||||
LogitsAll bool `json:"logits_all,omitempty"`
|
LogitsAll bool `json:"logits_all,omitempty"`
|
||||||
VocabOnly bool `json:"vocab_only,omitempty"`
|
VocabOnly bool `json:"vocab_only,omitempty"`
|
||||||
UseMMap bool `json:"use_mmap,omitempty"`
|
UseMMap bool `json:"use_mmap,omitempty"`
|
||||||
UseMLock bool `json:"use_mlock,omitempty"`
|
UseMLock bool `json:"use_mlock,omitempty"`
|
||||||
EmbeddingOnly bool `json:"embedding_only,omitempty"`
|
NumThread int `json:"num_thread,omitempty"`
|
||||||
RopeFrequencyBase float32 `json:"rope_frequency_base,omitempty"`
|
|
||||||
|
// Unused: RopeFrequencyBase is ignored. Instead the value in the model will be used
|
||||||
|
RopeFrequencyBase float32 `json:"rope_frequency_base,omitempty"`
|
||||||
|
// Unused: RopeFrequencyScale is ignored. Instead the value in the model will be used
|
||||||
RopeFrequencyScale float32 `json:"rope_frequency_scale,omitempty"`
|
RopeFrequencyScale float32 `json:"rope_frequency_scale,omitempty"`
|
||||||
NumThread int `json:"num_thread,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmbeddingRequest struct {
|
type EmbeddingRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
|
KeepAlive *Duration `json:"keep_alive,omitempty"`
|
||||||
|
|
||||||
Options map[string]interface{} `json:"options"`
|
Options map[string]interface{} `json:"options"`
|
||||||
}
|
}
|
||||||
@@ -137,10 +171,11 @@ type EmbeddingResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateRequest struct {
|
type CreateRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Modelfile string `json:"modelfile"`
|
Modelfile string `json:"modelfile"`
|
||||||
Stream *bool `json:"stream,omitempty"`
|
Stream *bool `json:"stream,omitempty"`
|
||||||
|
Quantization string `json:"quantization,omitempty"`
|
||||||
|
|
||||||
// Name is deprecated, see Model
|
// Name is deprecated, see Model
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -171,6 +206,7 @@ type ShowResponse struct {
|
|||||||
Template string `json:"template,omitempty"`
|
Template string `json:"template,omitempty"`
|
||||||
System string `json:"system,omitempty"`
|
System string `json:"system,omitempty"`
|
||||||
Details ModelDetails `json:"details,omitempty"`
|
Details ModelDetails `json:"details,omitempty"`
|
||||||
|
Messages []Message `json:"messages,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CopyRequest struct {
|
type CopyRequest struct {
|
||||||
@@ -236,6 +272,7 @@ type GenerateResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ModelDetails struct {
|
type ModelDetails struct {
|
||||||
|
ParentModel string `json:"parent_model"`
|
||||||
Format string `json:"format"`
|
Format string `json:"format"`
|
||||||
Family string `json:"family"`
|
Family string `json:"family"`
|
||||||
Families []string `json:"families"`
|
Families []string `json:"families"`
|
||||||
@@ -271,7 +308,7 @@ func (m *Metrics) Summary() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrInvalidOpts = fmt.Errorf("invalid options")
|
var ErrInvalidOpts = errors.New("invalid options")
|
||||||
|
|
||||||
func (opts *Options) FromMap(m map[string]interface{}) error {
|
func (opts *Options) FromMap(m map[string]interface{}) error {
|
||||||
valueOpts := reflect.ValueOf(opts).Elem() // names of the fields in the options struct
|
valueOpts := reflect.ValueOf(opts).Elem() // names of the fields in the options struct
|
||||||
@@ -359,8 +396,10 @@ func (opts *Options) FromMap(m map[string]interface{}) error {
|
|||||||
func DefaultOptions() Options {
|
func DefaultOptions() Options {
|
||||||
return Options{
|
return Options{
|
||||||
// options set on request to runner
|
// options set on request to runner
|
||||||
NumPredict: -1,
|
NumPredict: -1,
|
||||||
NumKeep: 0,
|
|
||||||
|
// set a minimal num_keep to avoid issues on context shifts
|
||||||
|
NumKeep: 4,
|
||||||
Temperature: 0.8,
|
Temperature: 0.8,
|
||||||
TopK: 40,
|
TopK: 40,
|
||||||
TopP: 0.9,
|
TopP: 0.9,
|
||||||
@@ -378,19 +417,16 @@ func DefaultOptions() Options {
|
|||||||
|
|
||||||
Runner: Runner{
|
Runner: Runner{
|
||||||
// options set when the model is loaded
|
// options set when the model is loaded
|
||||||
NumCtx: 2048,
|
NumCtx: 2048,
|
||||||
RopeFrequencyBase: 10000.0,
|
NumBatch: 512,
|
||||||
RopeFrequencyScale: 1.0,
|
NumGPU: -1, // -1 here indicates that NumGPU should be set dynamically
|
||||||
NumBatch: 512,
|
NumGQA: 1,
|
||||||
NumGPU: -1, // -1 here indicates that NumGPU should be set dynamically
|
NumThread: 0, // let the runtime decide
|
||||||
NumGQA: 1,
|
LowVRAM: false,
|
||||||
NumThread: 0, // let the runtime decide
|
F16KV: true,
|
||||||
LowVRAM: false,
|
UseMLock: false,
|
||||||
F16KV: true,
|
UseMMap: true,
|
||||||
UseMLock: false,
|
UseNUMA: false,
|
||||||
UseMMap: true,
|
|
||||||
UseNUMA: false,
|
|
||||||
EmbeddingOnly: true,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,15 +446,18 @@ func (d *Duration) UnmarshalJSON(b []byte) (err error) {
|
|||||||
switch t := v.(type) {
|
switch t := v.(type) {
|
||||||
case float64:
|
case float64:
|
||||||
if t < 0 {
|
if t < 0 {
|
||||||
t = math.MaxFloat64
|
d.Duration = time.Duration(math.MaxInt64)
|
||||||
|
} else {
|
||||||
|
d.Duration = time.Duration(t * float64(time.Second))
|
||||||
}
|
}
|
||||||
|
|
||||||
d.Duration = time.Duration(t)
|
|
||||||
case string:
|
case string:
|
||||||
d.Duration, err = time.ParseDuration(t)
|
d.Duration, err = time.ParseDuration(t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if d.Duration < 0 {
|
||||||
|
d.Duration = time.Duration(math.MaxInt64)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
50
api/types_test.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKeepAliveParsingFromJSON(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
req string
|
||||||
|
exp *Duration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Positive Integer",
|
||||||
|
req: `{ "keep_alive": 42 }`,
|
||||||
|
exp: &Duration{42 * time.Second},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Positive Integer String",
|
||||||
|
req: `{ "keep_alive": "42m" }`,
|
||||||
|
exp: &Duration{42 * time.Minute},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Negative Integer",
|
||||||
|
req: `{ "keep_alive": -1 }`,
|
||||||
|
exp: &Duration{math.MaxInt64},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Negative Integer String",
|
||||||
|
req: `{ "keep_alive": "-1m" }`,
|
||||||
|
exp: &Duration{math.MaxInt64},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
var dec ChatRequest
|
||||||
|
err := json.Unmarshal([]byte(test.req), &dec)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, test.exp, dec.KeepAlive)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"plugin:import/recommended",
|
|
||||||
"plugin:import/electron",
|
|
||||||
"plugin:import/typescript"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser"
|
|
||||||
}
|
|
94
app/.gitignore
vendored
@@ -1,92 +1,2 @@
|
|||||||
# Logs
|
ollama.syso
|
||||||
logs
|
app
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# TypeScript v1 declaration files
|
|
||||||
typings/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variables file
|
|
||||||
.env
|
|
||||||
.env.test
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# next.js build output
|
|
||||||
.next
|
|
||||||
|
|
||||||
# nuxt.js build output
|
|
||||||
.nuxt
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# Webpack
|
|
||||||
.webpack/
|
|
||||||
|
|
||||||
# Vite
|
|
||||||
.vite/
|
|
||||||
|
|
||||||
# Electron-Forge
|
|
||||||
out/
|
|
7
app/AppDelegate.h
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
@interface AppDelegate : NSObject <NSApplicationDelegate>
|
||||||
|
|
||||||
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
|
||||||
|
|
||||||
|
@end
|
@@ -1,21 +1,18 @@
|
|||||||
# Desktop
|
# Ollama App
|
||||||
|
|
||||||
This app builds upon Ollama to provide a desktop experience for running models.
|
## macOS
|
||||||
|
|
||||||
## Developing
|
TODO
|
||||||
|
|
||||||
First, build the `ollama` binary:
|
## Windows
|
||||||
|
|
||||||
|
If you want to build the installer, youll need to install
|
||||||
|
- https://jrsoftware.org/isinfo.php
|
||||||
|
|
||||||
|
|
||||||
|
In the top directory of this repo, run the following powershell script
|
||||||
|
to build the ollama CLI, ollama app, and ollama installer.
|
||||||
|
|
||||||
```
|
```
|
||||||
cd ..
|
powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1
|
||||||
go build .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Then run the desktop app with `npm start`:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd app
|
|
||||||
npm install
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
|
76
app/app_darwin.go
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// #cgo CFLAGS: -x objective-c
|
||||||
|
// #cgo LDFLAGS: -framework Cocoa -framework LocalAuthentication -framework ServiceManagement
|
||||||
|
// #include "app_darwin.h"
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerLogFile = filepath.Join(home, ".ollama", "logs", "server.log")
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() {
|
||||||
|
initLogging()
|
||||||
|
slog.Info("ollama macOS app started")
|
||||||
|
|
||||||
|
// Ask to move to applications directory
|
||||||
|
moving := C.askToMoveToApplications()
|
||||||
|
if moving {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
C.killOtherInstances()
|
||||||
|
|
||||||
|
code := C.installSymlink()
|
||||||
|
if code != 0 {
|
||||||
|
slog.Error("Failed to install symlink")
|
||||||
|
}
|
||||||
|
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var options ServerOptions
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
var done chan int
|
||||||
|
|
||||||
|
done, err = SpawnServer(ctx, filepath.Join(filepath.Dir(exe), "..", "Resources", "ollama"), options)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("Failed to spawn ollama server %s", err))
|
||||||
|
done = make(chan int, 1)
|
||||||
|
done <- 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the native macOS app
|
||||||
|
// Note: this will block until the app is closed
|
||||||
|
C.run()
|
||||||
|
|
||||||
|
slog.Info("ollama macOS app closed")
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
slog.Info("Waiting for ollama server to shutdown...")
|
||||||
|
if done != nil {
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
slog.Info("Ollama app exiting")
|
||||||
|
}
|
||||||
|
|
||||||
|
//export Quit
|
||||||
|
func Quit() {
|
||||||
|
syscall.Kill(os.Getpid(), syscall.SIGTERM)
|
||||||
|
}
|
13
app/app_darwin.h
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
@interface AppDelegate : NSObject <NSApplicationDelegate>
|
||||||
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
|
||||||
|
@end
|
||||||
|
|
||||||
|
void run();
|
||||||
|
void killOtherInstances();
|
||||||
|
bool askToMoveToApplications();
|
||||||
|
int createSymlinkWithAuthorization();
|
||||||
|
int installSymlink();
|
||||||
|
extern void Restart();
|
||||||
|
extern void Quit();
|
282
app/app_darwin.m
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
#import <AppKit/AppKit.h>
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
#import <CoreServices/CoreServices.h>
|
||||||
|
#import <Security/Security.h>
|
||||||
|
#import <ServiceManagement/ServiceManagement.h>
|
||||||
|
#import "app_darwin.h"
|
||||||
|
|
||||||
|
@interface AppDelegate ()
|
||||||
|
|
||||||
|
@property (strong, nonatomic) NSStatusItem *statusItem;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation AppDelegate
|
||||||
|
|
||||||
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
||||||
|
// show status menu
|
||||||
|
NSMenu *menu = [[NSMenu alloc] init];
|
||||||
|
|
||||||
|
NSMenuItem *aboutMenuItem = [[NSMenuItem alloc] initWithTitle:@"About Ollama" action:@selector(aboutOllama) keyEquivalent:@""];
|
||||||
|
[aboutMenuItem setTarget:self];
|
||||||
|
[menu addItem:aboutMenuItem];
|
||||||
|
|
||||||
|
// Settings submenu
|
||||||
|
NSMenu *settingsMenu = [[NSMenu alloc] initWithTitle:@"Settings"];
|
||||||
|
|
||||||
|
// Submenu items
|
||||||
|
NSMenuItem *chooseModelDirectoryItem = [[NSMenuItem alloc] initWithTitle:@"Choose model directory..." action:@selector(chooseModelDirectory) keyEquivalent:@""];
|
||||||
|
[chooseModelDirectoryItem setTarget:self];
|
||||||
|
[chooseModelDirectoryItem setEnabled:YES];
|
||||||
|
[settingsMenu addItem:chooseModelDirectoryItem];
|
||||||
|
|
||||||
|
NSMenuItem *exposeExternallyItem = [[NSMenuItem alloc] initWithTitle:@"Allow external connections" action:@selector(toggleExposeExternally:) keyEquivalent:@""];
|
||||||
|
[exposeExternallyItem setTarget:self];
|
||||||
|
[exposeExternallyItem setState:NSOffState]; // Set initial state to off
|
||||||
|
[exposeExternallyItem setEnabled:YES];
|
||||||
|
[settingsMenu addItem:exposeExternallyItem];
|
||||||
|
|
||||||
|
NSMenuItem *allowCrossOriginItem = [[NSMenuItem alloc] initWithTitle:@"Allow browser requests" action:@selector(toggleCrossOrigin:) keyEquivalent:@""];
|
||||||
|
[allowCrossOriginItem setTarget:self];
|
||||||
|
[allowCrossOriginItem setState:NSOffState]; // Set initial state to off
|
||||||
|
[allowCrossOriginItem setEnabled:YES];
|
||||||
|
[settingsMenu addItem:allowCrossOriginItem];
|
||||||
|
|
||||||
|
NSMenuItem *settingsMenuItem = [[NSMenuItem alloc] initWithTitle:@"Settings" action:nil keyEquivalent:@""];
|
||||||
|
[settingsMenuItem setSubmenu:settingsMenu];
|
||||||
|
[menu addItem:settingsMenuItem];
|
||||||
|
|
||||||
|
[menu addItemWithTitle:@"Quit Ollama" action:@selector(quit) keyEquivalent:@"q"];
|
||||||
|
|
||||||
|
self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
|
||||||
|
[self.statusItem addObserver:self forKeyPath:@"button.effectiveAppearance" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial context:nil];
|
||||||
|
|
||||||
|
self.statusItem.menu = menu;
|
||||||
|
[self showIcon];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)aboutOllama {
|
||||||
|
[[NSApplication sharedApplication] orderFrontStandardAboutPanel:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)toggleCrossOrigin:(id)sender {
|
||||||
|
NSMenuItem *item = (NSMenuItem *)sender;
|
||||||
|
if ([item state] == NSOffState) {
|
||||||
|
// Do something when cross-origin requests are allowed
|
||||||
|
[item setState:NSOnState];
|
||||||
|
} else {
|
||||||
|
// Do something when cross-origin requests are disallowed
|
||||||
|
[item setState:NSOffState];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)toggleExposeExternally:(id)sender {
|
||||||
|
NSMenuItem *item = (NSMenuItem *)sender;
|
||||||
|
if ([item state] == NSOffState) {
|
||||||
|
// Do something when Ollama is exposed externally
|
||||||
|
[item setState:NSOnState];
|
||||||
|
} else {
|
||||||
|
// Do something when Ollama is not exposed externally
|
||||||
|
[item setState:NSOffState];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)chooseModelDirectory {
|
||||||
|
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
|
||||||
|
[openPanel setCanChooseFiles:NO];
|
||||||
|
[openPanel setCanChooseDirectories:YES];
|
||||||
|
[openPanel setAllowsMultipleSelection:NO];
|
||||||
|
|
||||||
|
NSInteger result = [openPanel runModal];
|
||||||
|
if (result == NSModalResponseOK) {
|
||||||
|
NSURL *selectedDirectoryURL = [openPanel URLs].firstObject;
|
||||||
|
// Do something with the selected directory URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-(void) showIcon {
|
||||||
|
NSAppearance* appearance = self.statusItem.button.effectiveAppearance;
|
||||||
|
NSString* appearanceName = (NSString*)(appearance.name);
|
||||||
|
NSString* iconName = [[appearanceName lowercaseString] containsString:@"dark"] ? @"iconDark" : @"icon";
|
||||||
|
NSImage* statusImage = [NSImage imageNamed:iconName];
|
||||||
|
[statusImage setTemplate:YES];
|
||||||
|
self.statusItem.button.image = statusImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
|
||||||
|
[self showIcon];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)quit {
|
||||||
|
[NSApp stop:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
void run() {
|
||||||
|
@autoreleasepool {
|
||||||
|
[NSApplication sharedApplication];
|
||||||
|
AppDelegate *appDelegate = [[AppDelegate alloc] init];
|
||||||
|
[NSApp setDelegate:appDelegate];
|
||||||
|
[NSApp run];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// killOtherInstances kills all other instances of the app currently
|
||||||
|
// running. This way we can ensure that only the most recently started
|
||||||
|
// instance of Ollama is running
|
||||||
|
void killOtherInstances() {
|
||||||
|
pid_t pid = getpid();
|
||||||
|
NSArray *all = [[NSWorkspace sharedWorkspace] runningApplications];
|
||||||
|
NSMutableArray *apps = [NSMutableArray array];
|
||||||
|
|
||||||
|
for (NSRunningApplication *app in all) {
|
||||||
|
if ([app.bundleIdentifier isEqualToString:[[NSBundle mainBundle] bundleIdentifier]] ||
|
||||||
|
[app.bundleIdentifier isEqualToString:@"ai.ollama.ollama"] ||
|
||||||
|
[app.bundleIdentifier isEqualToString:@"com.electron.ollama"]) {
|
||||||
|
if (app.processIdentifier != pid) {
|
||||||
|
[apps addObject:app];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (NSRunningApplication *app in apps) {
|
||||||
|
kill(app.processIdentifier, SIGTERM);
|
||||||
|
}
|
||||||
|
|
||||||
|
NSDate *startTime = [NSDate date];
|
||||||
|
for (NSRunningApplication *app in apps) {
|
||||||
|
while (!app.terminated) {
|
||||||
|
if (-[startTime timeIntervalSinceNow] >= 5) {
|
||||||
|
kill(app.processIdentifier, SIGKILL);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool askToMoveToApplications() {
|
||||||
|
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
|
||||||
|
if ([bundlePath hasPrefix:@"/Applications"]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSAlert *alert = [[NSAlert alloc] init];
|
||||||
|
[alert setMessageText:@"Move to Applications?"];
|
||||||
|
[alert setInformativeText:@"Ollama works best when run from the Applications directory."];
|
||||||
|
[alert addButtonWithTitle:@"Move to Applications"];
|
||||||
|
[alert addButtonWithTitle:@"Don't move"];
|
||||||
|
|
||||||
|
[NSApp activateIgnoringOtherApps:YES];
|
||||||
|
|
||||||
|
if ([alert runModal] != NSAlertFirstButtonReturn) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// move to applications
|
||||||
|
NSString *applicationsPath = @"/Applications";
|
||||||
|
NSString *newPath = [applicationsPath stringByAppendingPathComponent:@"Ollama.app"];
|
||||||
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||||
|
|
||||||
|
// Check if the newPath already exists
|
||||||
|
if ([fileManager fileExistsAtPath:newPath]) {
|
||||||
|
NSError *removeError = nil;
|
||||||
|
[fileManager removeItemAtPath:newPath error:&removeError];
|
||||||
|
if (removeError) {
|
||||||
|
NSLog(@"Error removing file at %@: %@", newPath, removeError);
|
||||||
|
return false; // or handle the error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSError *moveError = nil;
|
||||||
|
[fileManager moveItemAtPath:bundlePath toPath:newPath error:&moveError];
|
||||||
|
if (moveError) {
|
||||||
|
NSLog(@"Error moving file from %@ to %@: %@", bundlePath, newPath, moveError);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog(@"Opening %@", newPath);
|
||||||
|
NSError *error = nil;
|
||||||
|
NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
|
||||||
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||||
|
[workspace launchApplicationAtURL:[NSURL fileURLWithPath:newPath]
|
||||||
|
options:NSWorkspaceLaunchNewInstance | NSWorkspaceLaunchDefault
|
||||||
|
configuration:@{}
|
||||||
|
error:&error];
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int installSymlink() {
|
||||||
|
NSString *linkPath = @"/usr/local/bin/ollama";
|
||||||
|
NSError *error = nil;
|
||||||
|
|
||||||
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||||
|
NSString *symlinkPath = [fileManager destinationOfSymbolicLinkAtPath:linkPath error:&error];
|
||||||
|
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
|
||||||
|
NSString *execPath = [[NSBundle mainBundle] executablePath];
|
||||||
|
NSString *resPath = [[NSBundle mainBundle] pathForResource:@"ollama" ofType:nil];
|
||||||
|
|
||||||
|
// if the symlink already exists and points to the right place, don't prompt
|
||||||
|
if ([symlinkPath isEqualToString:resPath]) {
|
||||||
|
NSLog(@"symbolic link already exists and points to the right place");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *authorizationPrompt = @"Ollama is trying to install its command line interface (CLI) tool.";
|
||||||
|
|
||||||
|
AuthorizationRef auth = NULL;
|
||||||
|
OSStatus createStatus = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &auth);
|
||||||
|
if (createStatus != errAuthorizationSuccess) {
|
||||||
|
NSLog(@"Error creating authorization");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString * bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
|
||||||
|
NSString *rightNameString = [NSString stringWithFormat:@"%@.%@", bundleIdentifier, @"auth3"];
|
||||||
|
const char *rightName = rightNameString.UTF8String;
|
||||||
|
|
||||||
|
OSStatus getRightResult = AuthorizationRightGet(rightName, NULL);
|
||||||
|
if (getRightResult == errAuthorizationDenied) {
|
||||||
|
if (AuthorizationRightSet(auth, rightName, (__bridge CFTypeRef _Nonnull)(@(kAuthorizationRuleAuthenticateAsAdmin)), (__bridge CFStringRef _Nullable)(authorizationPrompt), NULL, NULL) != errAuthorizationSuccess) {
|
||||||
|
NSLog(@"Failed to set right");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorizationItem right = { .name = rightName, .valueLength = 0, .value = NULL, .flags = 0 };
|
||||||
|
AuthorizationRights rights = { .count = 1, .items = &right };
|
||||||
|
AuthorizationFlags flags = (AuthorizationFlags)(kAuthorizationFlagExtendRights | kAuthorizationFlagInteractionAllowed);
|
||||||
|
AuthorizationItem iconAuthorizationItem = {.name = kAuthorizationEnvironmentIcon, .valueLength = 0, .value = NULL, .flags = 0};
|
||||||
|
AuthorizationEnvironment authorizationEnvironment = {.count = 0, .items = NULL};
|
||||||
|
|
||||||
|
BOOL failedToUseSystemDomain = NO;
|
||||||
|
OSStatus copyStatus = AuthorizationCopyRights(auth, &rights, &authorizationEnvironment, flags, NULL);
|
||||||
|
if (copyStatus != errAuthorizationSuccess) {
|
||||||
|
failedToUseSystemDomain = YES;
|
||||||
|
|
||||||
|
if (copyStatus == errAuthorizationCanceled) {
|
||||||
|
NSLog(@"User cancelled authorization");
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
NSLog(@"Failed copying system domain rights: %d", copyStatus);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *toolPath = "/bin/ln";
|
||||||
|
const char *args[] = {"-s", "-F", [resPath UTF8String], "/usr/local/bin/ollama", NULL};
|
||||||
|
FILE *pipe = NULL;
|
||||||
|
|
||||||
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||||
|
OSStatus status = AuthorizationExecuteWithPrivileges(auth, toolPath, kAuthorizationFlagDefaults, (char *const *)args, &pipe);
|
||||||
|
if (status != errAuthorizationSuccess) {
|
||||||
|
NSLog(@"Failed to create symlink");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorizationFree(auth, kAuthorizationFlagDestroyRights);
|
||||||
|
return 0;
|
||||||
|
}
|
166
app/app_windows.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/app/lifecycle"
|
||||||
|
"github.com/ollama/ollama/app/store"
|
||||||
|
"github.com/ollama/ollama/app/tray"
|
||||||
|
"github.com/ollama/ollama/app/updater"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AppName += ".exe"
|
||||||
|
CLIName += ".exe"
|
||||||
|
// Logs, configs, downloads go to LOCALAPPDATA
|
||||||
|
localAppData := os.Getenv("LOCALAPPDATA")
|
||||||
|
AppDataDir = filepath.Join(localAppData, "Ollama")
|
||||||
|
AppLogFile = filepath.Join(AppDataDir, "app.log")
|
||||||
|
ServerLogFile = filepath.Join(AppDataDir, "server.log")
|
||||||
|
|
||||||
|
// Executables are stored in APPDATA
|
||||||
|
AppDir = filepath.Join(localAppData, "Programs", "Ollama")
|
||||||
|
|
||||||
|
// Make sure we have PATH set correctly for any spawned children
|
||||||
|
paths := strings.Split(os.Getenv("PATH"), ";")
|
||||||
|
// Start with whatever we find in the PATH/LD_LIBRARY_PATH
|
||||||
|
found := false
|
||||||
|
for _, path := range paths {
|
||||||
|
d, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.EqualFold(AppDir, d) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
paths = append(paths, AppDir)
|
||||||
|
|
||||||
|
pathVal := strings.Join(paths, ";")
|
||||||
|
slog.Debug("setting PATH=" + pathVal)
|
||||||
|
err := os.Setenv("PATH", pathVal)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to update PATH: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure our logging dir exists
|
||||||
|
_, err := os.Stat(AppDataDir)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err := os.MkdirAll(AppDataDir, 0o755); err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("create ollama dir %s: %v", AppDataDir, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShowLogs() {
|
||||||
|
cmd_path := "c:\\Windows\\system32\\cmd.exe"
|
||||||
|
slog.Debug(fmt.Sprintf("viewing logs with start %s", AppDataDir))
|
||||||
|
cmd := exec.Command(cmd_path, "/c", "start", AppDataDir)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: false, CreationFlags: 0x08000000}
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("Failed to open log dir: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Start() {
|
||||||
|
cmd_path := "c:\\Windows\\system32\\cmd.exe"
|
||||||
|
slog.Debug(fmt.Sprintf("viewing logs with start %s", AppDataDir))
|
||||||
|
cmd := exec.Command(cmd_path, "/c", "start", AppDataDir)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: false, CreationFlags: 0x08000000}
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("Failed to open log dir: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() {
|
||||||
|
initLogging()
|
||||||
|
|
||||||
|
slog.Info("ollama windows app started")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
var done chan int
|
||||||
|
|
||||||
|
t, err := tray.NewTray()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to start: %s", err)
|
||||||
|
}
|
||||||
|
callbacks := t.GetCallbacks()
|
||||||
|
|
||||||
|
signals := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
slog.Debug("starting callback loop")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-callbacks.Quit:
|
||||||
|
slog.Debug("quit called")
|
||||||
|
t.Quit()
|
||||||
|
case <-signals:
|
||||||
|
slog.Debug("shutting down due to signal")
|
||||||
|
t.Quit()
|
||||||
|
case <-callbacks.Update:
|
||||||
|
err := updater.DoUpgrade(cancel, done)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("upgrade attempt failed: %s", err))
|
||||||
|
}
|
||||||
|
case <-callbacks.ShowLogs:
|
||||||
|
ShowLogs()
|
||||||
|
case <-callbacks.DoFirstUse:
|
||||||
|
err := lifecycle.GetStarted()
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("Failed to launch getting started shell: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if !store.GetFirstTimeRun() {
|
||||||
|
slog.Debug("First time run")
|
||||||
|
err = t.DisplayFirstUseNotification()
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug(fmt.Sprintf("XXX failed to display first use notification %v", err))
|
||||||
|
}
|
||||||
|
store.SetFirstTimeRun(true)
|
||||||
|
} else {
|
||||||
|
slog.Debug("Not first time, skipping first run notification")
|
||||||
|
}
|
||||||
|
|
||||||
|
if isServerRunning(ctx) {
|
||||||
|
slog.Info("Detected another instance of ollama running, exiting")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
done, err = SpawnServer(ctx, CLIName)
|
||||||
|
if err != nil {
|
||||||
|
// TODO - should we retry in a backoff loop?
|
||||||
|
// TODO - should we pop up a warning and maybe add a menu item to view application logs?
|
||||||
|
slog.Error(fmt.Sprintf("Failed to spawn ollama server %s", err))
|
||||||
|
done = make(chan int, 1)
|
||||||
|
done <- 1
|
||||||
|
}
|
||||||
|
|
||||||
|
updater.StartBackgroundUpdaterChecker(ctx, t.UpdateAvailable)
|
||||||
|
|
||||||
|
t.Run()
|
||||||
|
cancel()
|
||||||
|
slog.Info("Waiting for ollama server to shutdown...")
|
||||||
|
if done != nil {
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
slog.Info("Ollama app exiting")
|
||||||
|
}
|
BIN
app/assets/app.ico
Normal file
After Width: | Height: | Size: 7.3 KiB |
17
app/assets/assets.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package assets
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed *.ico
|
||||||
|
var icons embed.FS
|
||||||
|
|
||||||
|
func ListIcons() ([]string, error) {
|
||||||
|
return fs.Glob(icons, "*")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetIcon(filename string) ([]byte, error) {
|
||||||
|
return icons.ReadFile(filename)
|
||||||
|
}
|
Before Width: | Height: | Size: 402 B |
Before Width: | Height: | Size: 741 B |
Before Width: | Height: | Size: 440 B |
Before Width: | Height: | Size: 763 B |
Before Width: | Height: | Size: 447 B |
Before Width: | Height: | Size: 891 B |
Before Width: | Height: | Size: 443 B |
Before Width: | Height: | Size: 844 B |
BIN
app/assets/setup.bmp
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
app/assets/tray.ico
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
app/assets/tray_upgrade.ico
Normal file
After Width: | Height: | Size: 91 KiB |
40
app/darwin/Ollama.app/Contents/Info.plist
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Ollama</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>Ollama</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>icon.icns</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>com.ollama.ollama</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Ollama</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>0.0.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>0.0.0</string>
|
||||||
|
<key>DTCompiler</key>
|
||||||
|
<string>com.apple.compilers.llvm.clang.1_0</string>
|
||||||
|
<key>DTSDKBuild</key>
|
||||||
|
<string>22E245</string>
|
||||||
|
<key>DTSDKName</key>
|
||||||
|
<string>macosx13.3</string>
|
||||||
|
<key>DTXcode</key>
|
||||||
|
<string>1431</string>
|
||||||
|
<key>DTXcodeBuild</key>
|
||||||
|
<string>14E300c</string>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.developer-tools</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>11.0</string>
|
||||||
|
<key>LSUIElement</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
BIN
app/darwin/Ollama.app/Contents/Resources/icon.png
Normal file
After Width: | Height: | Size: 382 B |
BIN
app/darwin/Ollama.app/Contents/Resources/icon@2x.png
Normal file
After Width: | Height: | Size: 691 B |
BIN
app/darwin/Ollama.app/Contents/Resources/iconDark.png
Normal file
After Width: | Height: | Size: 382 B |
BIN
app/darwin/Ollama.app/Contents/Resources/iconDark@2x.png
Normal file
After Width: | Height: | Size: 721 B |
@@ -1,78 +0,0 @@
|
|||||||
import type { ForgeConfig } from '@electron-forge/shared-types'
|
|
||||||
import { MakerSquirrel } from '@electron-forge/maker-squirrel'
|
|
||||||
import { MakerZIP } from '@electron-forge/maker-zip'
|
|
||||||
import { PublisherGithub } from '@electron-forge/publisher-github'
|
|
||||||
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives'
|
|
||||||
import { WebpackPlugin } from '@electron-forge/plugin-webpack'
|
|
||||||
import * as path from 'path'
|
|
||||||
import * as fs from 'fs'
|
|
||||||
|
|
||||||
import { mainConfig } from './webpack.main.config'
|
|
||||||
import { rendererConfig } from './webpack.renderer.config'
|
|
||||||
|
|
||||||
const packageJson = JSON.parse(fs.readFileSync(path.resolve(__dirname, './package.json'), 'utf8'))
|
|
||||||
|
|
||||||
const config: ForgeConfig = {
|
|
||||||
packagerConfig: {
|
|
||||||
appVersion: process.env.VERSION || packageJson.version,
|
|
||||||
asar: true,
|
|
||||||
icon: './assets/icon.icns',
|
|
||||||
extraResource: [
|
|
||||||
'../dist/ollama',
|
|
||||||
path.join(__dirname, './assets/iconTemplate.png'),
|
|
||||||
path.join(__dirname, './assets/iconTemplate@2x.png'),
|
|
||||||
path.join(__dirname, './assets/iconUpdateTemplate.png'),
|
|
||||||
path.join(__dirname, './assets/iconUpdateTemplate@2x.png'),
|
|
||||||
path.join(__dirname, './assets/iconDarkTemplate.png'),
|
|
||||||
path.join(__dirname, './assets/iconDarkTemplate@2x.png'),
|
|
||||||
path.join(__dirname, './assets/iconDarkUpdateTemplate.png'),
|
|
||||||
path.join(__dirname, './assets/iconDarkUpdateTemplate@2x.png'),
|
|
||||||
],
|
|
||||||
...(process.env.SIGN
|
|
||||||
? {
|
|
||||||
osxSign: {
|
|
||||||
identity: process.env.APPLE_IDENTITY,
|
|
||||||
},
|
|
||||||
osxNotarize: {
|
|
||||||
tool: 'notarytool',
|
|
||||||
appleId: process.env.APPLE_ID || '',
|
|
||||||
appleIdPassword: process.env.APPLE_PASSWORD || '',
|
|
||||||
teamId: process.env.APPLE_TEAM_ID || '',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
osxUniversal: {
|
|
||||||
x64ArchFiles: '**/ollama',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rebuildConfig: {},
|
|
||||||
makers: [new MakerSquirrel({}), new MakerZIP({}, ['darwin'])],
|
|
||||||
hooks: {
|
|
||||||
readPackageJson: async (_, packageJson) => {
|
|
||||||
return { ...packageJson, version: process.env.VERSION || packageJson.version }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new AutoUnpackNativesPlugin({}),
|
|
||||||
new WebpackPlugin({
|
|
||||||
mainConfig,
|
|
||||||
devContentSecurityPolicy: `default-src * 'unsafe-eval' 'unsafe-inline'; img-src data: 'self'`,
|
|
||||||
renderer: {
|
|
||||||
config: rendererConfig,
|
|
||||||
nodeIntegration: true,
|
|
||||||
entryPoints: [
|
|
||||||
{
|
|
||||||
html: './src/index.html',
|
|
||||||
js: './src/renderer.tsx',
|
|
||||||
name: 'main_window',
|
|
||||||
preload: {
|
|
||||||
js: './src/preload.ts',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config
|
|
7
app/lifecycle/getstarted_darwin.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func GetStarted() error {
|
||||||
|
return fmt.Errorf("GetStarted not implemented")
|
||||||
|
}
|
44
app/lifecycle/getstarted_windows.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetStarted() error {
|
||||||
|
const CREATE_NEW_CONSOLE = 0x00000010
|
||||||
|
var err error
|
||||||
|
bannerScript := filepath.Join(AppDir, "ollama_welcome.ps1")
|
||||||
|
args := []string{
|
||||||
|
// TODO once we're signed, the execution policy bypass should be removed
|
||||||
|
"powershell", "-noexit", "-ExecutionPolicy", "Bypass", "-nologo", "-file", bannerScript,
|
||||||
|
}
|
||||||
|
args[0], err = exec.LookPath(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the script actually exists
|
||||||
|
_, err = os.Stat(bannerScript)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting started banner script error %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info(fmt.Sprintf("opening getting started terminal with %v", args))
|
||||||
|
attrs := &os.ProcAttr{
|
||||||
|
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
|
||||||
|
Sys: &syscall.SysProcAttr{CreationFlags: CREATE_NEW_CONSOLE, HideWindow: false},
|
||||||
|
}
|
||||||
|
proc, err := os.StartProcess(args[0], args, attrs)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to start getting started shell %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug(fmt.Sprintf("getting started terminal PID: %d", proc.Pid))
|
||||||
|
return proc.Release()
|
||||||
|
}
|
74
app/lifecycle/paths.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
AppName = "ollama app"
|
||||||
|
CLIName = "ollama"
|
||||||
|
AppDir = "/opt/Ollama"
|
||||||
|
AppDataDir = "/opt/Ollama"
|
||||||
|
// TODO - should there be a distinct log dir?
|
||||||
|
UpdateStageDir = "/tmp"
|
||||||
|
AppLogFile = "/tmp/ollama_app.log"
|
||||||
|
ServerLogFile = "/tmp/ollama.log"
|
||||||
|
UpgradeLogFile = "/tmp/ollama_update.log"
|
||||||
|
Installer = "OllamaSetup.exe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
AppName += ".exe"
|
||||||
|
CLIName += ".exe"
|
||||||
|
// Logs, configs, downloads go to LOCALAPPDATA
|
||||||
|
localAppData := os.Getenv("LOCALAPPDATA")
|
||||||
|
AppDataDir = filepath.Join(localAppData, "Ollama")
|
||||||
|
UpdateStageDir = filepath.Join(AppDataDir, "updates")
|
||||||
|
AppLogFile = filepath.Join(AppDataDir, "app.log")
|
||||||
|
ServerLogFile = filepath.Join(AppDataDir, "server.log")
|
||||||
|
UpgradeLogFile = filepath.Join(AppDataDir, "upgrade.log")
|
||||||
|
|
||||||
|
// Executables are stored in APPDATA
|
||||||
|
AppDir = filepath.Join(localAppData, "Programs", "Ollama")
|
||||||
|
|
||||||
|
// Make sure we have PATH set correctly for any spawned children
|
||||||
|
paths := strings.Split(os.Getenv("PATH"), ";")
|
||||||
|
// Start with whatever we find in the PATH/LD_LIBRARY_PATH
|
||||||
|
found := false
|
||||||
|
for _, path := range paths {
|
||||||
|
d, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.EqualFold(AppDir, d) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
paths = append(paths, AppDir)
|
||||||
|
|
||||||
|
pathVal := strings.Join(paths, ";")
|
||||||
|
slog.Debug("setting PATH=" + pathVal)
|
||||||
|
err := os.Setenv("PATH", pathVal)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to update PATH: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure our logging dir exists
|
||||||
|
_, err := os.Stat(AppDataDir)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err := os.MkdirAll(AppDataDir, 0o755); err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("create ollama dir %s: %v", AppDataDir, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
44
app/log.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initLogging() {
|
||||||
|
level := slog.LevelInfo
|
||||||
|
|
||||||
|
if debug := os.Getenv("OLLAMA_DEBUG"); debug != "" {
|
||||||
|
level = slog.LevelDebug
|
||||||
|
}
|
||||||
|
|
||||||
|
var logFile *os.File
|
||||||
|
var err error
|
||||||
|
// Detect if we're a GUI app on windows, and if not, send logs to console
|
||||||
|
if os.Stderr.Fd() != 0 {
|
||||||
|
// Console app detected
|
||||||
|
logFile = os.Stderr
|
||||||
|
// TODO - write one-line to the app.log file saying we're running in console mode to help avoid confusion
|
||||||
|
} else {
|
||||||
|
logFile, err = os.OpenFile(AppLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to create server log %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler := slog.NewTextHandler(logFile, &slog.HandlerOptions{
|
||||||
|
Level: level,
|
||||||
|
AddSource: true,
|
||||||
|
ReplaceAttr: func(_ []string, attr slog.Attr) slog.Attr {
|
||||||
|
if attr.Key == slog.SourceKey {
|
||||||
|
source := attr.Value.Any().(*slog.Source)
|
||||||
|
source.File = filepath.Base(source.File)
|
||||||
|
}
|
||||||
|
return attr
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
slog.SetDefault(slog.New(handler))
|
||||||
|
}
|
16
app/main.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
// Compile with the following to get rid of the cmd pop up on windows
|
||||||
|
// go build -ldflags="-H windowsgui" .
|
||||||
|
var (
|
||||||
|
AppName string
|
||||||
|
CLIName string
|
||||||
|
AppDir string
|
||||||
|
AppDataDir string
|
||||||
|
AppLogFile string
|
||||||
|
ServerLogFile string
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
run()
|
||||||
|
}
|
16695
app/package-lock.json
generated
@@ -1,84 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "ollama",
|
|
||||||
"productName": "Ollama",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"description": "ollama",
|
|
||||||
"main": ".webpack/main",
|
|
||||||
"scripts": {
|
|
||||||
"start": "electron-forge start",
|
|
||||||
"package": "electron-forge package --arch universal",
|
|
||||||
"package:sign": "SIGN=1 electron-forge package --arch universal",
|
|
||||||
"make": "electron-forge make --arch universal",
|
|
||||||
"make:sign": "SIGN=1 electron-forge make --arch universal",
|
|
||||||
"publish": "SIGN=1 electron-forge publish",
|
|
||||||
"lint": "eslint --ext .ts,.tsx .",
|
|
||||||
"format": "prettier --check . --ignore-path .gitignore",
|
|
||||||
"format:fix": "prettier --write . --ignore-path .gitignore"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": {
|
|
||||||
"name": "Jeffrey Morgan",
|
|
||||||
"email": "jmorganca@gmail.com"
|
|
||||||
},
|
|
||||||
"license": "MIT",
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.22.5",
|
|
||||||
"@babel/preset-react": "^7.22.5",
|
|
||||||
"@electron-forge/cli": "^6.2.1",
|
|
||||||
"@electron-forge/maker-deb": "^6.2.1",
|
|
||||||
"@electron-forge/maker-rpm": "^6.2.1",
|
|
||||||
"@electron-forge/maker-squirrel": "^6.2.1",
|
|
||||||
"@electron-forge/maker-zip": "^6.2.1",
|
|
||||||
"@electron-forge/plugin-auto-unpack-natives": "^6.2.1",
|
|
||||||
"@electron-forge/plugin-webpack": "^6.2.1",
|
|
||||||
"@electron-forge/publisher-github": "^6.2.1",
|
|
||||||
"@electron/universal": "^1.4.1",
|
|
||||||
"@svgr/webpack": "^8.0.1",
|
|
||||||
"@types/chmodr": "^1.0.0",
|
|
||||||
"@types/node": "^20.4.0",
|
|
||||||
"@types/react": "^18.2.14",
|
|
||||||
"@types/react-dom": "^18.2.6",
|
|
||||||
"@types/uuid": "^9.0.2",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
|
||||||
"@typescript-eslint/parser": "^5.60.0",
|
|
||||||
"@vercel/webpack-asset-relocator-loader": "^1.7.3",
|
|
||||||
"babel-loader": "^9.1.2",
|
|
||||||
"chmodr": "^1.2.0",
|
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
|
||||||
"css-loader": "^6.8.1",
|
|
||||||
"electron": "25.9.2",
|
|
||||||
"eslint": "^8.43.0",
|
|
||||||
"eslint-plugin-import": "^2.27.5",
|
|
||||||
"fork-ts-checker-webpack-plugin": "^7.3.0",
|
|
||||||
"node-loader": "^2.0.0",
|
|
||||||
"postcss": "^8.4.24",
|
|
||||||
"postcss-import": "^15.1.0",
|
|
||||||
"postcss-loader": "^7.3.3",
|
|
||||||
"postcss-preset-env": "^8.5.1",
|
|
||||||
"prettier": "^2.8.8",
|
|
||||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
|
||||||
"style-loader": "^3.3.3",
|
|
||||||
"svg-inline-loader": "^0.8.2",
|
|
||||||
"tailwindcss": "^3.3.2",
|
|
||||||
"ts-loader": "^9.4.3",
|
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"typescript": "~4.5.4",
|
|
||||||
"url-loader": "^4.1.1",
|
|
||||||
"webpack": "^5.88.0",
|
|
||||||
"webpack-cli": "^5.1.4",
|
|
||||||
"webpack-dev-server": "^4.15.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@electron/remote": "^2.0.10",
|
|
||||||
"@heroicons/react": "^2.0.18",
|
|
||||||
"@segment/analytics-node": "^1.0.0",
|
|
||||||
"copy-to-clipboard": "^3.3.3",
|
|
||||||
"electron-squirrel-startup": "^1.0.0",
|
|
||||||
"electron-store": "^8.1.0",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"uuid": "^9.0.0",
|
|
||||||
"winston": "^3.10.0",
|
|
||||||
"winston-daily-rotate-file": "^4.7.1"
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,7 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
'postcss-import': {},
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
163
app/server.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerOptions struct {
|
||||||
|
Cors bool
|
||||||
|
Expose bool
|
||||||
|
ModelsPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func start(ctx context.Context, command string, options ServerOptions) (*exec.Cmd, error) {
|
||||||
|
cmd := getCmd(ctx, command)
|
||||||
|
|
||||||
|
// set environment variables
|
||||||
|
if options.ModelsPath != "" {
|
||||||
|
cmd.Env = append(cmd.Env, fmt.Sprintf("OLLAMA_MODELS=%s", options.ModelsPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Cors {
|
||||||
|
cmd.Env = append(cmd.Env, "OLLAMA_ORIGINS=*")
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.Expose {
|
||||||
|
cmd.Env = append(cmd.Env, "OLLAMA_HOST=0.0.0.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to spawn server stdout pipe: %w", err)
|
||||||
|
}
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to spawn server stderr pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO - rotation
|
||||||
|
logFile, err := os.OpenFile(ServerLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create server log: %w", err)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
defer logFile.Close()
|
||||||
|
io.Copy(logFile, stdout) //nolint:errcheck
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer logFile.Close()
|
||||||
|
io.Copy(logFile, stderr) //nolint:errcheck
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Re-wire context done behavior to attempt a graceful shutdown of the server
|
||||||
|
cmd.Cancel = func() error {
|
||||||
|
if cmd.Process != nil {
|
||||||
|
err := terminate(cmd)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("error trying to gracefully terminate server", "err", err)
|
||||||
|
return cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
|
||||||
|
tick := time.NewTicker(10 * time.Millisecond)
|
||||||
|
defer tick.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-tick.C:
|
||||||
|
exited, err := isProcessExited(cmd.Process.Pid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if exited {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
slog.Warn("graceful server shutdown timeout, killing", "pid", cmd.Process.Pid)
|
||||||
|
return cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the command and wait for it to finish
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to start server %w", err)
|
||||||
|
}
|
||||||
|
if cmd.Process != nil {
|
||||||
|
slog.Info(fmt.Sprintf("started ollama server with pid %d", cmd.Process.Pid))
|
||||||
|
}
|
||||||
|
slog.Info(fmt.Sprintf("ollama server logs %s", ServerLogFile))
|
||||||
|
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SpawnServer(ctx context.Context, command string, options ServerOptions) (chan int, error) {
|
||||||
|
logDir := filepath.Dir(ServerLogFile)
|
||||||
|
_, err := os.Stat(logDir)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err := os.MkdirAll(logDir, 0o755); err != nil {
|
||||||
|
return nil, fmt.Errorf("create ollama server log dir %s: %v", logDir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan int)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Keep the server running unless we're shuttind down the app
|
||||||
|
crashCount := 0
|
||||||
|
for {
|
||||||
|
slog.Info(fmt.Sprintf("starting server..."))
|
||||||
|
cmd, err := start(ctx, command, options)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to start server %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Wait() //nolint:errcheck
|
||||||
|
var code int
|
||||||
|
if cmd.ProcessState != nil {
|
||||||
|
code = cmd.ProcessState.ExitCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
slog.Info(fmt.Sprintf("server shutdown with exit code %d", code))
|
||||||
|
done <- code
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
crashCount++
|
||||||
|
slog.Warn(fmt.Sprintf("server crash %d - exit code %d - respawning", crashCount, code))
|
||||||
|
time.Sleep(500 * time.Millisecond * time.Duration(crashCount))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return done, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isServerRunning(ctx context.Context) bool {
|
||||||
|
client, err := api.ClientFromEnvironment()
|
||||||
|
if err != nil {
|
||||||
|
slog.Info("unable to connect to server")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
err = client.Heartbeat(ctx)
|
||||||
|
if err != nil {
|
||||||
|
slog.Debug(fmt.Sprintf("heartbeat from server: %s", err))
|
||||||
|
slog.Info("unable to connect to server")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
36
app/server_darwin.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getCmd(ctx context.Context, cmd string) *exec.Cmd {
|
||||||
|
return exec.CommandContext(ctx, cmd, "serve")
|
||||||
|
}
|
||||||
|
|
||||||
|
func terminate(cmd *exec.Cmd) error {
|
||||||
|
return cmd.Process.Signal(os.Interrupt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isProcessExited(pid int) (bool, error) {
|
||||||
|
proc, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to find process: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = proc.Signal(syscall.Signal(0))
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrProcessDone) || errors.Is(err, syscall.ESRCH) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, fmt.Errorf("error signaling process: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
89
app/server_windows.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getCmd(ctx context.Context, exePath string) *exec.Cmd {
|
||||||
|
cmd := exec.CommandContext(ctx, exePath, "serve")
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
HideWindow: true,
|
||||||
|
CreationFlags: windows.CREATE_NEW_PROCESS_GROUP,
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func terminate(cmd *exec.Cmd) error {
|
||||||
|
dll, err := windows.LoadDLL("kernel32.dll")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dll.Release() // nolint: errcheck
|
||||||
|
|
||||||
|
pid := cmd.Process.Pid
|
||||||
|
|
||||||
|
f, err := dll.FindProc("AttachConsole")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r1, _, err := f.Call(uintptr(pid))
|
||||||
|
if r1 == 0 && err != syscall.ERROR_ACCESS_DENIED {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err = dll.FindProc("SetConsoleCtrlHandler")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r1, _, err = f.Call(0, 1)
|
||||||
|
if r1 == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err = dll.FindProc("GenerateConsoleCtrlEvent")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r1, _, err = f.Call(windows.CTRL_BREAK_EVENT, uintptr(pid))
|
||||||
|
if r1 == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r1, _, err = f.Call(windows.CTRL_C_EVENT, uintptr(pid))
|
||||||
|
if r1 == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const STILL_ACTIVE = 259
|
||||||
|
|
||||||
|
func isProcessExited(pid int) (bool, error) {
|
||||||
|
hProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, uint32(pid))
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to open process: %v", err)
|
||||||
|
}
|
||||||
|
defer windows.CloseHandle(hProcess) // nolint: errcheck
|
||||||
|
|
||||||
|
var exitCode uint32
|
||||||
|
err = windows.GetExitCodeProcess(hProcess, &exitCode)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to get exit code: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exitCode == STILL_ACTIVE {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
@@ -1,34 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag {
|
|
||||||
-webkit-app-region: drag;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-drag {
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blink {
|
|
||||||
-webkit-animation: 1s blink step-end infinite;
|
|
||||||
-moz-animation: 1s blink step-end infinite;
|
|
||||||
-ms-animation: 1s blink step-end infinite;
|
|
||||||
-o-animation: 1s blink step-end infinite;
|
|
||||||
animation: 1s blink step-end infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blink {
|
|
||||||
from,
|
|
||||||
to {
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
}
|
|
122
app/src/app.tsx
@@ -1,122 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import copy from 'copy-to-clipboard'
|
|
||||||
import { CheckIcon, DocumentDuplicateIcon } from '@heroicons/react/24/outline'
|
|
||||||
import Store from 'electron-store'
|
|
||||||
import { getCurrentWindow, app } from '@electron/remote'
|
|
||||||
|
|
||||||
import { install } from './install'
|
|
||||||
import OllamaIcon from './ollama.svg'
|
|
||||||
|
|
||||||
const store = new Store()
|
|
||||||
|
|
||||||
enum Step {
|
|
||||||
WELCOME = 0,
|
|
||||||
CLI,
|
|
||||||
FINISH,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function () {
|
|
||||||
const [step, setStep] = useState<Step>(Step.WELCOME)
|
|
||||||
const [commandCopied, setCommandCopied] = useState<boolean>(false)
|
|
||||||
|
|
||||||
const command = 'ollama run llama2'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='drag'>
|
|
||||||
<div className='mx-auto flex min-h-screen w-full flex-col justify-between bg-white px-4 pt-16'>
|
|
||||||
{step === Step.WELCOME && (
|
|
||||||
<>
|
|
||||||
<div className='mx-auto text-center'>
|
|
||||||
<h1 className='mb-6 mt-4 text-2xl tracking-tight text-gray-900'>Welcome to Ollama</h1>
|
|
||||||
<p className='mx-auto w-[65%] text-sm text-gray-400'>
|
|
||||||
Let's get you up and running with your own large language models.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={() => setStep(Step.CLI)}
|
|
||||||
className='no-drag rounded-dm mx-auto my-8 w-[40%] rounded-md bg-black px-4 py-2 text-sm text-white hover:brightness-110'
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className='mx-auto'>
|
|
||||||
<OllamaIcon />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{step === Step.CLI && (
|
|
||||||
<>
|
|
||||||
<div className='mx-auto flex flex-col space-y-28 text-center'>
|
|
||||||
<h1 className='mt-4 text-2xl tracking-tight text-gray-900'>Install the command line</h1>
|
|
||||||
<pre className='mx-auto text-4xl text-gray-400'>> ollama</pre>
|
|
||||||
<div className='mx-auto'>
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
await install()
|
|
||||||
setStep(Step.FINISH)
|
|
||||||
} catch (e) {
|
|
||||||
console.error('could not install: ', e)
|
|
||||||
} finally {
|
|
||||||
getCurrentWindow().show()
|
|
||||||
getCurrentWindow().focus()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className='no-drag rounded-dm mx-auto w-[60%] rounded-md bg-black px-4 py-2 text-sm text-white hover:brightness-110'
|
|
||||||
>
|
|
||||||
Install
|
|
||||||
</button>
|
|
||||||
<p className='mx-auto my-4 w-[70%] text-xs text-gray-400'>
|
|
||||||
You will be prompted for administrator access
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{step === Step.FINISH && (
|
|
||||||
<>
|
|
||||||
<div className='mx-auto flex flex-col space-y-20 text-center'>
|
|
||||||
<h1 className='mt-4 text-2xl tracking-tight text-gray-900'>Run your first model</h1>
|
|
||||||
<div className='flex flex-col'>
|
|
||||||
<div className='group relative flex items-center'>
|
|
||||||
<pre className='language-none text-2xs w-full rounded-md bg-gray-100 px-4 py-3 text-start leading-normal'>
|
|
||||||
{command}
|
|
||||||
</pre>
|
|
||||||
<button
|
|
||||||
className={`no-drag absolute right-[5px] px-2 py-2 ${
|
|
||||||
commandCopied
|
|
||||||
? 'text-gray-900 opacity-100 hover:cursor-auto'
|
|
||||||
: 'text-gray-200 opacity-50 hover:cursor-pointer'
|
|
||||||
} hover:font-bold hover:text-gray-900 group-hover:opacity-100`}
|
|
||||||
onClick={() => {
|
|
||||||
copy(command)
|
|
||||||
setCommandCopied(true)
|
|
||||||
setTimeout(() => setCommandCopied(false), 3000)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{commandCopied ? (
|
|
||||||
<CheckIcon className='h-4 w-4 font-bold text-gray-500' />
|
|
||||||
) : (
|
|
||||||
<DocumentDuplicateIcon className='h-4 w-4 text-gray-500' />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p className='mx-auto my-4 w-[70%] text-xs text-gray-400'>
|
|
||||||
Run this command in your favorite terminal.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
store.set('first-time-run', true)
|
|
||||||
window.close()
|
|
||||||
}}
|
|
||||||
className='no-drag rounded-dm mx-auto w-[60%] rounded-md bg-black px-4 py-2 text-sm text-white hover:brightness-110'
|
|
||||||
>
|
|
||||||
Finish
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
4
app/src/declarations.d.ts
vendored
@@ -1,4 +0,0 @@
|
|||||||
declare module '*.svg' {
|
|
||||||
const content: string
|
|
||||||
export default content
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
302
app/src/index.ts
@@ -1,302 +0,0 @@
|
|||||||
import { spawn, ChildProcess } from 'child_process'
|
|
||||||
import { app, autoUpdater, dialog, Tray, Menu, BrowserWindow, MenuItemConstructorOptions, nativeTheme } from 'electron'
|
|
||||||
import Store from 'electron-store'
|
|
||||||
import winston from 'winston'
|
|
||||||
import 'winston-daily-rotate-file'
|
|
||||||
import * as path from 'path'
|
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { installed } from './install'
|
|
||||||
|
|
||||||
require('@electron/remote/main').initialize()
|
|
||||||
|
|
||||||
if (require('electron-squirrel-startup')) {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = new Store()
|
|
||||||
|
|
||||||
let welcomeWindow: BrowserWindow | null = null
|
|
||||||
|
|
||||||
declare const MAIN_WINDOW_WEBPACK_ENTRY: string
|
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
|
||||||
transports: [
|
|
||||||
new winston.transports.Console(),
|
|
||||||
new winston.transports.File({
|
|
||||||
filename: path.join(app.getPath('home'), '.ollama', 'logs', 'server.log'),
|
|
||||||
maxsize: 1024 * 1024 * 20,
|
|
||||||
maxFiles: 5,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
format: winston.format.printf(info => info.message),
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('ready', () => {
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock()
|
|
||||||
if (!gotTheLock) {
|
|
||||||
app.exit(0)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on('second-instance', () => {
|
|
||||||
if (app.hasSingleInstanceLock()) {
|
|
||||||
app.releaseSingleInstanceLock()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (proc) {
|
|
||||||
proc.off('exit', restart)
|
|
||||||
proc.kill()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.exit(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.focus({ steal: true })
|
|
||||||
|
|
||||||
init()
|
|
||||||
})
|
|
||||||
|
|
||||||
function firstRunWindow() {
|
|
||||||
// Create the browser window.
|
|
||||||
welcomeWindow = new BrowserWindow({
|
|
||||||
width: 400,
|
|
||||||
height: 500,
|
|
||||||
frame: false,
|
|
||||||
fullscreenable: false,
|
|
||||||
resizable: false,
|
|
||||||
movable: true,
|
|
||||||
show: false,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: true,
|
|
||||||
contextIsolation: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
require('@electron/remote/main').enable(welcomeWindow.webContents)
|
|
||||||
|
|
||||||
welcomeWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY)
|
|
||||||
welcomeWindow.on('ready-to-show', () => welcomeWindow.show())
|
|
||||||
welcomeWindow.on('closed', () => {
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
app.dock.hide()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let tray: Tray | null = null
|
|
||||||
let updateAvailable = false
|
|
||||||
const assetPath = app.isPackaged ? process.resourcesPath : path.join(__dirname, '..', '..', 'assets')
|
|
||||||
|
|
||||||
function trayIconPath() {
|
|
||||||
return nativeTheme.shouldUseDarkColors
|
|
||||||
? updateAvailable
|
|
||||||
? path.join(assetPath, 'iconDarkUpdateTemplate.png')
|
|
||||||
: path.join(assetPath, 'iconDarkTemplate.png')
|
|
||||||
: updateAvailable
|
|
||||||
? path.join(assetPath, 'iconUpdateTemplate.png')
|
|
||||||
: path.join(assetPath, 'iconTemplate.png')
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTrayIcon() {
|
|
||||||
if (tray) {
|
|
||||||
tray.setImage(trayIconPath())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTray() {
|
|
||||||
const updateItems: MenuItemConstructorOptions[] = [
|
|
||||||
{ label: 'An update is available', enabled: false },
|
|
||||||
{
|
|
||||||
label: 'Restart to update',
|
|
||||||
click: () => autoUpdater.quitAndInstall(),
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const menu = Menu.buildFromTemplate([
|
|
||||||
...(updateAvailable ? updateItems : []),
|
|
||||||
{ role: 'quit', label: 'Quit Ollama', accelerator: 'Command+Q' },
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!tray) {
|
|
||||||
tray = new Tray(trayIconPath())
|
|
||||||
}
|
|
||||||
|
|
||||||
tray.setToolTip(updateAvailable ? 'An update is available' : 'Ollama')
|
|
||||||
tray.setContextMenu(menu)
|
|
||||||
tray.setImage(trayIconPath())
|
|
||||||
|
|
||||||
nativeTheme.off('updated', updateTrayIcon)
|
|
||||||
nativeTheme.on('updated', updateTrayIcon)
|
|
||||||
}
|
|
||||||
|
|
||||||
let proc: ChildProcess = null
|
|
||||||
|
|
||||||
function server() {
|
|
||||||
const binary = app.isPackaged
|
|
||||||
? path.join(process.resourcesPath, 'ollama')
|
|
||||||
: path.resolve(process.cwd(), '..', 'ollama')
|
|
||||||
|
|
||||||
proc = spawn(binary, ['serve'])
|
|
||||||
|
|
||||||
proc.stdout.on('data', data => {
|
|
||||||
logger.info(data.toString().trim())
|
|
||||||
})
|
|
||||||
|
|
||||||
proc.stderr.on('data', data => {
|
|
||||||
logger.error(data.toString().trim())
|
|
||||||
})
|
|
||||||
|
|
||||||
proc.on('exit', restart)
|
|
||||||
}
|
|
||||||
|
|
||||||
function restart() {
|
|
||||||
setTimeout(server, 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on('before-quit', () => {
|
|
||||||
if (proc) {
|
|
||||||
proc.off('exit', restart)
|
|
||||||
proc.kill('SIGINT') // send SIGINT signal to the server, which also stops any loaded llms
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateURL = `https://ollama.ai/api/update?os=${process.platform}&arch=${
|
|
||||||
process.arch
|
|
||||||
}&version=${app.getVersion()}&id=${id()}`
|
|
||||||
|
|
||||||
let latest = ''
|
|
||||||
async function isNewReleaseAvailable() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(updateURL)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status === 204) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
const url = data?.url
|
|
||||||
if (!url) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (latest === url) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
latest = url
|
|
||||||
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`update check failed - ${error}`)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkUpdate() {
|
|
||||||
const available = await isNewReleaseAvailable()
|
|
||||||
if (available) {
|
|
||||||
logger.info('checking for update')
|
|
||||||
autoUpdater.checkForUpdates()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
if (app.isPackaged) {
|
|
||||||
checkUpdate()
|
|
||||||
setInterval(() => {
|
|
||||||
checkUpdate()
|
|
||||||
}, 60 * 60 * 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTray()
|
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
if (app.isPackaged) {
|
|
||||||
if (!app.isInApplicationsFolder()) {
|
|
||||||
const chosen = dialog.showMessageBoxSync({
|
|
||||||
type: 'question',
|
|
||||||
buttons: ['Move to Applications', 'Do Not Move'],
|
|
||||||
message: 'Ollama works best when run from the Applications directory.',
|
|
||||||
defaultId: 0,
|
|
||||||
cancelId: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (chosen === 0) {
|
|
||||||
try {
|
|
||||||
app.moveToApplicationsFolder({
|
|
||||||
conflictHandler: conflictType => {
|
|
||||||
if (conflictType === 'existsAndRunning') {
|
|
||||||
dialog.showMessageBoxSync({
|
|
||||||
type: 'info',
|
|
||||||
message: 'Cannot move to Applications directory',
|
|
||||||
detail:
|
|
||||||
'Another version of Ollama is currently running from your Applications directory. Close it first and try again.',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(`[Move to Applications] Failed to move to applications folder - ${e.message}}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server()
|
|
||||||
|
|
||||||
if (store.get('first-time-run') && installed()) {
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
app.dock.hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.setLoginItemSettings({ openAtLogin: app.getLoginItemSettings().openAtLogin })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is the first run or the CLI is no longer installed
|
|
||||||
app.setLoginItemSettings({ openAtLogin: true })
|
|
||||||
firstRunWindow()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
|
||||||
// for applications and their menu bar to stay active until the user quits
|
|
||||||
// explicitly with Cmd + Q.
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function id(): string {
|
|
||||||
const id = store.get('id') as string
|
|
||||||
|
|
||||||
if (id) {
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
const uuid = uuidv4()
|
|
||||||
store.set('id', uuid)
|
|
||||||
return uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
autoUpdater.setFeedURL({ url: updateURL })
|
|
||||||
|
|
||||||
autoUpdater.on('error', e => {
|
|
||||||
logger.error(`update check failed - ${e.message}`)
|
|
||||||
console.error(`update check failed - ${e.message}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
autoUpdater.on('update-downloaded', () => {
|
|
||||||
updateAvailable = true
|
|
||||||
updateTray()
|
|
||||||
})
|
|
@@ -1,21 +0,0 @@
|
|||||||
import * as fs from 'fs'
|
|
||||||
import { exec as cbExec } from 'child_process'
|
|
||||||
import * as path from 'path'
|
|
||||||
import { promisify } from 'util'
|
|
||||||
|
|
||||||
const app = process && process.type === 'renderer' ? require('@electron/remote').app : require('electron').app
|
|
||||||
const ollama = app.isPackaged ? path.join(process.resourcesPath, 'ollama') : path.resolve(process.cwd(), '..', 'ollama')
|
|
||||||
const exec = promisify(cbExec)
|
|
||||||
const symlinkPath = '/usr/local/bin/ollama'
|
|
||||||
|
|
||||||
export function installed() {
|
|
||||||
return fs.existsSync(symlinkPath) && fs.readlinkSync(symlinkPath) === ollama
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function install() {
|
|
||||||
const command = `do shell script "mkdir -p ${path.dirname(
|
|
||||||
symlinkPath
|
|
||||||
)} && ln -F -s \\"${ollama}\\" \\"${symlinkPath}\\"" with administrator privileges`
|
|
||||||
|
|
||||||
await exec(`osascript -e '${command}'`)
|
|
||||||
}
|
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,7 +0,0 @@
|
|||||||
import App from './app'
|
|
||||||
import './app.css'
|
|
||||||
import { createRoot } from 'react-dom/client'
|
|
||||||
|
|
||||||
const container = document.getElementById('app')
|
|
||||||
const root = createRoot(container)
|
|
||||||
root.render(<App />)
|
|
98
app/store/store.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
FirstTimeRun bool `json:"first-time-run"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
lock sync.Mutex
|
||||||
|
store Store
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetID() string {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
if store.ID == "" {
|
||||||
|
initStore()
|
||||||
|
}
|
||||||
|
return store.ID
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFirstTimeRun() bool {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
if store.ID == "" {
|
||||||
|
initStore()
|
||||||
|
}
|
||||||
|
return store.FirstTimeRun
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetFirstTimeRun(val bool) {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
if store.FirstTimeRun == val {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
store.FirstTimeRun = val
|
||||||
|
writeStore(getStorePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
// lock must be held
|
||||||
|
func initStore() {
|
||||||
|
storeFile, err := os.Open(getStorePath())
|
||||||
|
if err == nil {
|
||||||
|
defer storeFile.Close()
|
||||||
|
err = json.NewDecoder(storeFile).Decode(&store)
|
||||||
|
if err == nil {
|
||||||
|
slog.Debug(fmt.Sprintf("loaded existing store %s - ID: %s", getStorePath(), store.ID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
slog.Debug(fmt.Sprintf("unexpected error searching for store: %s", err))
|
||||||
|
}
|
||||||
|
slog.Debug("initializing new store")
|
||||||
|
store.ID = uuid.New().String()
|
||||||
|
writeStore(getStorePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeStore(storeFilename string) {
|
||||||
|
ollamaDir := filepath.Dir(storeFilename)
|
||||||
|
_, err := os.Stat(ollamaDir)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err := os.MkdirAll(ollamaDir, 0o755); err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("create ollama dir %s: %v", ollamaDir, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload, err := json.Marshal(store)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to marshal store: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fp, err := os.OpenFile(storeFilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("write store payload %s: %v", storeFilename, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
if n, err := fp.Write(payload); err != nil || n != len(payload) {
|
||||||
|
slog.Error(fmt.Sprintf("write store payload %s: %d vs %d -- %v", storeFilename, n, len(payload), err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slog.Debug("Store contents: " + string(payload))
|
||||||
|
slog.Info(fmt.Sprintf("wrote store: %s", storeFilename))
|
||||||
|
}
|
13
app/store/store_darwin.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getStorePath() string {
|
||||||
|
// TODO - system wide location?
|
||||||
|
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
return filepath.Join(home, "Library", "Application Support", "Ollama", "config.json")
|
||||||
|
}
|
16
app/store/store_linux.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getStorePath() string {
|
||||||
|
if os.Geteuid() == 0 {
|
||||||
|
// TODO where should we store this on linux for system-wide operation?
|
||||||
|
return "/etc/ollama/config.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
return filepath.Join(home, ".ollama", "config.json")
|
||||||
|
}
|
11
app/store/store_windows.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getStorePath() string {
|
||||||
|
localAppData := os.Getenv("LOCALAPPDATA")
|
||||||
|
return filepath.Join(localAppData, "Ollama", "config.json")
|
||||||
|
}
|
@@ -1,6 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
|
|
||||||
theme: {},
|
|
||||||
plugins: [],
|
|
||||||
}
|
|
24
app/tray/commontray/types.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package commontray
|
||||||
|
|
||||||
|
var (
|
||||||
|
Title = "Ollama"
|
||||||
|
ToolTip = "Ollama"
|
||||||
|
|
||||||
|
UpdateIconName = "tray_upgrade"
|
||||||
|
IconName = "tray"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Callbacks struct {
|
||||||
|
Quit chan struct{}
|
||||||
|
Update chan struct{}
|
||||||
|
DoFirstUse chan struct{}
|
||||||
|
ShowLogs chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type OllamaTray interface {
|
||||||
|
GetCallbacks() Callbacks
|
||||||
|
Run()
|
||||||
|
UpdateAvailable(ver string) error
|
||||||
|
DisplayFirstUseNotification() error
|
||||||
|
Quit()
|
||||||
|
}
|
28
app/tray/tray.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package tray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/app/assets"
|
||||||
|
"github.com/ollama/ollama/app/tray/commontray"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewTray() (commontray.OllamaTray, error) {
|
||||||
|
extension := ".png"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
extension = ".ico"
|
||||||
|
}
|
||||||
|
iconName := commontray.UpdateIconName + extension
|
||||||
|
updateIcon, err := assets.GetIcon(iconName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load icon %s: %w", iconName, err)
|
||||||
|
}
|
||||||
|
iconName = commontray.IconName + extension
|
||||||
|
icon, err := assets.GetIcon(iconName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load icon %s: %w", iconName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return InitPlatformTray(icon, updateIcon)
|
||||||
|
}
|
11
app/tray/tray_darwin.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package tray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/app/tray/commontray"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitPlatformTray(icon, updateIcon []byte) (commontray.OllamaTray, error) {
|
||||||
|
return nil, fmt.Errorf("NOT IMPLEMENTED YET")
|
||||||
|
}
|
10
app/tray/tray_windows.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package tray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ollama/ollama/app/tray/commontray"
|
||||||
|
"github.com/ollama/ollama/app/tray/wintray"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitPlatformTray(icon, updateIcon []byte) (commontray.OllamaTray, error) {
|
||||||
|
return wintray.InitTray(icon, updateIcon)
|
||||||
|
}
|
184
app/tray/wintray/eventloop.go
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package wintray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
quitOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *winTray) Run() {
|
||||||
|
nativeLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func nativeLoop() {
|
||||||
|
// Main message pump.
|
||||||
|
slog.Debug("starting event handling loop")
|
||||||
|
m := &struct {
|
||||||
|
WindowHandle windows.Handle
|
||||||
|
Message uint32
|
||||||
|
Wparam uintptr
|
||||||
|
Lparam uintptr
|
||||||
|
Time uint32
|
||||||
|
Pt point
|
||||||
|
LPrivate uint32
|
||||||
|
}{}
|
||||||
|
for {
|
||||||
|
ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0)
|
||||||
|
|
||||||
|
// If the function retrieves a message other than WM_QUIT, the return value is nonzero.
|
||||||
|
// If the function retrieves the WM_QUIT message, the return value is zero.
|
||||||
|
// If there is an error, the return value is -1
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx
|
||||||
|
switch int32(ret) {
|
||||||
|
case -1:
|
||||||
|
slog.Error(fmt.Sprintf("get message failure: %v", err))
|
||||||
|
return
|
||||||
|
case 0:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
pTranslateMessage.Call(uintptr(unsafe.Pointer(m))) //nolint:errcheck
|
||||||
|
pDispatchMessage.Call(uintptr(unsafe.Pointer(m))) //nolint:errcheck
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowProc callback function that processes messages sent to a window.
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633573(v=vs.85).aspx
|
||||||
|
func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam uintptr) (lResult uintptr) {
|
||||||
|
const (
|
||||||
|
WM_RBUTTONUP = 0x0205
|
||||||
|
WM_LBUTTONUP = 0x0202
|
||||||
|
WM_COMMAND = 0x0111
|
||||||
|
WM_ENDSESSION = 0x0016
|
||||||
|
WM_CLOSE = 0x0010
|
||||||
|
WM_DESTROY = 0x0002
|
||||||
|
WM_MOUSEMOVE = 0x0200
|
||||||
|
WM_LBUTTONDOWN = 0x0201
|
||||||
|
)
|
||||||
|
switch message {
|
||||||
|
case WM_COMMAND:
|
||||||
|
menuItemId := int32(wParam)
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command#menus
|
||||||
|
switch menuItemId {
|
||||||
|
case quitMenuID:
|
||||||
|
select {
|
||||||
|
case t.callbacks.Quit <- struct{}{}:
|
||||||
|
// should not happen but in case not listening
|
||||||
|
default:
|
||||||
|
slog.Error("no listener on Quit")
|
||||||
|
}
|
||||||
|
case updateMenuID:
|
||||||
|
select {
|
||||||
|
case t.callbacks.Update <- struct{}{}:
|
||||||
|
// should not happen but in case not listening
|
||||||
|
default:
|
||||||
|
slog.Error("no listener on Update")
|
||||||
|
}
|
||||||
|
case diagLogsMenuID:
|
||||||
|
select {
|
||||||
|
case t.callbacks.ShowLogs <- struct{}{}:
|
||||||
|
// should not happen but in case not listening
|
||||||
|
default:
|
||||||
|
slog.Error("no listener on ShowLogs")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
slog.Debug(fmt.Sprintf("Unexpected menu item id: %d", menuItemId))
|
||||||
|
}
|
||||||
|
case WM_CLOSE:
|
||||||
|
boolRet, _, err := pDestroyWindow.Call(uintptr(t.window))
|
||||||
|
if boolRet == 0 {
|
||||||
|
slog.Error(fmt.Sprintf("failed to destroy window: %s", err))
|
||||||
|
}
|
||||||
|
err = t.wcex.unregister()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to uregister windo %s", err))
|
||||||
|
}
|
||||||
|
case WM_DESTROY:
|
||||||
|
// same as WM_ENDSESSION, but throws 0 exit code after all
|
||||||
|
defer pPostQuitMessage.Call(uintptr(int32(0))) //nolint:errcheck
|
||||||
|
fallthrough
|
||||||
|
case WM_ENDSESSION:
|
||||||
|
t.muNID.Lock()
|
||||||
|
if t.nid != nil {
|
||||||
|
err := t.nid.delete()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to delete nid: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.muNID.Unlock()
|
||||||
|
case t.wmSystrayMessage:
|
||||||
|
switch lParam {
|
||||||
|
case WM_MOUSEMOVE, WM_LBUTTONDOWN:
|
||||||
|
// Ignore these...
|
||||||
|
case WM_RBUTTONUP, WM_LBUTTONUP:
|
||||||
|
err := t.showMenu()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to show menu: %s", err))
|
||||||
|
}
|
||||||
|
case 0x405: // TODO - how is this magic value derived for the notification left click
|
||||||
|
if t.pendingUpdate {
|
||||||
|
select {
|
||||||
|
case t.callbacks.Update <- struct{}{}:
|
||||||
|
// should not happen but in case not listening
|
||||||
|
default:
|
||||||
|
slog.Error("no listener on Update")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
select {
|
||||||
|
case t.callbacks.DoFirstUse <- struct{}{}:
|
||||||
|
// should not happen but in case not listening
|
||||||
|
default:
|
||||||
|
slog.Error("no listener on DoFirstUse")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 0x404: // Middle click or close notification
|
||||||
|
// slog.Debug("doing nothing on close of first time notification")
|
||||||
|
default:
|
||||||
|
// 0x402 also seems common - what is it?
|
||||||
|
slog.Debug(fmt.Sprintf("unmanaged app message, lParm: 0x%x", lParam))
|
||||||
|
}
|
||||||
|
case t.wmTaskbarCreated: // on explorer.exe restarts
|
||||||
|
t.muNID.Lock()
|
||||||
|
err := t.nid.add()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to refresh the taskbar on explorer restart: %s", err))
|
||||||
|
}
|
||||||
|
t.muNID.Unlock()
|
||||||
|
default:
|
||||||
|
// Calls the default window procedure to provide default processing for any window messages that an application does not process.
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633572(v=vs.85).aspx
|
||||||
|
lResult, _, _ = pDefWindowProc.Call(
|
||||||
|
uintptr(hWnd),
|
||||||
|
uintptr(message),
|
||||||
|
uintptr(wParam),
|
||||||
|
uintptr(lParam),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) Quit() {
|
||||||
|
quitOnce.Do(quit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func quit() {
|
||||||
|
boolRet, _, err := pPostMessage.Call(
|
||||||
|
uintptr(wt.window),
|
||||||
|
WM_CLOSE,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if boolRet == 0 {
|
||||||
|
slog.Error(fmt.Sprintf("failed to post close message on shutdown %s", err))
|
||||||
|
}
|
||||||
|
}
|
71
app/tray/wintray/menus.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package wintray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
updatAvailableMenuID = 1
|
||||||
|
updateMenuID = updatAvailableMenuID + 1
|
||||||
|
separatorMenuID = updateMenuID + 1
|
||||||
|
diagLogsMenuID = separatorMenuID + 1
|
||||||
|
diagSeparatorMenuID = diagLogsMenuID + 1
|
||||||
|
quitMenuID = diagSeparatorMenuID + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *winTray) initMenus() error {
|
||||||
|
if err := t.addOrUpdateMenuItem(diagLogsMenuID, 0, diagLogsMenuTitle, false); err != nil {
|
||||||
|
return fmt.Errorf("unable to create menu entries %w\n", err)
|
||||||
|
}
|
||||||
|
if err := t.addSeparatorMenuItem(diagSeparatorMenuID, 0); err != nil {
|
||||||
|
return fmt.Errorf("unable to create menu entries %w", err)
|
||||||
|
}
|
||||||
|
if err := t.addOrUpdateMenuItem(quitMenuID, 0, quitMenuTitle, false); err != nil {
|
||||||
|
return fmt.Errorf("unable to create menu entries %w\n", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) UpdateAvailable(ver string) error {
|
||||||
|
if !t.updateNotified {
|
||||||
|
slog.Debug("updating menu and sending notification for new update")
|
||||||
|
if err := t.addOrUpdateMenuItem(updatAvailableMenuID, 0, updateAvailableMenuTitle, true); err != nil {
|
||||||
|
return fmt.Errorf("unable to create menu entries %w", err)
|
||||||
|
}
|
||||||
|
if err := t.addOrUpdateMenuItem(updateMenuID, 0, updateMenutTitle, false); err != nil {
|
||||||
|
return fmt.Errorf("unable to create menu entries %w", err)
|
||||||
|
}
|
||||||
|
if err := t.addSeparatorMenuItem(separatorMenuID, 0); err != nil {
|
||||||
|
return fmt.Errorf("unable to create menu entries %w", err)
|
||||||
|
}
|
||||||
|
iconFilePath, err := iconBytesToFilePath(wt.updateIcon)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to write icon data to temp file: %w", err)
|
||||||
|
}
|
||||||
|
if err := wt.setIcon(iconFilePath); err != nil {
|
||||||
|
return fmt.Errorf("unable to set icon: %w", err)
|
||||||
|
}
|
||||||
|
t.updateNotified = true
|
||||||
|
|
||||||
|
t.pendingUpdate = true
|
||||||
|
// Now pop up the notification
|
||||||
|
t.muNID.Lock()
|
||||||
|
defer t.muNID.Unlock()
|
||||||
|
copy(t.nid.InfoTitle[:], windows.StringToUTF16(updateTitle))
|
||||||
|
copy(t.nid.Info[:], windows.StringToUTF16(fmt.Sprintf(updateMessage, ver)))
|
||||||
|
t.nid.Flags |= NIF_INFO
|
||||||
|
t.nid.Timeout = 10
|
||||||
|
t.nid.Size = uint32(unsafe.Sizeof(*wt.nid))
|
||||||
|
err = t.nid.modify()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
15
app/tray/wintray/messages.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package wintray
|
||||||
|
|
||||||
|
const (
|
||||||
|
firstTimeTitle = "Ollama is running"
|
||||||
|
firstTimeMessage = "Click here to get started"
|
||||||
|
updateTitle = "Update available"
|
||||||
|
updateMessage = "Ollama version %s is ready to install"
|
||||||
|
|
||||||
|
quitMenuTitle = "Quit Ollama"
|
||||||
|
updateAvailableMenuTitle = "An update is available"
|
||||||
|
updateMenutTitle = "Restart to update"
|
||||||
|
diagLogsMenuTitle = "View logs"
|
||||||
|
)
|
66
app/tray/wintray/notifyicon.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package wintray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Contains information that the system needs to display notifications in the notification area.
|
||||||
|
// Used by Shell_NotifyIcon.
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/bb773352(v=vs.85).aspx
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159
|
||||||
|
type notifyIconData struct {
|
||||||
|
Size uint32
|
||||||
|
Wnd windows.Handle
|
||||||
|
ID, Flags, CallbackMessage uint32
|
||||||
|
Icon windows.Handle
|
||||||
|
Tip [128]uint16
|
||||||
|
State, StateMask uint32
|
||||||
|
Info [256]uint16
|
||||||
|
// Timeout, Version uint32
|
||||||
|
Timeout uint32
|
||||||
|
|
||||||
|
InfoTitle [64]uint16
|
||||||
|
InfoFlags uint32
|
||||||
|
GuidItem windows.GUID
|
||||||
|
BalloonIcon windows.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nid *notifyIconData) add() error {
|
||||||
|
const NIM_ADD = 0x00000000
|
||||||
|
res, _, err := pShellNotifyIcon.Call(
|
||||||
|
uintptr(NIM_ADD),
|
||||||
|
uintptr(unsafe.Pointer(nid)),
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nid *notifyIconData) modify() error {
|
||||||
|
const NIM_MODIFY = 0x00000001
|
||||||
|
res, _, err := pShellNotifyIcon.Call(
|
||||||
|
uintptr(NIM_MODIFY),
|
||||||
|
uintptr(unsafe.Pointer(nid)),
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nid *notifyIconData) delete() error {
|
||||||
|
const NIM_DELETE = 0x00000002
|
||||||
|
res, _, err := pShellNotifyIcon.Call(
|
||||||
|
uintptr(NIM_DELETE),
|
||||||
|
uintptr(unsafe.Pointer(nid)),
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
485
app/tray/wintray/tray.go
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package wintray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/app/tray/commontray"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Helpful sources: https://github.com/golang/exp/blob/master/shiny/driver/internal/win32
|
||||||
|
|
||||||
|
// Contains information about loaded resources
|
||||||
|
type winTray struct {
|
||||||
|
instance,
|
||||||
|
icon,
|
||||||
|
cursor,
|
||||||
|
window windows.Handle
|
||||||
|
|
||||||
|
loadedImages map[string]windows.Handle
|
||||||
|
muLoadedImages sync.RWMutex
|
||||||
|
|
||||||
|
// menus keeps track of the submenus keyed by the menu item ID, plus 0
|
||||||
|
// which corresponds to the main popup menu.
|
||||||
|
menus map[uint32]windows.Handle
|
||||||
|
muMenus sync.RWMutex
|
||||||
|
menuOf map[uint32]windows.Handle
|
||||||
|
muMenuOf sync.RWMutex
|
||||||
|
// menuItemIcons maintains the bitmap of each menu item (if applies). It's
|
||||||
|
// needed to show the icon correctly when showing a previously hidden menu
|
||||||
|
// item again.
|
||||||
|
// menuItemIcons map[uint32]windows.Handle
|
||||||
|
// muMenuItemIcons sync.RWMutex
|
||||||
|
visibleItems map[uint32][]uint32
|
||||||
|
muVisibleItems sync.RWMutex
|
||||||
|
|
||||||
|
nid *notifyIconData
|
||||||
|
muNID sync.RWMutex
|
||||||
|
wcex *wndClassEx
|
||||||
|
|
||||||
|
wmSystrayMessage,
|
||||||
|
wmTaskbarCreated uint32
|
||||||
|
|
||||||
|
pendingUpdate bool
|
||||||
|
updateNotified bool // Only pop up the notification once - TODO consider daily nag?
|
||||||
|
// Callbacks
|
||||||
|
callbacks commontray.Callbacks
|
||||||
|
normalIcon []byte
|
||||||
|
updateIcon []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var wt winTray
|
||||||
|
|
||||||
|
func (t *winTray) GetCallbacks() commontray.Callbacks {
|
||||||
|
return t.callbacks
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitTray(icon, updateIcon []byte) (*winTray, error) {
|
||||||
|
wt.callbacks.Quit = make(chan struct{})
|
||||||
|
wt.callbacks.Update = make(chan struct{})
|
||||||
|
wt.callbacks.ShowLogs = make(chan struct{})
|
||||||
|
wt.callbacks.DoFirstUse = make(chan struct{})
|
||||||
|
wt.normalIcon = icon
|
||||||
|
wt.updateIcon = updateIcon
|
||||||
|
if err := wt.initInstance(); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to init instance: %w\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wt.createMenu(); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to create menu: %w\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
iconFilePath, err := iconBytesToFilePath(wt.normalIcon)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to write icon data to temp file: %w", err)
|
||||||
|
}
|
||||||
|
if err := wt.setIcon(iconFilePath); err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to set icon: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &wt, wt.initMenus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) initInstance() error {
|
||||||
|
const (
|
||||||
|
className = "OllamaClass"
|
||||||
|
windowName = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
t.wmSystrayMessage = WM_USER + 1
|
||||||
|
t.visibleItems = make(map[uint32][]uint32)
|
||||||
|
t.menus = make(map[uint32]windows.Handle)
|
||||||
|
t.menuOf = make(map[uint32]windows.Handle)
|
||||||
|
|
||||||
|
t.loadedImages = make(map[string]windows.Handle)
|
||||||
|
|
||||||
|
taskbarEventNamePtr, _ := windows.UTF16PtrFromString("TaskbarCreated")
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms644947
|
||||||
|
res, _, err := pRegisterWindowMessage.Call(
|
||||||
|
uintptr(unsafe.Pointer(taskbarEventNamePtr)),
|
||||||
|
)
|
||||||
|
if res == 0 { // success 0xc000-0xfff
|
||||||
|
return fmt.Errorf("failed to register window: %w", err)
|
||||||
|
}
|
||||||
|
t.wmTaskbarCreated = uint32(res)
|
||||||
|
|
||||||
|
instanceHandle, _, err := pGetModuleHandle.Call(0)
|
||||||
|
if instanceHandle == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.instance = windows.Handle(instanceHandle)
|
||||||
|
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms648072(v=vs.85).aspx
|
||||||
|
iconHandle, _, err := pLoadIcon.Call(0, uintptr(IDI_APPLICATION))
|
||||||
|
if iconHandle == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.icon = windows.Handle(iconHandle)
|
||||||
|
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms648391(v=vs.85).aspx
|
||||||
|
cursorHandle, _, err := pLoadCursor.Call(0, uintptr(IDC_ARROW))
|
||||||
|
if cursorHandle == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.cursor = windows.Handle(cursorHandle)
|
||||||
|
|
||||||
|
classNamePtr, err := windows.UTF16PtrFromString(className)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
windowNamePtr, err := windows.UTF16PtrFromString(windowName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.wcex = &wndClassEx{
|
||||||
|
Style: CS_HREDRAW | CS_VREDRAW,
|
||||||
|
WndProc: windows.NewCallback(t.wndProc),
|
||||||
|
Instance: t.instance,
|
||||||
|
Icon: t.icon,
|
||||||
|
Cursor: t.cursor,
|
||||||
|
Background: windows.Handle(6), // (COLOR_WINDOW + 1)
|
||||||
|
ClassName: classNamePtr,
|
||||||
|
IconSm: t.icon,
|
||||||
|
}
|
||||||
|
if err := t.wcex.register(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
windowHandle, _, err := pCreateWindowEx.Call(
|
||||||
|
uintptr(0),
|
||||||
|
uintptr(unsafe.Pointer(classNamePtr)),
|
||||||
|
uintptr(unsafe.Pointer(windowNamePtr)),
|
||||||
|
uintptr(WS_OVERLAPPEDWINDOW),
|
||||||
|
uintptr(CW_USEDEFAULT),
|
||||||
|
uintptr(CW_USEDEFAULT),
|
||||||
|
uintptr(CW_USEDEFAULT),
|
||||||
|
uintptr(CW_USEDEFAULT),
|
||||||
|
uintptr(0),
|
||||||
|
uintptr(0),
|
||||||
|
uintptr(t.instance),
|
||||||
|
uintptr(0),
|
||||||
|
)
|
||||||
|
if windowHandle == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.window = windows.Handle(windowHandle)
|
||||||
|
|
||||||
|
pShowWindow.Call(uintptr(t.window), uintptr(SW_HIDE)) //nolint:errcheck
|
||||||
|
|
||||||
|
boolRet, _, err := pUpdateWindow.Call(uintptr(t.window))
|
||||||
|
if boolRet == 0 {
|
||||||
|
slog.Error(fmt.Sprintf("failed to update window: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.muNID.Lock()
|
||||||
|
defer t.muNID.Unlock()
|
||||||
|
t.nid = ¬ifyIconData{
|
||||||
|
Wnd: windows.Handle(t.window),
|
||||||
|
ID: 100,
|
||||||
|
Flags: NIF_MESSAGE,
|
||||||
|
CallbackMessage: t.wmSystrayMessage,
|
||||||
|
}
|
||||||
|
t.nid.Size = uint32(unsafe.Sizeof(*t.nid))
|
||||||
|
|
||||||
|
return t.nid.add()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) createMenu() error {
|
||||||
|
|
||||||
|
menuHandle, _, err := pCreatePopupMenu.Call()
|
||||||
|
if menuHandle == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.menus[0] = windows.Handle(menuHandle)
|
||||||
|
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647575(v=vs.85).aspx
|
||||||
|
mi := struct {
|
||||||
|
Size, Mask, Style, Max uint32
|
||||||
|
Background windows.Handle
|
||||||
|
ContextHelpID uint32
|
||||||
|
MenuData uintptr
|
||||||
|
}{
|
||||||
|
Mask: MIM_APPLYTOSUBMENUS,
|
||||||
|
}
|
||||||
|
mi.Size = uint32(unsafe.Sizeof(mi))
|
||||||
|
|
||||||
|
res, _, err := pSetMenuInfo.Call(
|
||||||
|
uintptr(t.menus[0]),
|
||||||
|
uintptr(unsafe.Pointer(&mi)),
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains information about a menu item.
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx
|
||||||
|
type menuItemInfo struct {
|
||||||
|
Size, Mask, Type, State uint32
|
||||||
|
ID uint32
|
||||||
|
SubMenu, Checked, Unchecked windows.Handle
|
||||||
|
ItemData uintptr
|
||||||
|
TypeData *uint16
|
||||||
|
Cch uint32
|
||||||
|
BMPItem windows.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) addOrUpdateMenuItem(menuItemId uint32, parentId uint32, title string, disabled bool) error {
|
||||||
|
titlePtr, err := windows.UTF16PtrFromString(title)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mi := menuItemInfo{
|
||||||
|
Mask: MIIM_FTYPE | MIIM_STRING | MIIM_ID | MIIM_STATE,
|
||||||
|
Type: MFT_STRING,
|
||||||
|
ID: uint32(menuItemId),
|
||||||
|
TypeData: titlePtr,
|
||||||
|
Cch: uint32(len(title)),
|
||||||
|
}
|
||||||
|
mi.Size = uint32(unsafe.Sizeof(mi))
|
||||||
|
if disabled {
|
||||||
|
mi.State |= MFS_DISABLED
|
||||||
|
}
|
||||||
|
|
||||||
|
var res uintptr
|
||||||
|
t.muMenus.RLock()
|
||||||
|
menu := t.menus[parentId]
|
||||||
|
t.muMenus.RUnlock()
|
||||||
|
if t.getVisibleItemIndex(parentId, menuItemId) != -1 {
|
||||||
|
// We set the menu item info based on the menuID
|
||||||
|
boolRet, _, err := pSetMenuItemInfo.Call(
|
||||||
|
uintptr(menu),
|
||||||
|
uintptr(menuItemId),
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&mi)),
|
||||||
|
)
|
||||||
|
if boolRet == 0 {
|
||||||
|
return fmt.Errorf("failed to set menu item: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if res == 0 {
|
||||||
|
// Menu item does not already exist, create it
|
||||||
|
t.muMenus.RLock()
|
||||||
|
submenu, exists := t.menus[menuItemId]
|
||||||
|
t.muMenus.RUnlock()
|
||||||
|
if exists {
|
||||||
|
mi.Mask |= MIIM_SUBMENU
|
||||||
|
mi.SubMenu = submenu
|
||||||
|
}
|
||||||
|
t.addToVisibleItems(parentId, menuItemId)
|
||||||
|
position := t.getVisibleItemIndex(parentId, menuItemId)
|
||||||
|
res, _, err = pInsertMenuItem.Call(
|
||||||
|
uintptr(menu),
|
||||||
|
uintptr(position),
|
||||||
|
1,
|
||||||
|
uintptr(unsafe.Pointer(&mi)),
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
t.delFromVisibleItems(parentId, menuItemId)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.muMenuOf.Lock()
|
||||||
|
t.menuOf[menuItemId] = menu
|
||||||
|
t.muMenuOf.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) addSeparatorMenuItem(menuItemId, parentId uint32) error {
|
||||||
|
|
||||||
|
mi := menuItemInfo{
|
||||||
|
Mask: MIIM_FTYPE | MIIM_ID | MIIM_STATE,
|
||||||
|
Type: MFT_SEPARATOR,
|
||||||
|
ID: uint32(menuItemId),
|
||||||
|
}
|
||||||
|
|
||||||
|
mi.Size = uint32(unsafe.Sizeof(mi))
|
||||||
|
|
||||||
|
t.addToVisibleItems(parentId, menuItemId)
|
||||||
|
position := t.getVisibleItemIndex(parentId, menuItemId)
|
||||||
|
t.muMenus.RLock()
|
||||||
|
menu := uintptr(t.menus[parentId])
|
||||||
|
t.muMenus.RUnlock()
|
||||||
|
res, _, err := pInsertMenuItem.Call(
|
||||||
|
menu,
|
||||||
|
uintptr(position),
|
||||||
|
1,
|
||||||
|
uintptr(unsafe.Pointer(&mi)),
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (t *winTray) hideMenuItem(menuItemId, parentId uint32) error {
|
||||||
|
// const ERROR_SUCCESS syscall.Errno = 0
|
||||||
|
|
||||||
|
// t.muMenus.RLock()
|
||||||
|
// menu := uintptr(t.menus[parentId])
|
||||||
|
// t.muMenus.RUnlock()
|
||||||
|
// res, _, err := pRemoveMenu.Call(
|
||||||
|
// menu,
|
||||||
|
// uintptr(menuItemId),
|
||||||
|
// MF_BYCOMMAND,
|
||||||
|
// )
|
||||||
|
// if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// t.delFromVisibleItems(parentId, menuItemId)
|
||||||
|
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (t *winTray) showMenu() error {
|
||||||
|
p := point{}
|
||||||
|
boolRet, _, err := pGetCursorPos.Call(uintptr(unsafe.Pointer(&p)))
|
||||||
|
if boolRet == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
boolRet, _, err = pSetForegroundWindow.Call(uintptr(t.window))
|
||||||
|
if boolRet == 0 {
|
||||||
|
slog.Warn(fmt.Sprintf("failed to bring menu to foreground: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
boolRet, _, err = pTrackPopupMenu.Call(
|
||||||
|
uintptr(t.menus[0]),
|
||||||
|
TPM_BOTTOMALIGN|TPM_LEFTALIGN,
|
||||||
|
uintptr(p.X),
|
||||||
|
uintptr(p.Y),
|
||||||
|
0,
|
||||||
|
uintptr(t.window),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if boolRet == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) delFromVisibleItems(parent, val uint32) {
|
||||||
|
t.muVisibleItems.Lock()
|
||||||
|
defer t.muVisibleItems.Unlock()
|
||||||
|
visibleItems := t.visibleItems[parent]
|
||||||
|
for i, itemval := range visibleItems {
|
||||||
|
if val == itemval {
|
||||||
|
t.visibleItems[parent] = append(visibleItems[:i], visibleItems[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) addToVisibleItems(parent, val uint32) {
|
||||||
|
t.muVisibleItems.Lock()
|
||||||
|
defer t.muVisibleItems.Unlock()
|
||||||
|
if visibleItems, exists := t.visibleItems[parent]; !exists {
|
||||||
|
t.visibleItems[parent] = []uint32{val}
|
||||||
|
} else {
|
||||||
|
newvisible := append(visibleItems, val)
|
||||||
|
sort.Slice(newvisible, func(i, j int) bool { return newvisible[i] < newvisible[j] })
|
||||||
|
t.visibleItems[parent] = newvisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) getVisibleItemIndex(parent, val uint32) int {
|
||||||
|
t.muVisibleItems.RLock()
|
||||||
|
defer t.muVisibleItems.RUnlock()
|
||||||
|
for i, itemval := range t.visibleItems[parent] {
|
||||||
|
if val == itemval {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func iconBytesToFilePath(iconBytes []byte) (string, error) {
|
||||||
|
bh := md5.Sum(iconBytes)
|
||||||
|
dataHash := hex.EncodeToString(bh[:])
|
||||||
|
iconFilePath := filepath.Join(os.TempDir(), "ollama_temp_icon_"+dataHash)
|
||||||
|
|
||||||
|
if _, err := os.Stat(iconFilePath); os.IsNotExist(err) {
|
||||||
|
if err := os.WriteFile(iconFilePath, iconBytes, 0644); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return iconFilePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads an image from file and shows it in tray.
|
||||||
|
// Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx
|
||||||
|
func (t *winTray) setIcon(src string) error {
|
||||||
|
|
||||||
|
h, err := t.loadIconFrom(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.muNID.Lock()
|
||||||
|
defer t.muNID.Unlock()
|
||||||
|
t.nid.Icon = h
|
||||||
|
t.nid.Flags |= NIF_ICON
|
||||||
|
t.nid.Size = uint32(unsafe.Sizeof(*t.nid))
|
||||||
|
|
||||||
|
return t.nid.modify()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads an image from file to be shown in tray or menu item.
|
||||||
|
// LoadImage: https://msdn.microsoft.com/en-us/library/windows/desktop/ms648045(v=vs.85).aspx
|
||||||
|
func (t *winTray) loadIconFrom(src string) (windows.Handle, error) {
|
||||||
|
|
||||||
|
// Save and reuse handles of loaded images
|
||||||
|
t.muLoadedImages.RLock()
|
||||||
|
h, ok := t.loadedImages[src]
|
||||||
|
t.muLoadedImages.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
srcPtr, err := windows.UTF16PtrFromString(src)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
res, _, err := pLoadImage.Call(
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(srcPtr)),
|
||||||
|
IMAGE_ICON,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
LR_LOADFROMFILE|LR_DEFAULTSIZE,
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
h = windows.Handle(res)
|
||||||
|
t.muLoadedImages.Lock()
|
||||||
|
t.loadedImages[src] = h
|
||||||
|
t.muLoadedImages.Unlock()
|
||||||
|
}
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *winTray) DisplayFirstUseNotification() error {
|
||||||
|
t.muNID.Lock()
|
||||||
|
defer t.muNID.Unlock()
|
||||||
|
copy(t.nid.InfoTitle[:], windows.StringToUTF16(firstTimeTitle))
|
||||||
|
copy(t.nid.Info[:], windows.StringToUTF16(firstTimeMessage))
|
||||||
|
t.nid.Flags |= NIF_INFO
|
||||||
|
t.nid.Size = uint32(unsafe.Sizeof(*wt.nid))
|
||||||
|
|
||||||
|
return t.nid.modify()
|
||||||
|
}
|
89
app/tray/wintray/w32api.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package wintray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
k32 = windows.NewLazySystemDLL("Kernel32.dll")
|
||||||
|
u32 = windows.NewLazySystemDLL("User32.dll")
|
||||||
|
s32 = windows.NewLazySystemDLL("Shell32.dll")
|
||||||
|
|
||||||
|
pCreatePopupMenu = u32.NewProc("CreatePopupMenu")
|
||||||
|
pCreateWindowEx = u32.NewProc("CreateWindowExW")
|
||||||
|
pDefWindowProc = u32.NewProc("DefWindowProcW")
|
||||||
|
pDestroyWindow = u32.NewProc("DestroyWindow")
|
||||||
|
pDispatchMessage = u32.NewProc("DispatchMessageW")
|
||||||
|
pGetCursorPos = u32.NewProc("GetCursorPos")
|
||||||
|
pGetMessage = u32.NewProc("GetMessageW")
|
||||||
|
pGetModuleHandle = k32.NewProc("GetModuleHandleW")
|
||||||
|
pInsertMenuItem = u32.NewProc("InsertMenuItemW")
|
||||||
|
pLoadCursor = u32.NewProc("LoadCursorW")
|
||||||
|
pLoadIcon = u32.NewProc("LoadIconW")
|
||||||
|
pLoadImage = u32.NewProc("LoadImageW")
|
||||||
|
pPostMessage = u32.NewProc("PostMessageW")
|
||||||
|
pPostQuitMessage = u32.NewProc("PostQuitMessage")
|
||||||
|
pRegisterClass = u32.NewProc("RegisterClassExW")
|
||||||
|
pRegisterWindowMessage = u32.NewProc("RegisterWindowMessageW")
|
||||||
|
pSetForegroundWindow = u32.NewProc("SetForegroundWindow")
|
||||||
|
pSetMenuInfo = u32.NewProc("SetMenuInfo")
|
||||||
|
pSetMenuItemInfo = u32.NewProc("SetMenuItemInfoW")
|
||||||
|
pShellNotifyIcon = s32.NewProc("Shell_NotifyIconW")
|
||||||
|
pShowWindow = u32.NewProc("ShowWindow")
|
||||||
|
pTrackPopupMenu = u32.NewProc("TrackPopupMenu")
|
||||||
|
pTranslateMessage = u32.NewProc("TranslateMessage")
|
||||||
|
pUnregisterClass = u32.NewProc("UnregisterClassW")
|
||||||
|
pUpdateWindow = u32.NewProc("UpdateWindow")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CS_HREDRAW = 0x0002
|
||||||
|
CS_VREDRAW = 0x0001
|
||||||
|
CW_USEDEFAULT = 0x80000000
|
||||||
|
IDC_ARROW = 32512 // Standard arrow
|
||||||
|
IDI_APPLICATION = 32512
|
||||||
|
IMAGE_ICON = 1 // Loads an icon
|
||||||
|
LR_DEFAULTSIZE = 0x00000040 // Loads default-size icon for windows(SM_CXICON x SM_CYICON) if cx, cy are set to zero
|
||||||
|
LR_LOADFROMFILE = 0x00000010 // Loads the stand-alone image from the file
|
||||||
|
MF_BYCOMMAND = 0x00000000
|
||||||
|
MFS_DISABLED = 0x00000003
|
||||||
|
MFT_SEPARATOR = 0x00000800
|
||||||
|
MFT_STRING = 0x00000000
|
||||||
|
MIIM_BITMAP = 0x00000080
|
||||||
|
MIIM_FTYPE = 0x00000100
|
||||||
|
MIIM_ID = 0x00000002
|
||||||
|
MIIM_STATE = 0x00000001
|
||||||
|
MIIM_STRING = 0x00000040
|
||||||
|
MIIM_SUBMENU = 0x00000004
|
||||||
|
MIM_APPLYTOSUBMENUS = 0x80000000
|
||||||
|
NIF_ICON = 0x00000002
|
||||||
|
NIF_INFO = 0x00000010
|
||||||
|
NIF_MESSAGE = 0x00000001
|
||||||
|
SW_HIDE = 0
|
||||||
|
TPM_BOTTOMALIGN = 0x0020
|
||||||
|
TPM_LEFTALIGN = 0x0000
|
||||||
|
WM_CLOSE = 0x0010
|
||||||
|
WM_USER = 0x0400
|
||||||
|
WS_CAPTION = 0x00C00000
|
||||||
|
WS_MAXIMIZEBOX = 0x00010000
|
||||||
|
WS_MINIMIZEBOX = 0x00020000
|
||||||
|
WS_OVERLAPPED = 0x00000000
|
||||||
|
WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
|
||||||
|
WS_SYSMENU = 0x00080000
|
||||||
|
WS_THICKFRAME = 0x00040000
|
||||||
|
)
|
||||||
|
|
||||||
|
// Not sure if this is actually needed on windows
|
||||||
|
func init() {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
}
|
||||||
|
|
||||||
|
// The POINT structure defines the x- and y- coordinates of a point.
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd162805(v=vs.85).aspx
|
||||||
|
type point struct {
|
||||||
|
X, Y int32
|
||||||
|
}
|
45
app/tray/wintray/winclass.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package wintray
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Contains window class information.
|
||||||
|
// It is used with the RegisterClassEx and GetClassInfoEx functions.
|
||||||
|
// https://msdn.microsoft.com/en-us/library/ms633577.aspx
|
||||||
|
type wndClassEx struct {
|
||||||
|
Size, Style uint32
|
||||||
|
WndProc uintptr
|
||||||
|
ClsExtra, WndExtra int32
|
||||||
|
Instance, Icon, Cursor, Background windows.Handle
|
||||||
|
MenuName, ClassName *uint16
|
||||||
|
IconSm windows.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registers a window class for subsequent use in calls to the CreateWindow or CreateWindowEx function.
|
||||||
|
// https://msdn.microsoft.com/en-us/library/ms633587.aspx
|
||||||
|
func (w *wndClassEx) register() error {
|
||||||
|
w.Size = uint32(unsafe.Sizeof(*w))
|
||||||
|
res, _, err := pRegisterClass.Call(uintptr(unsafe.Pointer(w)))
|
||||||
|
if res == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregisters a window class, freeing the memory required for the class.
|
||||||
|
// https://msdn.microsoft.com/en-us/library/ms644899.aspx
|
||||||
|
func (w *wndClassEx) unregister() error {
|
||||||
|
res, _, err := pUnregisterClass.Call(
|
||||||
|
uintptr(unsafe.Pointer(w.ClassName)),
|
||||||
|
uintptr(w.Instance),
|
||||||
|
)
|
||||||
|
if res == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES6",
|
|
||||||
"allowJs": true,
|
|
||||||
"module": "commonjs",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"outDir": "dist",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"paths": {
|
|
||||||
"*": ["node_modules/*"]
|
|
||||||
},
|
|
||||||
"jsx": "react-jsx"
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"]
|
|
||||||
}
|
|
232
app/updater/updater.go
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/auth"
|
||||||
|
"github.com/ollama/ollama/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
UpdateStageDir string
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
UpdateCheckURLBase = "https://ollama.com/api/update"
|
||||||
|
UpdateDownloaded = false
|
||||||
|
UpdateCheckInterval = 60 * 60 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO - maybe move up to the API package?
|
||||||
|
type UpdateResponse struct {
|
||||||
|
UpdateURL string `json:"url"`
|
||||||
|
UpdateVersion string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsNewReleaseAvailable(ctx context.Context) (bool, UpdateResponse) {
|
||||||
|
var updateResp UpdateResponse
|
||||||
|
|
||||||
|
requestURL, err := url.Parse(UpdateCheckURLBase)
|
||||||
|
if err != nil {
|
||||||
|
return false, updateResp
|
||||||
|
}
|
||||||
|
|
||||||
|
query := requestURL.Query()
|
||||||
|
query.Add("os", runtime.GOOS)
|
||||||
|
query.Add("arch", runtime.GOARCH)
|
||||||
|
query.Add("version", version.Version)
|
||||||
|
query.Add("ts", fmt.Sprintf("%d", time.Now().Unix()))
|
||||||
|
|
||||||
|
nonce, err := auth.NewNonce(rand.Reader, 16)
|
||||||
|
if err != nil {
|
||||||
|
return false, updateResp
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Add("nonce", nonce)
|
||||||
|
requestURL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
data := []byte(fmt.Sprintf("%s,%s", http.MethodGet, requestURL.RequestURI()))
|
||||||
|
signature, err := auth.Sign(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return false, updateResp
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("failed to check for update: %s", err))
|
||||||
|
return false, updateResp
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", signature)
|
||||||
|
req.Header.Set("User-Agent", fmt.Sprintf("ollama/%s (%s %s) Go/%s", version.Version, runtime.GOARCH, runtime.GOOS, runtime.Version()))
|
||||||
|
|
||||||
|
slog.Debug("checking for available update", "requestURL", requestURL)
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("failed to check for update: %s", err))
|
||||||
|
return false, updateResp
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == 204 {
|
||||||
|
slog.Debug("check update response 204 (current version is up to date)")
|
||||||
|
return false, updateResp
|
||||||
|
}
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("failed to read body response: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
slog.Info(fmt.Sprintf("check update error %d - %.96s", resp.StatusCode, string(body)))
|
||||||
|
return false, updateResp
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(body, &updateResp)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("malformed response checking for update: %s", err))
|
||||||
|
return false, updateResp
|
||||||
|
}
|
||||||
|
// Extract the version string from the URL in the github release artifact path
|
||||||
|
updateResp.UpdateVersion = path.Base(path.Dir(updateResp.UpdateURL))
|
||||||
|
|
||||||
|
slog.Info("New update available at " + updateResp.UpdateURL)
|
||||||
|
return true, updateResp
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadNewRelease(ctx context.Context, updateResp UpdateResponse) error {
|
||||||
|
// Do a head first to check etag info
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodHead, updateResp.UpdateURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error checking update: %w", err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("unexpected status attempting to download update %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
etag := strings.Trim(resp.Header.Get("etag"), "\"")
|
||||||
|
if etag == "" {
|
||||||
|
slog.Debug("no etag detected, falling back to filename based dedup")
|
||||||
|
etag = "_"
|
||||||
|
}
|
||||||
|
filename := "OllamaSetup.exe"
|
||||||
|
_, params, err := mime.ParseMediaType(resp.Header.Get("content-disposition"))
|
||||||
|
if err == nil {
|
||||||
|
filename = params["filename"]
|
||||||
|
}
|
||||||
|
|
||||||
|
stageFilename := filepath.Join(UpdateStageDir, etag, filename)
|
||||||
|
|
||||||
|
// Check to see if we already have it downloaded
|
||||||
|
_, err = os.Stat(stageFilename)
|
||||||
|
if err == nil {
|
||||||
|
slog.Info("update already downloaded")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupOldDownloads()
|
||||||
|
|
||||||
|
req.Method = http.MethodGet
|
||||||
|
resp, err = http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error checking update: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
etag = strings.Trim(resp.Header.Get("etag"), "\"")
|
||||||
|
if etag == "" {
|
||||||
|
slog.Debug("no etag detected, falling back to filename based dedup") // TODO probably can get rid of this redundant log
|
||||||
|
etag = "_"
|
||||||
|
}
|
||||||
|
|
||||||
|
stageFilename = filepath.Join(UpdateStageDir, etag, filename)
|
||||||
|
|
||||||
|
_, err = os.Stat(filepath.Dir(stageFilename))
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(stageFilename), 0o755); err != nil {
|
||||||
|
return fmt.Errorf("create ollama dir %s: %v", filepath.Dir(stageFilename), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read body response: %w", err)
|
||||||
|
}
|
||||||
|
fp, err := os.OpenFile(stageFilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write payload %s: %w", stageFilename, err)
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
if n, err := fp.Write(payload); err != nil || n != len(payload) {
|
||||||
|
return fmt.Errorf("write payload %s: %d vs %d -- %w", stageFilename, n, len(payload), err)
|
||||||
|
}
|
||||||
|
slog.Info("new update downloaded " + stageFilename)
|
||||||
|
|
||||||
|
UpdateDownloaded = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupOldDownloads() {
|
||||||
|
files, err := os.ReadDir(UpdateStageDir)
|
||||||
|
if err != nil && errors.Is(err, os.ErrNotExist) {
|
||||||
|
// Expected behavior on first run
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("failed to list stage dir: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
fullname := filepath.Join(UpdateStageDir, file.Name())
|
||||||
|
slog.Debug("cleaning up old download: " + fullname)
|
||||||
|
err = os.RemoveAll(fullname)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("failed to cleanup stale update download %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartBackgroundUpdaterChecker(ctx context.Context, cb func(string) error) {
|
||||||
|
go func() {
|
||||||
|
// Don't blast an update message immediately after startup
|
||||||
|
// time.Sleep(30 * time.Second)
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
for {
|
||||||
|
available, resp := IsNewReleaseAvailable(ctx)
|
||||||
|
if available {
|
||||||
|
err := DownloadNewRelease(ctx, resp)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to download new release: %s", err))
|
||||||
|
}
|
||||||
|
err = cb(resp.UpdateVersion)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn(fmt.Sprintf("failed to register update available with tray: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
slog.Debug("stopping background update checker")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
time.Sleep(UpdateCheckInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
10
app/updater/updater_darwin.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DoUpgrade(cancel context.CancelFunc, done chan int) error {
|
||||||
|
return fmt.Errorf("DoUpgrade not yet implemented")
|
||||||
|
}
|
86
app/updater/updater_windows.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package updater
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
UpdateStageDir = filepath.Join(os.Getenv("LOCALAPPDATA"), "Ollama", "updates")
|
||||||
|
}
|
||||||
|
|
||||||
|
func DoUpgrade(cancel context.CancelFunc, done chan int) error {
|
||||||
|
logFile := filepath.Join(os.Getenv("LOCALAPPDATA"), "Ollama", "upgrade.log")
|
||||||
|
|
||||||
|
files, err := filepath.Glob(filepath.Join(UpdateStageDir, "*", "*.exe")) // TODO generalize for multiplatform
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to lookup downloads: %s", err)
|
||||||
|
}
|
||||||
|
if len(files) == 0 {
|
||||||
|
return fmt.Errorf("no update downloads found")
|
||||||
|
} else if len(files) > 1 {
|
||||||
|
// Shouldn't happen
|
||||||
|
slog.Warn(fmt.Sprintf("multiple downloads found, using first one %v", files))
|
||||||
|
}
|
||||||
|
installerExe := files[0]
|
||||||
|
|
||||||
|
slog.Info("starting upgrade with " + installerExe)
|
||||||
|
slog.Info("upgrade log file " + logFile)
|
||||||
|
|
||||||
|
// When running in debug mode, we'll be "verbose" and let the installer pop up and prompt
|
||||||
|
installArgs := []string{
|
||||||
|
"/CLOSEAPPLICATIONS", // Quit the tray app if it's still running
|
||||||
|
"/LOG=" + filepath.Base(logFile), // Only relative seems reliable, so set pwd
|
||||||
|
"/FORCECLOSEAPPLICATIONS", // Force close the tray app - might be needed
|
||||||
|
}
|
||||||
|
// When we're not in debug mode, make the upgrade as quiet as possible (no GUI, no prompts)
|
||||||
|
// TODO - temporarily disable since we're pinning in debug mode for the preview
|
||||||
|
// if debug := os.Getenv("OLLAMA_DEBUG"); debug == "" {
|
||||||
|
installArgs = append(installArgs,
|
||||||
|
"/SP", // Skip the "This will install... Do you wish to continue" prompt
|
||||||
|
"/SUPPRESSMSGBOXES",
|
||||||
|
"/SILENT",
|
||||||
|
"/VERYSILENT",
|
||||||
|
)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Safeguard in case we have requests in flight that need to drain...
|
||||||
|
slog.Info("Waiting for server to shutdown")
|
||||||
|
cancel()
|
||||||
|
if done != nil {
|
||||||
|
<-done
|
||||||
|
} else {
|
||||||
|
// Shouldn't happen
|
||||||
|
slog.Warn("done chan was nil, not actually waiting")
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug(fmt.Sprintf("starting installer: %s %v", installerExe, installArgs))
|
||||||
|
os.Chdir(filepath.Dir(logFile)) //nolint:errcheck
|
||||||
|
cmd := exec.Command(installerExe, installArgs...)
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("unable to start ollama app %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Process != nil {
|
||||||
|
err = cmd.Process.Release()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error(fmt.Sprintf("failed to release server process: %s", err))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO - some details about why it didn't start, or is this a pedantic error case?
|
||||||
|
return fmt.Errorf("installer process did not start")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO should we linger for a moment and check to make sure it's actually running by checking the pid?
|
||||||
|
|
||||||
|
slog.Info("Installer started in background, exiting")
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
// Not reached
|
||||||
|
return nil
|
||||||
|
}
|
@@ -1,20 +0,0 @@
|
|||||||
import type { Configuration } from 'webpack'
|
|
||||||
|
|
||||||
import { rules } from './webpack.rules'
|
|
||||||
import { plugins } from './webpack.plugins'
|
|
||||||
|
|
||||||
export const mainConfig: Configuration = {
|
|
||||||
/**
|
|
||||||
* This is the main entry point for your application, it's the first file
|
|
||||||
* that runs in the main process.
|
|
||||||
*/
|
|
||||||
entry: './src/index.ts',
|
|
||||||
// Put your normal webpack config below here
|
|
||||||
module: {
|
|
||||||
rules,
|
|
||||||
},
|
|
||||||
plugins,
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'],
|
|
||||||
},
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
import type IForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'
|
|
||||||
import { DefinePlugin } from 'webpack'
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
const ForkTsCheckerWebpackPlugin: typeof IForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
|
|
||||||
|
|
||||||
export const plugins = [
|
|
||||||
new ForkTsCheckerWebpackPlugin({
|
|
||||||
logger: 'webpack-infrastructure',
|
|
||||||
}),
|
|
||||||
new DefinePlugin({
|
|
||||||
'process.env.TELEMETRY_WRITE_KEY': JSON.stringify(process.env.TELEMETRY_WRITE_KEY),
|
|
||||||
}),
|
|
||||||
]
|
|
@@ -1,19 +0,0 @@
|
|||||||
import type { Configuration } from 'webpack'
|
|
||||||
|
|
||||||
import { rules } from './webpack.rules'
|
|
||||||
import { plugins } from './webpack.plugins'
|
|
||||||
|
|
||||||
rules.push({
|
|
||||||
test: /\.css$/,
|
|
||||||
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'postcss-loader' }],
|
|
||||||
})
|
|
||||||
|
|
||||||
export const rendererConfig: Configuration = {
|
|
||||||
module: {
|
|
||||||
rules,
|
|
||||||
},
|
|
||||||
plugins,
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'],
|
|
||||||
},
|
|
||||||
}
|
|
@@ -1,35 +0,0 @@
|
|||||||
import type { ModuleOptions } from 'webpack'
|
|
||||||
|
|
||||||
export const rules: Required<ModuleOptions>['rules'] = [
|
|
||||||
// Add support for native node modules
|
|
||||||
{
|
|
||||||
// We're specifying native_modules in the test because the asset relocator loader generates a
|
|
||||||
// "fake" .node file which is really a cjs file.
|
|
||||||
test: /native_modules[/\\].+\.node$/,
|
|
||||||
use: 'node-loader',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /[/\\]node_modules[/\\].+\.(m?js|node)$/,
|
|
||||||
parser: { amd: false },
|
|
||||||
use: {
|
|
||||||
loader: '@vercel/webpack-asset-relocator-loader',
|
|
||||||
options: {
|
|
||||||
outputAssetBase: 'native_modules',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
exclude: /(node_modules|\.webpack)/,
|
|
||||||
use: {
|
|
||||||
loader: 'ts-loader',
|
|
||||||
options: {
|
|
||||||
transpileOnly: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.svg$/,
|
|
||||||
use: ['@svgr/webpack'],
|
|
||||||
},
|
|
||||||
]
|
|
156
app/windows/ollama.iss
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
; Inno Setup Installer for Ollama
|
||||||
|
;
|
||||||
|
; To build the installer use the build script invoked from the top of the source tree
|
||||||
|
;
|
||||||
|
; powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps
|
||||||
|
|
||||||
|
|
||||||
|
#define MyAppName "Ollama"
|
||||||
|
#if GetEnv("PKG_VERSION") != ""
|
||||||
|
#define MyAppVersion GetEnv("PKG_VERSION")
|
||||||
|
#else
|
||||||
|
#define MyAppVersion "0.0.0"
|
||||||
|
#endif
|
||||||
|
#define MyAppPublisher "Ollama"
|
||||||
|
#define MyAppURL "https://ollama.com/"
|
||||||
|
#define MyAppExeName "ollama app.exe"
|
||||||
|
#define MyIcon ".\assets\app.ico"
|
||||||
|
|
||||||
|
[Setup]
|
||||||
|
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
|
||||||
|
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
||||||
|
AppId={{44E83376-CE68-45EB-8FC1-393500EB558C}
|
||||||
|
AppName={#MyAppName}
|
||||||
|
AppVersion={#MyAppVersion}
|
||||||
|
VersionInfoVersion={#MyAppVersion}
|
||||||
|
;AppVerName={#MyAppName} {#MyAppVersion}
|
||||||
|
AppPublisher={#MyAppPublisher}
|
||||||
|
AppPublisherURL={#MyAppURL}
|
||||||
|
AppSupportURL={#MyAppURL}
|
||||||
|
AppUpdatesURL={#MyAppURL}
|
||||||
|
ArchitecturesAllowed=x64 arm64
|
||||||
|
ArchitecturesInstallIn64BitMode=x64 arm64
|
||||||
|
DefaultDirName={localappdata}\Programs\{#MyAppName}
|
||||||
|
DefaultGroupName={#MyAppName}
|
||||||
|
DisableProgramGroupPage=yes
|
||||||
|
PrivilegesRequired=lowest
|
||||||
|
OutputBaseFilename="OllamaSetup"
|
||||||
|
SetupIconFile={#MyIcon}
|
||||||
|
UninstallDisplayIcon={uninstallexe}
|
||||||
|
Compression=lzma2
|
||||||
|
SolidCompression=no
|
||||||
|
WizardStyle=modern
|
||||||
|
ChangesEnvironment=yes
|
||||||
|
OutputDir=..\dist\
|
||||||
|
|
||||||
|
; Disable logging once everything's battle tested
|
||||||
|
; Filename will be %TEMP%\Setup Log*.txt
|
||||||
|
SetupLogging=yes
|
||||||
|
CloseApplications=yes
|
||||||
|
RestartApplications=no
|
||||||
|
|
||||||
|
; https://jrsoftware.org/ishelp/index.php?topic=setup_wizardimagefile
|
||||||
|
WizardSmallImageFile=.\assets\setup.bmp
|
||||||
|
|
||||||
|
; TODO verifty actual min windows version...
|
||||||
|
; OG Win 10
|
||||||
|
MinVersion=10.0.10240
|
||||||
|
|
||||||
|
; First release that supports WinRT UI Composition for win32 apps
|
||||||
|
; MinVersion=10.0.17134
|
||||||
|
; First release with XAML Islands - possible UI path forward
|
||||||
|
; MinVersion=10.0.18362
|
||||||
|
|
||||||
|
; quiet...
|
||||||
|
DisableDirPage=yes
|
||||||
|
DisableFinishedPage=yes
|
||||||
|
DisableReadyMemo=yes
|
||||||
|
DisableReadyPage=yes
|
||||||
|
DisableStartupPrompt=yes
|
||||||
|
DisableWelcomePage=yes
|
||||||
|
|
||||||
|
; TODO - percentage can't be set less than 100, so how to make it shorter?
|
||||||
|
; WizardSizePercent=100,80
|
||||||
|
|
||||||
|
#if GetEnv("KEY_CONTAINER")
|
||||||
|
SignTool=MySignTool
|
||||||
|
SignedUninstaller=yes
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SetupMutex=OllamaSetupMutex
|
||||||
|
|
||||||
|
[Languages]
|
||||||
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
|
||||||
|
[LangOptions]
|
||||||
|
DialogFontSize=12
|
||||||
|
|
||||||
|
[Files]
|
||||||
|
Source: ".\app.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}" ; Flags: ignoreversion 64bit
|
||||||
|
Source: "..\ollama.exe"; DestDir: "{app}"; Flags: ignoreversion 64bit
|
||||||
|
Source: "..\dist\windows-amd64\*.dll"; DestDir: "{app}"; Flags: ignoreversion 64bit
|
||||||
|
Source: "..\dist\windows-amd64\ollama_runners\*"; DestDir: "{app}\ollama_runners"; Flags: ignoreversion 64bit recursesubdirs
|
||||||
|
Source: "..\dist\ollama_welcome.ps1"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
Source: ".\assets\app.ico"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
|
#if DirExists("..\dist\windows-amd64\rocm")
|
||||||
|
Source: "..\dist\windows-amd64\rocm\*"; DestDir: "{app}\rocm\"; Flags: ignoreversion recursesubdirs
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
[Icons]
|
||||||
|
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\app.ico"
|
||||||
|
Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\app.ico"
|
||||||
|
Name: "{userprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\app.ico"
|
||||||
|
|
||||||
|
[Run]
|
||||||
|
Filename: "{cmd}"; Parameters: "/C set PATH={app};%PATH% & ""{app}\{#MyAppExeName}"""; Flags: postinstall nowait runhidden
|
||||||
|
|
||||||
|
[UninstallRun]
|
||||||
|
; Filename: "{cmd}"; Parameters: "/C ""taskkill /im ''{#MyAppExeName}'' /f /t"; Flags: runhidden
|
||||||
|
; Filename: "{cmd}"; Parameters: "/C ""taskkill /im ollama.exe /f /t"; Flags: runhidden
|
||||||
|
Filename: "taskkill"; Parameters: "/im ""{#MyAppExeName}"" /f /t"; Flags: runhidden
|
||||||
|
Filename: "taskkill"; Parameters: "/im ""ollama.exe"" /f /t"; Flags: runhidden
|
||||||
|
; HACK! need to give the server and app enough time to exit
|
||||||
|
; TODO - convert this to a Pascal code script so it waits until they're no longer running, then completes
|
||||||
|
Filename: "{cmd}"; Parameters: "/c timeout 5"; Flags: runhidden
|
||||||
|
|
||||||
|
[UninstallDelete]
|
||||||
|
Type: filesandordirs; Name: "{%TEMP}\ollama*"
|
||||||
|
Type: filesandordirs; Name: "{%LOCALAPPDATA}\Ollama"
|
||||||
|
Type: filesandordirs; Name: "{%LOCALAPPDATA}\Programs\Ollama"
|
||||||
|
Type: filesandordirs; Name: "{%USERPROFILE}\.ollama\models"
|
||||||
|
Type: filesandordirs; Name: "{%USERPROFILE}\.ollama\history"
|
||||||
|
; NOTE: if the user has a custom OLLAMA_MODELS it will be preserved
|
||||||
|
|
||||||
|
[Messages]
|
||||||
|
WizardReady=Ollama Windows Preview
|
||||||
|
ReadyLabel1=%nLet's get you up and running with your own large language models.
|
||||||
|
SetupAppRunningError=Another Ollama installer is running.%n%nPlease cancel or finish the other installer, then click OK to continue with this install, or Cancel to exit.
|
||||||
|
|
||||||
|
|
||||||
|
;FinishedHeadingLabel=Run your first model
|
||||||
|
;FinishedLabel=%nRun this command in a PowerShell or cmd terminal.%n%n%n ollama run llama3
|
||||||
|
;ClickFinish=%n
|
||||||
|
|
||||||
|
[Registry]
|
||||||
|
Root: HKCU; Subkey: "Environment"; \
|
||||||
|
ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; \
|
||||||
|
Check: NeedsAddPath('{app}')
|
||||||
|
|
||||||
|
[Code]
|
||||||
|
|
||||||
|
function NeedsAddPath(Param: string): boolean;
|
||||||
|
var
|
||||||
|
OrigPath: string;
|
||||||
|
begin
|
||||||
|
if not RegQueryStringValue(HKEY_CURRENT_USER,
|
||||||
|
'Environment',
|
||||||
|
'Path', OrigPath)
|
||||||
|
then begin
|
||||||
|
Result := True;
|
||||||
|
exit;
|
||||||
|
end;
|
||||||
|
{ look for the path with leading and trailing semicolon }
|
||||||
|
{ Pos() returns 0 if not found }
|
||||||
|
Result := Pos(';' + ExpandConstant(Param) + ';', ';' + OrigPath + ';') = 0;
|
||||||
|
end;
|
29
app/windows/ollama.rc
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#include <winver.h>
|
||||||
|
|
||||||
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
FILEFLAGSMASK 0x3fL
|
||||||
|
#ifdef _DEBUG
|
||||||
|
FILEFLAGS 0x1L
|
||||||
|
#else
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
#endif
|
||||||
|
FILEOS 0x40004L
|
||||||
|
FILETYPE 0x1L
|
||||||
|
FILESUBTYPE 0x0L
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904b0"
|
||||||
|
BEGIN
|
||||||
|
VALUE "FileDescription", "Ollama"
|
||||||
|
VALUE "InternalName", "Ollama"
|
||||||
|
VALUE "OriginalFilename", "ollama app.exe"
|
||||||
|
VALUE "ProductName", "Ollama"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x409, 1200
|
||||||
|
END
|
||||||
|
END
|
8
app/windows/ollama_welcome.ps1
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# TODO - consider ANSI colors and maybe ASCII art...
|
||||||
|
write-host ""
|
||||||
|
write-host "Welcome to Ollama!"
|
||||||
|
write-host ""
|
||||||
|
write-host "Run your first model:"
|
||||||
|
write-host ""
|
||||||
|
write-host "`tollama run llama2"
|
||||||
|
write-host ""
|
61
auth/auth.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultPrivateKey = "id_ed25519"
|
||||||
|
|
||||||
|
func NewNonce(r io.Reader, length int) (string, error) {
|
||||||
|
nonce := make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(r, nonce); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.RawURLEncoding.EncodeToString(nonce), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Sign(ctx context.Context, bts []byte) (string, error) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
keyPath := filepath.Join(home, ".ollama", defaultPrivateKey)
|
||||||
|
|
||||||
|
privateKeyFile, err := os.ReadFile(keyPath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Info(fmt.Sprintf("Failed to load private key: %v", err))
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey, err := ssh.ParsePrivateKey(privateKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the pubkey, but remove the type
|
||||||
|
publicKey := ssh.MarshalAuthorizedKey(privateKey.PublicKey())
|
||||||
|
parts := bytes.Split(publicKey, []byte(" "))
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return "", fmt.Errorf("malformed public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
signedData, err := privateKey.Sign(rand.Reader, bts)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// signature is <pubkey>:<signature>
|
||||||
|
return fmt.Sprintf("%s:%s", bytes.TrimSpace(parts[1]), base64.StdEncoding.EncodeToString(signedData.Blob)), nil
|
||||||
|
}
|
406
cmd/cmd.go
@@ -1,6 +1,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
@@ -14,25 +15,28 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/containerd/console"
|
||||||
|
|
||||||
"github.com/olekukonko/tablewriter"
|
"github.com/olekukonko/tablewriter"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
|
|
||||||
"github.com/jmorganca/ollama/api"
|
"github.com/ollama/ollama/api"
|
||||||
"github.com/jmorganca/ollama/format"
|
"github.com/ollama/ollama/format"
|
||||||
"github.com/jmorganca/ollama/parser"
|
"github.com/ollama/ollama/parser"
|
||||||
"github.com/jmorganca/ollama/progress"
|
"github.com/ollama/ollama/progress"
|
||||||
"github.com/jmorganca/ollama/server"
|
"github.com/ollama/ollama/server"
|
||||||
"github.com/jmorganca/ollama/version"
|
"github.com/ollama/ollama/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateHandler(cmd *cobra.Command, args []string) error {
|
func CreateHandler(cmd *cobra.Command, args []string) error {
|
||||||
@@ -50,8 +54,6 @@ func CreateHandler(cmd *cobra.Command, args []string) error {
|
|||||||
p := progress.NewProgress(os.Stderr)
|
p := progress.NewProgress(os.Stderr)
|
||||||
defer p.Stop()
|
defer p.Stop()
|
||||||
|
|
||||||
bars := make(map[string]*progress.Bar)
|
|
||||||
|
|
||||||
modelfile, err := os.ReadFile(filename)
|
modelfile, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -85,29 +87,41 @@ func CreateHandler(cmd *cobra.Command, args []string) error {
|
|||||||
path = filepath.Join(filepath.Dir(filename), path)
|
path = filepath.Join(filepath.Dir(filename), path)
|
||||||
}
|
}
|
||||||
|
|
||||||
bin, err := os.Open(path)
|
fi, err := os.Stat(path)
|
||||||
if errors.Is(err, os.ErrNotExist) && c.Name == "model" {
|
if errors.Is(err, os.ErrNotExist) && c.Name == "model" {
|
||||||
continue
|
continue
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer bin.Close()
|
|
||||||
|
|
||||||
hash := sha256.New()
|
if fi.IsDir() {
|
||||||
if _, err := io.Copy(hash, bin); err != nil {
|
// this is likely a safetensors or pytorch directory
|
||||||
return err
|
// TODO make this work w/ adapters
|
||||||
|
tempfile, err := tempZipFiles(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempfile)
|
||||||
|
|
||||||
|
path = tempfile
|
||||||
}
|
}
|
||||||
bin.Seek(0, io.SeekStart)
|
|
||||||
|
|
||||||
digest := fmt.Sprintf("sha256:%x", hash.Sum(nil))
|
digest, err := createBlob(cmd, client, path)
|
||||||
if err = client.CreateBlob(cmd.Context(), digest, bin); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
modelfile = bytes.ReplaceAll(modelfile, []byte(c.Args), []byte("@"+digest))
|
name := c.Name
|
||||||
|
if c.Name == "model" {
|
||||||
|
name = "from"
|
||||||
|
}
|
||||||
|
|
||||||
|
re := regexp.MustCompile(fmt.Sprintf(`(?im)^(%s)\s+%s\s*$`, name, c.Args))
|
||||||
|
modelfile = re.ReplaceAll(modelfile, []byte("$1 @"+digest))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bars := make(map[string]*progress.Bar)
|
||||||
fn := func(resp api.ProgressResponse) error {
|
fn := func(resp api.ProgressResponse) error {
|
||||||
if resp.Digest != "" {
|
if resp.Digest != "" {
|
||||||
spinner.Stop()
|
spinner.Stop()
|
||||||
@@ -131,7 +145,9 @@ func CreateHandler(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
request := api.CreateRequest{Name: args[0], Modelfile: string(modelfile)}
|
quantization, _ := cmd.Flags().GetString("quantization")
|
||||||
|
|
||||||
|
request := api.CreateRequest{Name: args[0], Modelfile: string(modelfile), Quantization: quantization}
|
||||||
if err := client.Create(cmd.Context(), &request, fn); err != nil {
|
if err := client.Create(cmd.Context(), &request, fn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -139,6 +155,137 @@ func CreateHandler(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tempZipFiles(path string) (string, error) {
|
||||||
|
tempfile, err := os.CreateTemp("", "ollama-tf")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer tempfile.Close()
|
||||||
|
|
||||||
|
zipfile := zip.NewWriter(tempfile)
|
||||||
|
defer zipfile.Close()
|
||||||
|
|
||||||
|
detectContentType := func(path string) (string, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
b.Grow(512)
|
||||||
|
|
||||||
|
if _, err := io.CopyN(&b, f, 512); err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType, _, _ := strings.Cut(http.DetectContentType(b.Bytes()), ";")
|
||||||
|
return contentType, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
glob := func(pattern, contentType string) ([]string, error) {
|
||||||
|
matches, err := filepath.Glob(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, safetensor := range matches {
|
||||||
|
if ct, err := detectContentType(safetensor); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if ct != contentType {
|
||||||
|
return nil, fmt.Errorf("invalid content type: expected %s for %s", ct, safetensor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var files []string
|
||||||
|
if st, _ := glob(filepath.Join(path, "model*.safetensors"), "application/octet-stream"); len(st) > 0 {
|
||||||
|
// safetensors files might be unresolved git lfs references; skip if they are
|
||||||
|
// covers model-x-of-y.safetensors, model.fp32-x-of-y.safetensors, model.safetensors
|
||||||
|
files = append(files, st...)
|
||||||
|
} else if pt, _ := glob(filepath.Join(path, "pytorch_model*.bin"), "application/zip"); len(pt) > 0 {
|
||||||
|
// pytorch files might also be unresolved git lfs references; skip if they are
|
||||||
|
// covers pytorch_model-x-of-y.bin, pytorch_model.fp32-x-of-y.bin, pytorch_model.bin
|
||||||
|
files = append(files, pt...)
|
||||||
|
} else if pt, _ := glob(filepath.Join(path, "consolidated*.pth"), "application/octet-stream"); len(pt) > 0 {
|
||||||
|
// pytorch files might also be unresolved git lfs references; skip if they are
|
||||||
|
// covers consolidated.x.pth, consolidated.pth
|
||||||
|
files = append(files, pt...)
|
||||||
|
} else {
|
||||||
|
return "", errors.New("no safetensors or torch files found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// add configuration files, json files are detected as text/plain
|
||||||
|
js, err := glob(filepath.Join(path, "*.json"), "text/plain")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
files = append(files, js...)
|
||||||
|
|
||||||
|
if tks, _ := glob(filepath.Join(path, "tokenizer.model"), "application/octet-stream"); len(tks) > 0 {
|
||||||
|
// add tokenizer.model if it exists, tokenizer.json is automatically picked up by the previous glob
|
||||||
|
// tokenizer.model might be a unresolved git lfs reference; error if it is
|
||||||
|
files = append(files, tks...)
|
||||||
|
} else if tks, _ := glob(filepath.Join(path, "**/tokenizer.model"), "text/plain"); len(tks) > 0 {
|
||||||
|
// some times tokenizer.model is in a subdirectory (e.g. meta-llama/Meta-Llama-3-8B)
|
||||||
|
files = append(files, tks...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
f, err := os.Open(file)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
fi, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
zfi, err := zip.FileInfoHeader(fi)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
zf, err := zipfile.CreateHeader(zfi)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(zf, f); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempfile.Name(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createBlob(cmd *cobra.Command, client *api.Client, path string) (string, error) {
|
||||||
|
bin, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer bin.Close()
|
||||||
|
|
||||||
|
hash := sha256.New()
|
||||||
|
if _, err := io.Copy(hash, bin); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := bin.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := fmt.Sprintf("sha256:%x", hash.Sum(nil))
|
||||||
|
if err = client.CreateBlob(cmd.Context(), digest, bin); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return digest, nil
|
||||||
|
}
|
||||||
|
|
||||||
func RunHandler(cmd *cobra.Command, args []string) error {
|
func RunHandler(cmd *cobra.Command, args []string) error {
|
||||||
client, err := api.ClientFromEnvironment()
|
client, err := api.ClientFromEnvironment()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -146,19 +293,68 @@ func RunHandler(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
name := args[0]
|
name := args[0]
|
||||||
|
|
||||||
// check if the model exists on the server
|
// check if the model exists on the server
|
||||||
_, err = client.Show(cmd.Context(), &api.ShowRequest{Name: name})
|
show, err := client.Show(cmd.Context(), &api.ShowRequest{Name: name})
|
||||||
var statusError api.StatusError
|
var statusError api.StatusError
|
||||||
switch {
|
switch {
|
||||||
case errors.As(err, &statusError) && statusError.StatusCode == http.StatusNotFound:
|
case errors.As(err, &statusError) && statusError.StatusCode == http.StatusNotFound:
|
||||||
if err := PullHandler(cmd, []string{name}); err != nil {
|
if err := PullHandler(cmd, []string{name}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
show, err = client.Show(cmd.Context(), &api.ShowRequest{Name: name})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return RunGenerate(cmd, args)
|
interactive := true
|
||||||
|
|
||||||
|
opts := runOptions{
|
||||||
|
Model: args[0],
|
||||||
|
WordWrap: os.Getenv("TERM") == "xterm-256color",
|
||||||
|
Options: map[string]interface{}{},
|
||||||
|
MultiModal: slices.Contains(show.Details.Families, "clip"),
|
||||||
|
ParentModel: show.Details.ParentModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
format, err := cmd.Flags().GetString("format")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
opts.Format = format
|
||||||
|
|
||||||
|
prompts := args[1:]
|
||||||
|
// prepend stdin to the prompt if provided
|
||||||
|
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||||
|
in, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
prompts = append([]string{string(in)}, prompts...)
|
||||||
|
opts.WordWrap = false
|
||||||
|
interactive = false
|
||||||
|
}
|
||||||
|
opts.Prompt = strings.Join(prompts, " ")
|
||||||
|
if len(prompts) > 0 {
|
||||||
|
interactive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
nowrap, err := cmd.Flags().GetBool("nowordwrap")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
opts.WordWrap = !nowrap
|
||||||
|
|
||||||
|
if !interactive {
|
||||||
|
return generate(cmd, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateInteractive(cmd, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PushHandler(cmd *cobra.Command, args []string) error {
|
func PushHandler(cmd *cobra.Command, args []string) error {
|
||||||
@@ -410,63 +606,20 @@ func PullHandler(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunGenerate(cmd *cobra.Command, args []string) error {
|
|
||||||
interactive := true
|
|
||||||
|
|
||||||
opts := runOptions{
|
|
||||||
Model: args[0],
|
|
||||||
WordWrap: os.Getenv("TERM") == "xterm-256color",
|
|
||||||
Options: map[string]interface{}{},
|
|
||||||
}
|
|
||||||
|
|
||||||
format, err := cmd.Flags().GetString("format")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
opts.Format = format
|
|
||||||
|
|
||||||
prompts := args[1:]
|
|
||||||
// prepend stdin to the prompt if provided
|
|
||||||
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
|
||||||
in, err := io.ReadAll(os.Stdin)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
prompts = append([]string{string(in)}, prompts...)
|
|
||||||
opts.WordWrap = false
|
|
||||||
interactive = false
|
|
||||||
}
|
|
||||||
opts.Prompt = strings.Join(prompts, " ")
|
|
||||||
if len(prompts) > 0 {
|
|
||||||
interactive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
nowrap, err := cmd.Flags().GetBool("nowordwrap")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
opts.WordWrap = !nowrap
|
|
||||||
|
|
||||||
if !interactive {
|
|
||||||
return generate(cmd, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
return generateInteractive(cmd, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
type generateContextKey string
|
type generateContextKey string
|
||||||
|
|
||||||
type runOptions struct {
|
type runOptions struct {
|
||||||
Model string
|
Model string
|
||||||
Prompt string
|
ParentModel string
|
||||||
Messages []api.Message
|
Prompt string
|
||||||
WordWrap bool
|
Messages []api.Message
|
||||||
Format string
|
WordWrap bool
|
||||||
System string
|
Format string
|
||||||
Template string
|
System string
|
||||||
Images []api.ImageData
|
Template string
|
||||||
Options map[string]interface{}
|
Images []api.ImageData
|
||||||
|
Options map[string]interface{}
|
||||||
|
MultiModal bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type displayResponseState struct {
|
type displayResponseState struct {
|
||||||
@@ -628,10 +781,18 @@ func generate(cmd *cobra.Command, opts runOptions) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.MultiModal {
|
||||||
|
opts.Prompt, opts.Images, err = extractFileData(opts.Prompt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
request := api.GenerateRequest{
|
request := api.GenerateRequest{
|
||||||
Model: opts.Model,
|
Model: opts.Model,
|
||||||
Prompt: opts.Prompt,
|
Prompt: opts.Prompt,
|
||||||
Context: generateContext,
|
Context: generateContext,
|
||||||
|
Images: opts.Images,
|
||||||
Format: opts.Format,
|
Format: opts.Format,
|
||||||
System: opts.System,
|
System: opts.System,
|
||||||
Template: opts.Template,
|
Template: opts.Template,
|
||||||
@@ -670,7 +831,7 @@ func generate(cmd *cobra.Command, opts runOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RunServer(cmd *cobra.Command, _ []string) error {
|
func RunServer(cmd *cobra.Command, _ []string) error {
|
||||||
host, port, err := net.SplitHostPort(os.Getenv("OLLAMA_HOST"))
|
host, port, err := net.SplitHostPort(strings.Trim(os.Getenv("OLLAMA_HOST"), "\"'"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
host, port = "127.0.0.1", "11434"
|
host, port = "127.0.0.1", "11434"
|
||||||
if ip := net.ParseIP(strings.Trim(os.Getenv("OLLAMA_HOST"), "[]")); ip != nil {
|
if ip := net.ParseIP(strings.Trim(os.Getenv("OLLAMA_HOST"), "[]")); ip != nil {
|
||||||
@@ -702,59 +863,42 @@ func initializeKeypair() error {
|
|||||||
_, err = os.Stat(privKeyPath)
|
_, err = os.Stat(privKeyPath)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
fmt.Printf("Couldn't find '%s'. Generating new private key.\n", privKeyPath)
|
fmt.Printf("Couldn't find '%s'. Generating new private key.\n", privKeyPath)
|
||||||
_, privKey, err := ed25519.GenerateKey(rand.Reader)
|
cryptoPublicKey, cryptoPrivateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
privKeyBytes, err := format.OpenSSHPrivateKey(privKey, "")
|
privateKeyBytes, err := ssh.MarshalPrivateKey(cryptoPrivateKey, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.MkdirAll(filepath.Dir(privKeyPath), 0o755)
|
if err := os.MkdirAll(filepath.Dir(privKeyPath), 0o755); err != nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not create directory %w", err)
|
return fmt.Errorf("could not create directory %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(privKeyPath, pem.EncodeToMemory(privKeyBytes), 0o600)
|
if err := os.WriteFile(privKeyPath, pem.EncodeToMemory(privateKeyBytes), 0o600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sshPublicKey, err := ssh.NewPublicKey(cryptoPublicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
sshPrivateKey, err := ssh.NewSignerFromKey(privKey)
|
publicKeyBytes := ssh.MarshalAuthorizedKey(sshPublicKey)
|
||||||
if err != nil {
|
|
||||||
|
if err := os.WriteFile(pubKeyPath, publicKeyBytes, 0o644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pubKeyData := ssh.MarshalAuthorizedKey(sshPrivateKey.PublicKey())
|
fmt.Printf("Your new public key is: \n\n%s\n", publicKeyBytes)
|
||||||
|
|
||||||
err = os.WriteFile(pubKeyPath, pubKeyData, 0o644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Your new public key is: \n\n%s\n", string(pubKeyData))
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func startMacApp(ctx context.Context, client *api.Client) error {
|
//nolint:unused
|
||||||
exe, err := os.Executable()
|
func waitForServer(ctx context.Context, client *api.Client) error {
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
link, err := os.Readlink(exe)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !strings.Contains(link, "Ollama.app") {
|
|
||||||
return fmt.Errorf("could not find ollama app")
|
|
||||||
}
|
|
||||||
path := strings.Split(link, "Ollama.app")
|
|
||||||
if err := exec.Command("/usr/bin/open", "-a", path[0]+"Ollama.app").Run(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// wait for the server to start
|
// wait for the server to start
|
||||||
timeout := time.After(5 * time.Second)
|
timeout := time.After(5 * time.Second)
|
||||||
tick := time.Tick(500 * time.Millisecond)
|
tick := time.Tick(500 * time.Millisecond)
|
||||||
@@ -768,6 +912,7 @@ func startMacApp(ctx context.Context, client *api.Client) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkServerHeartbeat(cmd *cobra.Command, _ []string) error {
|
func checkServerHeartbeat(cmd *cobra.Command, _ []string) error {
|
||||||
@@ -776,15 +921,11 @@ func checkServerHeartbeat(cmd *cobra.Command, _ []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := client.Heartbeat(cmd.Context()); err != nil {
|
if err := client.Heartbeat(cmd.Context()); err != nil {
|
||||||
if !strings.Contains(err.Error(), "connection refused") {
|
if !strings.Contains(err.Error(), " refused") {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if runtime.GOOS == "darwin" {
|
if err := startApp(cmd.Context(), client); err != nil {
|
||||||
if err := startMacApp(cmd.Context(), client); err != nil {
|
return fmt.Errorf("could not connect to ollama app, is it running?")
|
||||||
return fmt.Errorf("could not connect to ollama app, is it running?")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("could not connect to ollama server, run 'ollama serve' to start it")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -810,10 +951,22 @@ func versionHandler(cmd *cobra.Command, _ []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appendHostEnvDocs(cmd *cobra.Command) {
|
||||||
|
const hostEnvDocs = `
|
||||||
|
Environment Variables:
|
||||||
|
OLLAMA_HOST The host:port or base URL of the Ollama server (e.g. http://localhost:11434)
|
||||||
|
`
|
||||||
|
cmd.SetUsageTemplate(cmd.UsageTemplate() + hostEnvDocs)
|
||||||
|
}
|
||||||
|
|
||||||
func NewCLI() *cobra.Command {
|
func NewCLI() *cobra.Command {
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
cobra.EnableCommandSorting = false
|
cobra.EnableCommandSorting = false
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
console.ConsoleFromFile(os.Stdin) //nolint:errcheck
|
||||||
|
}
|
||||||
|
|
||||||
rootCmd := &cobra.Command{
|
rootCmd := &cobra.Command{
|
||||||
Use: "ollama",
|
Use: "ollama",
|
||||||
Short: "Large language model runner",
|
Short: "Large language model runner",
|
||||||
@@ -843,6 +996,7 @@ func NewCLI() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createCmd.Flags().StringP("file", "f", "Modelfile", "Name of the Modelfile (default \"Modelfile\")")
|
createCmd.Flags().StringP("file", "f", "Modelfile", "Name of the Modelfile (default \"Modelfile\")")
|
||||||
|
createCmd.Flags().StringP("quantization", "q", "", "Quantization level.")
|
||||||
|
|
||||||
showCmd := &cobra.Command{
|
showCmd := &cobra.Command{
|
||||||
Use: "show MODEL",
|
Use: "show MODEL",
|
||||||
@@ -870,7 +1024,6 @@ func NewCLI() *cobra.Command {
|
|||||||
runCmd.Flags().Bool("insecure", false, "Use an insecure registry")
|
runCmd.Flags().Bool("insecure", false, "Use an insecure registry")
|
||||||
runCmd.Flags().Bool("nowordwrap", false, "Don't wrap words to the next line automatically")
|
runCmd.Flags().Bool("nowordwrap", false, "Don't wrap words to the next line automatically")
|
||||||
runCmd.Flags().String("format", "", "Response format (e.g. json)")
|
runCmd.Flags().String("format", "", "Response format (e.g. json)")
|
||||||
|
|
||||||
serveCmd := &cobra.Command{
|
serveCmd := &cobra.Command{
|
||||||
Use: "serve",
|
Use: "serve",
|
||||||
Aliases: []string{"start"},
|
Aliases: []string{"start"},
|
||||||
@@ -878,6 +1031,15 @@ func NewCLI() *cobra.Command {
|
|||||||
Args: cobra.ExactArgs(0),
|
Args: cobra.ExactArgs(0),
|
||||||
RunE: RunServer,
|
RunE: RunServer,
|
||||||
}
|
}
|
||||||
|
serveCmd.SetUsageTemplate(serveCmd.UsageTemplate() + `
|
||||||
|
Environment Variables:
|
||||||
|
|
||||||
|
OLLAMA_HOST The host:port to bind to (default "127.0.0.1:11434")
|
||||||
|
OLLAMA_ORIGINS A comma separated list of allowed origins.
|
||||||
|
OLLAMA_MODELS The path to the models directory (default is "~/.ollama/models")
|
||||||
|
OLLAMA_KEEP_ALIVE The duration that models stay loaded in memory (default is "5m")
|
||||||
|
OLLAMA_DEBUG Set to 1 to enable additional debug logging
|
||||||
|
`)
|
||||||
|
|
||||||
pullCmd := &cobra.Command{
|
pullCmd := &cobra.Command{
|
||||||
Use: "pull MODEL",
|
Use: "pull MODEL",
|
||||||
@@ -906,7 +1068,6 @@ func NewCLI() *cobra.Command {
|
|||||||
PreRunE: checkServerHeartbeat,
|
PreRunE: checkServerHeartbeat,
|
||||||
RunE: ListHandler,
|
RunE: ListHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
copyCmd := &cobra.Command{
|
copyCmd := &cobra.Command{
|
||||||
Use: "cp SOURCE TARGET",
|
Use: "cp SOURCE TARGET",
|
||||||
Short: "Copy a model",
|
Short: "Copy a model",
|
||||||
@@ -923,6 +1084,19 @@ func NewCLI() *cobra.Command {
|
|||||||
RunE: DeleteHandler,
|
RunE: DeleteHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, cmd := range []*cobra.Command{
|
||||||
|
createCmd,
|
||||||
|
showCmd,
|
||||||
|
runCmd,
|
||||||
|
pullCmd,
|
||||||
|
pushCmd,
|
||||||
|
listCmd,
|
||||||
|
copyCmd,
|
||||||
|
deleteCmd,
|
||||||
|
} {
|
||||||
|
appendHostEnvDocs(cmd)
|
||||||
|
}
|
||||||
|
|
||||||
rootCmd.AddCommand(
|
rootCmd.AddCommand(
|
||||||
serveCmd,
|
serveCmd,
|
||||||
createCmd,
|
createCmd,
|
||||||
|
@@ -6,14 +6,17 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
"github.com/jmorganca/ollama/api"
|
"github.com/ollama/ollama/api"
|
||||||
"github.com/jmorganca/ollama/readline"
|
"github.com/ollama/ollama/progress"
|
||||||
|
"github.com/ollama/ollama/readline"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MultilineState int
|
type MultilineState int
|
||||||
@@ -25,45 +28,82 @@ const (
|
|||||||
MultilineTemplate
|
MultilineTemplate
|
||||||
)
|
)
|
||||||
|
|
||||||
func modelIsMultiModal(cmd *cobra.Command, name string) bool {
|
func loadModel(cmd *cobra.Command, opts *runOptions) error {
|
||||||
// get model details
|
|
||||||
client, err := api.ClientFromEnvironment()
|
client, err := api.ClientFromEnvironment()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("error: couldn't connect to ollama server")
|
return err
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
req := api.ShowRequest{Name: name}
|
p := progress.NewProgress(os.Stderr)
|
||||||
resp, err := client.Show(cmd.Context(), &req)
|
defer p.StopAndClear()
|
||||||
|
|
||||||
|
spinner := progress.NewSpinner("")
|
||||||
|
p.Add("", spinner)
|
||||||
|
|
||||||
|
showReq := api.ShowRequest{Name: opts.Model}
|
||||||
|
showResp, err := client.Show(cmd.Context(), &showReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return err
|
||||||
|
}
|
||||||
|
opts.MultiModal = slices.Contains(showResp.Details.Families, "clip")
|
||||||
|
opts.ParentModel = showResp.Details.ParentModel
|
||||||
|
|
||||||
|
if len(showResp.Messages) > 0 {
|
||||||
|
opts.Messages = append(opts.Messages, showResp.Messages...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return slices.Contains(resp.Details.Families, "clip")
|
chatReq := &api.ChatRequest{
|
||||||
|
Model: opts.Model,
|
||||||
|
Messages: []api.Message{},
|
||||||
|
}
|
||||||
|
err = client.Chat(cmd.Context(), chatReq, func(resp api.ChatResponse) error {
|
||||||
|
p.StopAndClear()
|
||||||
|
if len(opts.Messages) > 0 {
|
||||||
|
for _, msg := range opts.Messages {
|
||||||
|
switch msg.Role {
|
||||||
|
case "user":
|
||||||
|
fmt.Printf(">>> %s\n", msg.Content)
|
||||||
|
case "assistant":
|
||||||
|
state := &displayResponseState{}
|
||||||
|
displayResponse(msg.Content, opts.WordWrap, state)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateInteractive(cmd *cobra.Command, opts runOptions) error {
|
func generateInteractive(cmd *cobra.Command, opts runOptions) error {
|
||||||
multiModal := modelIsMultiModal(cmd, opts.Model)
|
opts.Messages = make([]api.Message, 0)
|
||||||
|
|
||||||
// load the model
|
err := loadModel(cmd, &opts)
|
||||||
loadOpts := runOptions{
|
if err != nil {
|
||||||
Model: opts.Model,
|
|
||||||
Prompt: "",
|
|
||||||
Messages: []api.Message{},
|
|
||||||
}
|
|
||||||
if _, err := chat(cmd, loadOpts); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
usage := func() {
|
usage := func() {
|
||||||
fmt.Fprintln(os.Stderr, "Available Commands:")
|
fmt.Fprintln(os.Stderr, "Available Commands:")
|
||||||
fmt.Fprintln(os.Stderr, " /set Set session variables")
|
fmt.Fprintln(os.Stderr, " /set Set session variables")
|
||||||
fmt.Fprintln(os.Stderr, " /show Show model information")
|
fmt.Fprintln(os.Stderr, " /show Show model information")
|
||||||
fmt.Fprintln(os.Stderr, " /bye Exit")
|
fmt.Fprintln(os.Stderr, " /load <model> Load a session or model")
|
||||||
fmt.Fprintln(os.Stderr, " /?, /help Help for a command")
|
fmt.Fprintln(os.Stderr, " /save <model> Save your current session")
|
||||||
fmt.Fprintln(os.Stderr, " /? shortcuts Help for keyboard shortcuts")
|
fmt.Fprintln(os.Stderr, " /bye Exit")
|
||||||
|
fmt.Fprintln(os.Stderr, " /?, /help Help for a command")
|
||||||
|
fmt.Fprintln(os.Stderr, " /? shortcuts Help for keyboard shortcuts")
|
||||||
fmt.Fprintln(os.Stderr, "")
|
fmt.Fprintln(os.Stderr, "")
|
||||||
fmt.Fprintln(os.Stderr, "Use \"\"\" to begin a multi-line message.")
|
fmt.Fprintln(os.Stderr, "Use \"\"\" to begin a multi-line message.")
|
||||||
|
|
||||||
|
if opts.MultiModal {
|
||||||
|
fmt.Fprintf(os.Stderr, "Use %s to include .jpg or .png images.\n", filepath.FromSlash("/path/to/file"))
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Fprintln(os.Stderr, "")
|
fmt.Fprintln(os.Stderr, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +180,6 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
|
|||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
var multiline MultilineState
|
var multiline MultilineState
|
||||||
opts.Messages = make([]api.Message, 0)
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
line, err := scanner.Readline()
|
line, err := scanner.Readline()
|
||||||
@@ -174,6 +213,7 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
|
|||||||
switch multiline {
|
switch multiline {
|
||||||
case MultilineSystem:
|
case MultilineSystem:
|
||||||
opts.System = sb.String()
|
opts.System = sb.String()
|
||||||
|
opts.Messages = append(opts.Messages, api.Message{Role: "system", Content: opts.System})
|
||||||
fmt.Println("Set system message.")
|
fmt.Println("Set system message.")
|
||||||
sb.Reset()
|
sb.Reset()
|
||||||
case MultilineTemplate:
|
case MultilineTemplate:
|
||||||
@@ -193,7 +233,6 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
|
|||||||
fmt.Fprintln(&sb)
|
fmt.Fprintln(&sb)
|
||||||
multiline = MultilinePrompt
|
multiline = MultilinePrompt
|
||||||
scanner.Prompt.UseAlt = true
|
scanner.Prompt.UseAlt = true
|
||||||
break
|
|
||||||
}
|
}
|
||||||
case scanner.Pasting:
|
case scanner.Pasting:
|
||||||
fmt.Fprintln(&sb, line)
|
fmt.Fprintln(&sb, line)
|
||||||
@@ -203,6 +242,44 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
|
|||||||
if err := ListHandler(cmd, args[1:]); err != nil {
|
if err := ListHandler(cmd, args[1:]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case strings.HasPrefix(line, "/load"):
|
||||||
|
args := strings.Fields(line)
|
||||||
|
if len(args) != 2 {
|
||||||
|
fmt.Println("Usage:\n /load <modelname>")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
opts.Model = args[1]
|
||||||
|
opts.Messages = []api.Message{}
|
||||||
|
fmt.Printf("Loading model '%s'\n", opts.Model)
|
||||||
|
if err := loadModel(cmd, &opts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
case strings.HasPrefix(line, "/save"):
|
||||||
|
args := strings.Fields(line)
|
||||||
|
if len(args) != 2 {
|
||||||
|
fmt.Println("Usage:\n /save <modelname>")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := api.ClientFromEnvironment()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("error: couldn't connect to ollama server")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &api.CreateRequest{
|
||||||
|
Name: args[1],
|
||||||
|
Modelfile: buildModelfile(opts),
|
||||||
|
}
|
||||||
|
fn := func(resp api.ProgressResponse) error { return nil }
|
||||||
|
err = client.Create(cmd.Context(), req, fn)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("error: couldn't save model")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Created new model '%s'\n", args[1])
|
||||||
|
continue
|
||||||
case strings.HasPrefix(line, "/set"):
|
case strings.HasPrefix(line, "/set"):
|
||||||
args := strings.Fields(line)
|
args := strings.Fields(line)
|
||||||
if len(args) > 1 {
|
if len(args) > 1 {
|
||||||
@@ -218,10 +295,14 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
|
|||||||
opts.WordWrap = false
|
opts.WordWrap = false
|
||||||
fmt.Println("Set 'nowordwrap' mode.")
|
fmt.Println("Set 'nowordwrap' mode.")
|
||||||
case "verbose":
|
case "verbose":
|
||||||
cmd.Flags().Set("verbose", "true")
|
if err := cmd.Flags().Set("verbose", "true"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
fmt.Println("Set 'verbose' mode.")
|
fmt.Println("Set 'verbose' mode.")
|
||||||
case "quiet":
|
case "quiet":
|
||||||
cmd.Flags().Set("verbose", "false")
|
if err := cmd.Flags().Set("verbose", "false"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
fmt.Println("Set 'quiet' mode.")
|
fmt.Println("Set 'quiet' mode.")
|
||||||
case "format":
|
case "format":
|
||||||
if len(args) < 3 || args[2] != "json" {
|
if len(args) < 3 || args[2] != "json" {
|
||||||
@@ -277,11 +358,21 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if args[1] == "system" {
|
if args[1] == "system" {
|
||||||
opts.System = sb.String()
|
opts.System = sb.String() // for display in modelfile
|
||||||
|
newMessage := api.Message{Role: "system", Content: sb.String()}
|
||||||
|
// Check if the slice is not empty and the last message is from 'system'
|
||||||
|
if len(opts.Messages) > 0 && opts.Messages[len(opts.Messages)-1].Role == "system" {
|
||||||
|
// Replace the last message
|
||||||
|
opts.Messages[len(opts.Messages)-1] = newMessage
|
||||||
|
} else {
|
||||||
|
opts.Messages = append(opts.Messages, newMessage)
|
||||||
|
}
|
||||||
fmt.Println("Set system message.")
|
fmt.Println("Set system message.")
|
||||||
|
sb.Reset()
|
||||||
} else if args[1] == "template" {
|
} else if args[1] == "template" {
|
||||||
opts.Template = sb.String()
|
opts.Template = sb.String()
|
||||||
fmt.Println("Set prompt template.")
|
fmt.Println("Set prompt template.")
|
||||||
|
sb.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.Reset()
|
sb.Reset()
|
||||||
@@ -383,13 +474,13 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
|
|||||||
} else {
|
} else {
|
||||||
usage()
|
usage()
|
||||||
}
|
}
|
||||||
case line == "/exit", line == "/bye":
|
case strings.HasPrefix(line, "/exit"), strings.HasPrefix(line, "/bye"):
|
||||||
return nil
|
return nil
|
||||||
case strings.HasPrefix(line, "/"):
|
case strings.HasPrefix(line, "/"):
|
||||||
args := strings.Fields(line)
|
args := strings.Fields(line)
|
||||||
isFile := false
|
isFile := false
|
||||||
|
|
||||||
if multiModal {
|
if opts.MultiModal {
|
||||||
for _, f := range extractFileNames(line) {
|
for _, f := range extractFileNames(line) {
|
||||||
if strings.HasPrefix(f, args[0]) {
|
if strings.HasPrefix(f, args[0]) {
|
||||||
isFile = true
|
isFile = true
|
||||||
@@ -411,34 +502,23 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
|
|||||||
if sb.Len() > 0 && multiline == MultilineNone {
|
if sb.Len() > 0 && multiline == MultilineNone {
|
||||||
newMessage := api.Message{Role: "user", Content: sb.String()}
|
newMessage := api.Message{Role: "user", Content: sb.String()}
|
||||||
|
|
||||||
if multiModal {
|
if opts.MultiModal {
|
||||||
msg, images, err := extractFileData(sb.String())
|
msg, images, err := extractFileData(sb.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
newMessage.Content = msg
|
|
||||||
|
|
||||||
// reset the context if we find another image
|
// clear all previous images for better responses
|
||||||
if len(images) > 0 {
|
if len(images) > 0 {
|
||||||
newMessage.Images = append(newMessage.Images, images...)
|
for i := range opts.Messages {
|
||||||
// reset the context for the new image
|
opts.Messages[i].Images = nil
|
||||||
opts.Messages = []api.Message{}
|
|
||||||
} else {
|
|
||||||
if len(opts.Messages) > 1 {
|
|
||||||
newMessage.Images = append(newMessage.Images, opts.Messages[len(opts.Messages)-2].Images...)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(newMessage.Images) == 0 {
|
|
||||||
fmt.Println("This model requires you to add a jpeg, png, or svg image.")
|
newMessage.Content = msg
|
||||||
fmt.Println()
|
newMessage.Images = images
|
||||||
sb.Reset()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.System != "" {
|
|
||||||
opts.Messages = append(opts.Messages, api.Message{Role: "system", Content: opts.System})
|
|
||||||
}
|
|
||||||
opts.Messages = append(opts.Messages, newMessage)
|
opts.Messages = append(opts.Messages, newMessage)
|
||||||
|
|
||||||
assistant, err := chat(cmd, opts)
|
assistant, err := chat(cmd, opts)
|
||||||
@@ -454,6 +534,38 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildModelfile(opts runOptions) string {
|
||||||
|
var mf strings.Builder
|
||||||
|
model := opts.ParentModel
|
||||||
|
if model == "" {
|
||||||
|
model = opts.Model
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&mf, "FROM %s\n", model)
|
||||||
|
if opts.System != "" {
|
||||||
|
fmt.Fprintf(&mf, "SYSTEM \"\"\"%s\"\"\"\n", opts.System)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Template != "" {
|
||||||
|
fmt.Fprintf(&mf, "TEMPLATE \"\"\"%s\"\"\"\n", opts.Template)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make([]string, 0)
|
||||||
|
for k := range opts.Options {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, k := range keys {
|
||||||
|
fmt.Fprintf(&mf, "PARAMETER %s %v\n", k, opts.Options[k])
|
||||||
|
}
|
||||||
|
fmt.Fprintln(&mf)
|
||||||
|
|
||||||
|
for _, msg := range opts.Messages {
|
||||||
|
fmt.Fprintf(&mf, "MESSAGE %s \"\"\"%s\"\"\"\n", msg.Role, msg.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mf.String()
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeFilePath(fp string) string {
|
func normalizeFilePath(fp string) string {
|
||||||
// Define a map of escaped characters and their replacements
|
// Define a map of escaped characters and their replacements
|
||||||
replacements := map[string]string{
|
replacements := map[string]string{
|
||||||
@@ -500,10 +612,10 @@ func extractFileData(input string) (string, []api.ImageData, error) {
|
|||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fmt.Printf("Couldn't process image: %q\n", err)
|
fmt.Fprintf(os.Stderr, "Couldn't process image: %q\n", err)
|
||||||
return "", imgs, err
|
return "", imgs, err
|
||||||
}
|
}
|
||||||
fmt.Printf("Added image '%s'\n", nfp)
|
fmt.Fprintf(os.Stderr, "Added image '%s'\n", nfp)
|
||||||
input = strings.ReplaceAll(input, fp, "")
|
input = strings.ReplaceAll(input, fp, "")
|
||||||
imgs = append(imgs, data)
|
imgs = append(imgs, data)
|
||||||
}
|
}
|
||||||
@@ -524,7 +636,7 @@ func getImageData(filePath string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
contentType := http.DetectContentType(buf)
|
contentType := http.DetectContentType(buf)
|
||||||
allowedTypes := []string{"image/jpeg", "image/jpg", "image/svg+xml", "image/png"}
|
allowedTypes := []string{"image/jpeg", "image/jpg", "image/png"}
|
||||||
if !slices.Contains(allowedTypes, contentType) {
|
if !slices.Contains(allowedTypes, contentType) {
|
||||||
return nil, fmt.Errorf("invalid image type: %s", contentType)
|
return nil, fmt.Errorf("invalid image type: %s", contentType)
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,13 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestExtractFilenames(t *testing.T) {
|
func TestExtractFilenames(t *testing.T) {
|
||||||
@@ -49,3 +53,64 @@ d:\path with\spaces\seven.svg inbetween7 c:\users\jdoe\eight.png inbetween8
|
|||||||
assert.Contains(t, res[9], "ten.svg")
|
assert.Contains(t, res[9], "ten.svg")
|
||||||
assert.Contains(t, res[9], "E:")
|
assert.Contains(t, res[9], "E:")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestModelfileBuilder(t *testing.T) {
|
||||||
|
opts := runOptions{
|
||||||
|
Model: "hork",
|
||||||
|
System: "You are part horse and part shark, but all hork. Do horklike things",
|
||||||
|
Template: "This is a template.",
|
||||||
|
Messages: []api.Message{
|
||||||
|
{Role: "user", Content: "Hey there hork!"},
|
||||||
|
{Role: "assistant", Content: "Yes it is true, I am half horse, half shark."},
|
||||||
|
},
|
||||||
|
Options: map[string]interface{}{},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Options["temperature"] = 0.9
|
||||||
|
opts.Options["seed"] = 42
|
||||||
|
opts.Options["penalize_newline"] = false
|
||||||
|
opts.Options["stop"] = []string{"hi", "there"}
|
||||||
|
|
||||||
|
mf := buildModelfile(opts)
|
||||||
|
expectedModelfile := `FROM {{.Model}}
|
||||||
|
SYSTEM """{{.System}}"""
|
||||||
|
TEMPLATE """{{.Template}}"""
|
||||||
|
PARAMETER penalize_newline false
|
||||||
|
PARAMETER seed 42
|
||||||
|
PARAMETER stop [hi there]
|
||||||
|
PARAMETER temperature 0.9
|
||||||
|
|
||||||
|
MESSAGE user """Hey there hork!"""
|
||||||
|
MESSAGE assistant """Yes it is true, I am half horse, half shark."""
|
||||||
|
`
|
||||||
|
|
||||||
|
tmpl, err := template.New("").Parse(expectedModelfile)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = tmpl.Execute(&buf, opts)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, buf.String(), mf)
|
||||||
|
|
||||||
|
opts.ParentModel = "horseshark"
|
||||||
|
mf = buildModelfile(opts)
|
||||||
|
expectedModelfile = `FROM {{.ParentModel}}
|
||||||
|
SYSTEM """{{.System}}"""
|
||||||
|
TEMPLATE """{{.Template}}"""
|
||||||
|
PARAMETER penalize_newline false
|
||||||
|
PARAMETER seed 42
|
||||||
|
PARAMETER stop [hi there]
|
||||||
|
PARAMETER temperature 0.9
|
||||||
|
|
||||||
|
MESSAGE user """Hey there hork!"""
|
||||||
|
MESSAGE assistant """Yes it is true, I am half horse, half shark."""
|
||||||
|
`
|
||||||
|
|
||||||
|
tmpl, err = template.New("").Parse(expectedModelfile)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
var parentBuf bytes.Buffer
|
||||||
|
err = tmpl.Execute(&parentBuf, opts)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, parentBuf.String(), mf)
|
||||||
|
}
|
||||||
|
30
cmd/start_darwin.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func startApp(ctx context.Context, client *api.Client) error {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
link, err := os.Readlink(exe)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !strings.Contains(link, "Ollama.app") {
|
||||||
|
return fmt.Errorf("could not find ollama app")
|
||||||
|
}
|
||||||
|
path := strings.Split(link, "Ollama.app")
|
||||||
|
if err := exec.Command("/usr/bin/open", "-a", path[0]+"Ollama.app").Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return waitForServer(ctx, client)
|
||||||
|
}
|
14
cmd/start_default.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build !windows && !darwin
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func startApp(ctx context.Context, client *api.Client) error {
|
||||||
|
return fmt.Errorf("could not connect to ollama server, run 'ollama serve' to start it")
|
||||||
|
}
|
58
cmd/start_windows.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func startApp(ctx context.Context, client *api.Client) error {
|
||||||
|
// log.Printf("XXX Attempting to find and start ollama app")
|
||||||
|
AppName := "ollama app.exe"
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
appExe := filepath.Join(filepath.Dir(exe), AppName)
|
||||||
|
_, err = os.Stat(appExe)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
// Try the standard install location
|
||||||
|
localAppData := os.Getenv("LOCALAPPDATA")
|
||||||
|
appExe = filepath.Join(localAppData, "Ollama", AppName)
|
||||||
|
_, err := os.Stat(appExe)
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
// Finally look in the path
|
||||||
|
appExe, err = exec.LookPath(AppName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not locate ollama app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// log.Printf("XXX attempting to start app %s", appExe)
|
||||||
|
|
||||||
|
cmd_path := "c:\\Windows\\system32\\cmd.exe"
|
||||||
|
cmd := exec.Command(cmd_path, "/c", appExe)
|
||||||
|
// TODO - these hide flags aren't working - still pops up a command window for some reason
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x08000000, HideWindow: true}
|
||||||
|
|
||||||
|
// TODO this didn't help either...
|
||||||
|
cmd.Stdin = strings.NewReader("")
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("unable to start ollama app %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Process != nil {
|
||||||
|
defer cmd.Process.Release() //nolint:errcheck
|
||||||
|
}
|
||||||
|
return waitForServer(ctx, client)
|
||||||
|
}
|
187
convert/convert.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package convert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
"github.com/ollama/ollama/convert/sentencepiece"
|
||||||
|
"github.com/ollama/ollama/llm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Params struct {
|
||||||
|
Architectures []string `json:"architectures"`
|
||||||
|
VocabSize int `json:"vocab_size"`
|
||||||
|
HiddenSize int `json:"hidden_size"` // n_embd
|
||||||
|
HiddenLayers int `json:"num_hidden_layers"` // n_layer
|
||||||
|
ContextSize int `json:"max_position_embeddings"`
|
||||||
|
IntermediateSize int `json:"intermediate_size"`
|
||||||
|
AttentionHeads int `json:"num_attention_heads"` // n_head
|
||||||
|
KeyValHeads int `json:"num_key_value_heads"`
|
||||||
|
NormEPS float64 `json:"rms_norm_eps"`
|
||||||
|
BoSTokenID int `json:"bos_token_id"`
|
||||||
|
EoSTokenID int `json:"eos_token_id"`
|
||||||
|
HeadDimension int `json:"head_dim"`
|
||||||
|
PaddingTokenID int `json:"pad_token_id"`
|
||||||
|
RopeFrequencyBase float64 `json:"rope_theta"`
|
||||||
|
|
||||||
|
Experts int `json:"num_local_experts"`
|
||||||
|
ExpertsUsed int `json:"num_experts_per_tok"`
|
||||||
|
|
||||||
|
ByteOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
type ByteOrder interface {
|
||||||
|
binary.ByteOrder
|
||||||
|
binary.AppendByteOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelArch interface {
|
||||||
|
GetTensors() error
|
||||||
|
LoadVocab() error
|
||||||
|
WriteGGUF() (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelFormat interface {
|
||||||
|
GetLayerName(string) (string, error)
|
||||||
|
GetTensors(string, *Params) ([]llm.Tensor, error)
|
||||||
|
GetParams(string) (*Params, error)
|
||||||
|
GetModelArch(string, string, *Params) (ModelArch, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelData struct {
|
||||||
|
Path string
|
||||||
|
Name string
|
||||||
|
Params *Params
|
||||||
|
Vocab *Vocab
|
||||||
|
Tensors []llm.Tensor
|
||||||
|
Format ModelFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetModelFormat(dirname string) (ModelFormat, error) {
|
||||||
|
files, err := filepath.Glob(filepath.Join(dirname, "*"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fn := range files {
|
||||||
|
slog.Debug(fmt.Sprintf("file = %s", fn))
|
||||||
|
if strings.HasSuffix(fn, ".safetensors") {
|
||||||
|
return &SafetensorFormat{}, nil
|
||||||
|
} else if strings.HasSuffix(fn, ".bin") {
|
||||||
|
slog.Debug("model is torch")
|
||||||
|
return &TorchFormat{}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("couldn't determine model format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Details on gguf's tokenizer can be found at:
|
||||||
|
// https://github.com/ggerganov/ggml/blob/master/docs/gguf.md#tokenizer
|
||||||
|
type Vocab struct {
|
||||||
|
Tokens []string
|
||||||
|
Scores []float32
|
||||||
|
Types []int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadSentencePieceTokens(dirpath string, params *Params) (*Vocab, error) {
|
||||||
|
slog.Info(fmt.Sprintf("reading vocab from %s", filepath.Join(dirpath, "tokenizer.model")))
|
||||||
|
in, err := os.ReadFile(filepath.Join(dirpath, "tokenizer.model"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// To regenerate sentencepiece from the protobufs use:
|
||||||
|
// protoc -I=./ --go_out=./ sentencepiece_model.proto
|
||||||
|
modelProto := &sentencepiece.ModelProto{}
|
||||||
|
if err := proto.Unmarshal(in, modelProto); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v := &Vocab{
|
||||||
|
Tokens: make([]string, 0),
|
||||||
|
Scores: make([]float32, 0),
|
||||||
|
Types: make([]int32, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
pieces := modelProto.GetPieces()
|
||||||
|
for _, p := range pieces {
|
||||||
|
v.Tokens = append(v.Tokens, p.GetPiece())
|
||||||
|
v.Scores = append(v.Scores, p.GetScore())
|
||||||
|
t := p.GetType()
|
||||||
|
switch t {
|
||||||
|
case sentencepiece.ModelProto_SentencePiece_UNKNOWN:
|
||||||
|
case sentencepiece.ModelProto_SentencePiece_CONTROL:
|
||||||
|
case sentencepiece.ModelProto_SentencePiece_UNUSED:
|
||||||
|
case sentencepiece.ModelProto_SentencePiece_BYTE:
|
||||||
|
default:
|
||||||
|
t = sentencepiece.ModelProto_SentencePiece_NORMAL
|
||||||
|
}
|
||||||
|
v.Types = append(v.Types, int32(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info(fmt.Sprintf("vocab size: %d", len(v.Tokens)))
|
||||||
|
|
||||||
|
// add any additional tokens
|
||||||
|
addIn, err := os.ReadFile(filepath.Join(dirpath, "added_tokens.json"))
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return v, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("reading user defined tokens")
|
||||||
|
|
||||||
|
var extraTokenData map[string]int
|
||||||
|
if err := json.Unmarshal(addIn, &extraTokenData); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type token struct {
|
||||||
|
key string
|
||||||
|
pos int
|
||||||
|
}
|
||||||
|
|
||||||
|
extraTokens := make([]token, 0)
|
||||||
|
for k, id := range extraTokenData {
|
||||||
|
extraTokens = append(extraTokens, token{k, id})
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(extraTokens, func(a, b token) int {
|
||||||
|
return cmp.Compare(a.pos, b.pos)
|
||||||
|
})
|
||||||
|
|
||||||
|
numToks := len(v.Tokens)
|
||||||
|
|
||||||
|
for cnt, t := range extraTokens {
|
||||||
|
// the token id should match the specific index for the total number of tokens
|
||||||
|
if t.pos != cnt+numToks {
|
||||||
|
return nil, fmt.Errorf("token ID '%d' for '%s' doesn't match total token size", t.pos, t.key)
|
||||||
|
}
|
||||||
|
v.Tokens = append(v.Tokens, t.key)
|
||||||
|
v.Scores = append(v.Scores, -1000.0)
|
||||||
|
v.Types = append(v.Types, int32(llm.GGUFTokenUserDefined))
|
||||||
|
}
|
||||||
|
slog.Info(fmt.Sprintf("vocab size w/ extra tokens: %d", len(v.Tokens)))
|
||||||
|
|
||||||
|
if params.VocabSize > len(v.Tokens) {
|
||||||
|
missingTokens := params.VocabSize - len(v.Tokens)
|
||||||
|
slog.Warn(fmt.Sprintf("vocab is missing %d tokens", missingTokens))
|
||||||
|
for cnt := 0; cnt < missingTokens; cnt++ {
|
||||||
|
v.Tokens = append(v.Tokens, fmt.Sprintf("<dummy%05d>", cnt+1))
|
||||||
|
v.Scores = append(v.Scores, -1)
|
||||||
|
v.Types = append(v.Types, int32(llm.GGUFTokenUserDefined))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, nil
|
||||||
|
}
|