Compare commits
	
		
			2447 Commits
		
	
	
		
			scratch
			...
			tool-parsi
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					8ed95a4e96 | ||
| 
						 | 
					bc83789be9 | ||
| 
						 | 
					4059b8db01 | ||
| 
						 | 
					b8b9c0c7cf | ||
| 
						 | 
					779547fcde | ||
| 
						 | 
					6cb7494061 | ||
| 
						 | 
					a44734b030 | ||
| 
						 | 
					b5a982ecb0 | ||
| 
						 | 
					516a540df7 | ||
| 
						 | 
					7f2f996cd6 | ||
| 
						 | 
					610054a234 | ||
| 
						 | 
					23125648b8 | ||
| 
						 | 
					0478d440f0 | ||
| 
						 | 
					8cc33f4c2b | ||
| 
						 | 
					f46df4e5d2 | ||
| 
						 | 
					c6bcdc4223 | ||
| 
						 | 
					4b903f088a | ||
| 
						 | 
					c7f4ae7b9c | ||
| 
						 | 
					526b2ed102 | ||
| 
						 | 
					a7240c6d63 | ||
| 
						 | 
					9d6df90805 | ||
| 
						 | 
					0cefd46f23 | ||
| 
						 | 
					ad035ad595 | ||
| 
						 | 
					f95a1f2bef | ||
| 
						 | 
					82a9e9462a | ||
| 
						 | 
					76724e2f29 | ||
| 
						 | 
					ecf14a220f | ||
| 
						 | 
					69ce44b33c | ||
| 
						 | 
					5969674cf1 | ||
| 
						 | 
					867d75b21e | ||
| 
						 | 
					3fa78598a1 | ||
| 
						 | 
					0d6e35d3c6 | ||
| 
						 | 
					6e9a7a2568 | ||
| 
						 | 
					b585a58121 | ||
| 
						 | 
					fa9973cd7f | ||
| 
						 | 
					3d9498a425 | ||
| 
						 | 
					3098c8b29b | ||
| 
						 | 
					5e380c3b42 | ||
| 
						 | 
					392de84031 | ||
| 
						 | 
					af31ccefc0 | ||
| 
						 | 
					fa393554b9 | ||
| 
						 | 
					307e3b3e1d | ||
| 
						 | 
					4090aca97b | ||
| 
						 | 
					92ce438de0 | ||
| 
						 | 
					424810450f | ||
| 
						 | 
					95e744beeb | ||
| 
						 | 
					3b2d2c8326 | ||
| 
						 | 
					d931ee8f22 | ||
| 
						 | 
					7073600797 | ||
| 
						 | 
					b1c40138da | ||
| 
						 | 
					17466217e5 | ||
| 
						 | 
					1703d1472e | ||
| 
						 | 
					913905028b | ||
| 
						 | 
					7e5c8eee5c | ||
| 
						 | 
					6a74bba7e7 | ||
| 
						 | 
					76ea735aaf | ||
| 
						 | 
					dd1d4e99e7 | ||
| 
						 | 
					a6ef73f4f2 | ||
| 
						 | 
					c2f5d6662b | ||
| 
						 | 
					57fb759f3c | ||
| 
						 | 
					8dd12c873d | ||
| 
						 | 
					e6d2d04121 | ||
| 
						 | 
					074bac8447 | ||
| 
						 | 
					8e8f2c6d67 | ||
| 
						 | 
					938e8447e8 | ||
| 
						 | 
					d5d5f0c445 | ||
| 
						 | 
					a7835c6716 | ||
| 
						 | 
					ad3c7c9bda | ||
| 
						 | 
					415c8fcc3d | ||
| 
						 | 
					718eda1b3e | ||
| 
						 | 
					421b7edeb4 | ||
| 
						 | 
					7b68e254c2 | ||
| 
						 | 
					7bec2724a5 | ||
| 
						 | 
					a27462b708 | ||
| 
						 | 
					6bf0b8193a | ||
| 
						 | 
					db428adbb8 | ||
| 
						 | 
					fe5b9bb21b | ||
| 
						 | 
					6ec71d8fb6 | ||
| 
						 | 
					44b466eeb2 | ||
| 
						 | 
					a25f3f8260 | ||
| 
						 | 
					dd93e1af85 | ||
| 
						 | 
					5cfc1c39f3 | ||
| 
						 | 
					f0ad49ea17 | ||
| 
						 | 
					7ba9fa9c7d | ||
| 
						 | 
					8bf11b84c1 | ||
| 
						 | 
					470af8ab89 | ||
| 
						 | 
					178761aef3 | ||
| 
						 | 
					f0c66e6dea | ||
| 
						 | 
					54055a6dae | ||
| 
						 | 
					340448d2d1 | ||
| 
						 | 
					ced7d0e53d | ||
| 
						 | 
					a0dba0f8ae | ||
| 
						 | 
					5e20b170a7 | ||
| 
						 | 
					d26c18e25c | ||
| 
						 | 
					8d376acc9b | ||
| 
						 | 
					dc1e81f027 | ||
| 
						 | 
					5d0279164c | ||
| 
						 | 
					214a7678ea | ||
| 
						 | 
					4892872c18 | ||
| 
						 | 
					0b9198bf47 | ||
| 
						 | 
					e9e5f61c45 | ||
| 
						 | 
					11dde41824 | ||
| 
						 | 
					a53d744b01 | ||
| 
						 | 
					40b10eee6d | ||
| 
						 | 
					424f648632 | ||
| 
						 | 
					2eb1fb3231 | ||
| 
						 | 
					0806521642 | ||
| 
						 | 
					88738b357b | ||
| 
						 | 
					4e535e6188 | ||
| 
						 | 
					40b8fdbdca | ||
| 
						 | 
					1d99451ad7 | ||
| 
						 | 
					09bb2e30f6 | ||
| 
						 | 
					dc264be6ff | ||
| 
						 | 
					fbe7039618 | ||
| 
						 | 
					943464ccb8 | ||
| 
						 | 
					369de832cd | ||
| 
						 | 
					3457a315b2 | ||
| 
						 | 
					ed4e139314 | ||
| 
						 | 
					56dc316a57 | ||
| 
						 | 
					2fec73eef6 | ||
| 
						 | 
					1e7f62cb42 | ||
| 
						 | 
					ccb7eb8135 | ||
| 
						 | 
					637fd21230 | ||
| 
						 | 
					0fe487e732 | ||
| 
						 | 
					6bfaa6e282 | ||
| 
						 | 
					378d3210dc | ||
| 
						 | 
					97fe45e36d | ||
| 
						 | 
					64a9cc8f05 | ||
| 
						 | 
					f50d691254 | ||
| 
						 | 
					34c3b68fc8 | ||
| 
						 | 
					f33ccd5d27 | ||
| 
						 | 
					bc108b9ad6 | ||
| 
						 | 
					ef65174df2 | ||
| 
						 | 
					42ecb9f138 | ||
| 
						 | 
					5c0331fd83 | ||
| 
						 | 
					e7019c9455 | ||
| 
						 | 
					d98bfe7e70 | ||
| 
						 | 
					6747099d71 | ||
| 
						 | 
					ccc8c6777b | ||
| 
						 | 
					dbb149e6f7 | ||
| 
						 | 
					a807985e59 | ||
| 
						 | 
					8643c4d5bf | ||
| 
						 | 
					b0c3aba590 | ||
| 
						 | 
					19c0c25de8 | ||
| 
						 | 
					2f723ac2d6 | ||
| 
						 | 
					249fbbe52f | ||
| 
						 | 
					c38680b8a1 | ||
| 
						 | 
					16fca86c4a | ||
| 
						 | 
					0f3f9e353d | ||
| 
						 | 
					6bd0a983cd | ||
| 
						 | 
					1861fbdeb5 | ||
| 
						 | 
					3b96a93672 | ||
| 
						 | 
					e53b3cbd0c | ||
| 
						 | 
					b51e0f397c | ||
| 
						 | 
					b42970063d | ||
| 
						 | 
					493385eb3e | ||
| 
						 | 
					9876c9faa4 | ||
| 
						 | 
					4e415029b3 | ||
| 
						 | 
					e172f095ba | ||
| 
						 | 
					c001b98087 | ||
| 
						 | 
					23fc8e92eb | ||
| 
						 | 
					4059a297a6 | ||
| 
						 | 
					66b2539238 | ||
| 
						 | 
					ef27d52e79 | ||
| 
						 | 
					b2a465296d | ||
| 
						 | 
					5d097277ef | ||
| 
						 | 
					071a9872cb | ||
| 
						 | 
					0bd0454ea7 | ||
| 
						 | 
					01aa788722 | ||
| 
						 | 
					ead27aa9fe | ||
| 
						 | 
					b816ff86c9 | ||
| 
						 | 
					e5d84fb90b | ||
| 
						 | 
					dd66712e31 | ||
| 
						 | 
					f66216e399 | ||
| 
						 | 
					f4f0992b6e | ||
| 
						 | 
					1feff61977 | ||
| 
						 | 
					5e0b904e88 | ||
| 
						 | 
					131f0355a5 | ||
| 
						 | 
					ce929984a3 | ||
| 
						 | 
					4b34930a31 | ||
| 
						 | 
					74bd09652d | ||
| 
						 | 
					fb6252d786 | ||
| 
						 | 
					c794fef2f2 | ||
| 
						 | 
					00ebda8cc4 | ||
| 
						 | 
					d14ce75b95 | ||
| 
						 | 
					2d6eac9084 | ||
| 
						 | 
					3ed7ad3ab3 | ||
| 
						 | 
					6d1103048e | ||
| 
						 | 
					0ff28758b3 | ||
| 
						 | 
					d3e9ca3eda | ||
| 
						 | 
					0fbfcf3c9c | ||
| 
						 | 
					0c220935bd | ||
| 
						 | 
					ffbfe833da | ||
| 
						 | 
					42a14f7f63 | ||
| 
						 | 
					f8c3dbe5b5 | ||
| 
						 | 
					b078dd157c | ||
| 
						 | 
					2ddacd7516 | ||
| 
						 | 
					da0e345200 | ||
| 
						 | 
					df94175a0f | ||
| 
						 | 
					61a8825216 | ||
| 
						 | 
					021dcf089d | ||
| 
						 | 
					bf24498b1e | ||
| 
						 | 
					95e271d98f | ||
| 
						 | 
					364629b8d6 | ||
| 
						 | 
					108fe02165 | ||
| 
						 | 
					4561fff36e | ||
| 
						 | 
					50b5962042 | ||
| 
						 | 
					e27e4a3c1b | ||
| 
						 | 
					088514bbd4 | ||
| 
						 | 
					2c8b484643 | ||
| 
						 | 
					8294676150 | ||
| 
						 | 
					ef378ad673 | ||
| 
						 | 
					2d2247e59e | ||
| 
						 | 
					7bf793a600 | ||
| 
						 | 
					282bfaaa95 | ||
| 
						 | 
					9679f40146 | ||
| 
						 | 
					3892c3a703 | ||
| 
						 | 
					4e320b8b90 | ||
| 
						 | 
					eb2b22b042 | ||
| 
						 | 
					4ea4d2b189 | ||
| 
						 | 
					8d76fa23ef | ||
| 
						 | 
					74b44fdf8f | ||
| 
						 | 
					65b88c544f | ||
| 
						 | 
					a422ba39c9 | ||
| 
						 | 
					d2ec22371e | ||
| 
						 | 
					033cec232a | ||
| 
						 | 
					543240fb5f | ||
| 
						 | 
					4bed739259 | ||
| 
						 | 
					80c7ce381b | ||
| 
						 | 
					ccfd41c4f0 | ||
| 
						 | 
					3e102b7dad | ||
| 
						 | 
					ec46f3286c | ||
| 
						 | 
					5e2e0b46b1 | ||
| 
						 | 
					45a13b1dec | ||
| 
						 | 
					5c0b663969 | ||
| 
						 | 
					30d7a59ba8 | ||
| 
						 | 
					4aeb67ef4c | ||
| 
						 | 
					3ba91634c1 | ||
| 
						 | 
					1b7433b71e | ||
| 
						 | 
					a70820daa0 | ||
| 
						 | 
					6b45b1d6b4 | ||
| 
						 | 
					85ab552028 | ||
| 
						 | 
					b3af953a55 | ||
| 
						 | 
					ad4e0bf3be | ||
| 
						 | 
					aee28501b5 | ||
| 
						 | 
					83f0ec8269 | ||
| 
						 | 
					c6b6938b3a | ||
| 
						 | 
					fb4664fcec | ||
| 
						 | 
					20e3593863 | ||
| 
						 | 
					63a394068c | ||
| 
						 | 
					ab39e08eb9 | ||
| 
						 | 
					11bfa62796 | ||
| 
						 | 
					f63e62e546 | ||
| 
						 | 
					65b0f329d1 | ||
| 
						 | 
					06007c0a18 | ||
| 
						 | 
					a8e83a7654 | ||
| 
						 | 
					475005504e | ||
| 
						 | 
					2c40c4d35e | ||
| 
						 | 
					e95278932b | ||
| 
						 | 
					9d2a20a763 | ||
| 
						 | 
					2e54d72fc3 | ||
| 
						 | 
					6b32a2d549 | ||
| 
						 | 
					c5cbe4fc2a | ||
| 
						 | 
					f888912870 | ||
| 
						 | 
					9e4642e9b3 | ||
| 
						 | 
					6b0486c216 | ||
| 
						 | 
					d368c039f0 | ||
| 
						 | 
					9b54267e69 | ||
| 
						 | 
					46bb0169c4 | ||
| 
						 | 
					8934324b72 | ||
| 
						 | 
					0e886595bf | ||
| 
						 | 
					c62861f4fa | ||
| 
						 | 
					0df1800436 | ||
| 
						 | 
					631fecc6d9 | ||
| 
						 | 
					4346c2409d | ||
| 
						 | 
					4b037a97dc | ||
| 
						 | 
					5f74d1fd47 | ||
| 
						 | 
					4dcf80167a | ||
| 
						 | 
					26a26998fb | ||
| 
						 | 
					9926eae015 | ||
| 
						 | 
					8585b7b151 | ||
| 
						 | 
					7e34f4fbfa | ||
| 
						 | 
					fe776293f7 | ||
| 
						 | 
					d8a5d96b98 | ||
| 
						 | 
					757668c42f | ||
| 
						 | 
					96ec8afd09 | ||
| 
						 | 
					e093db92c4 | ||
| 
						 | 
					a1cda80bcb | ||
| 
						 | 
					4614fafae0 | ||
| 
						 | 
					4100ed7bdd | ||
| 
						 | 
					f52b2615ef | ||
| 
						 | 
					25f9b152f9 | ||
| 
						 | 
					6da8b6a879 | ||
| 
						 | 
					0daaaef8c9 | ||
| 
						 | 
					98272fbd58 | ||
| 
						 | 
					b27e8f3f10 | ||
| 
						 | 
					45df786f09 | ||
| 
						 | 
					daaf42e4a4 | ||
| 
						 | 
					2dc60d4620 | ||
| 
						 | 
					b5312f30e8 | ||
| 
						 | 
					26c2e0bd35 | ||
| 
						 | 
					bf920883d5 | ||
| 
						 | 
					58b9ec1f6b | ||
| 
						 | 
					7bae7fa5ce | ||
| 
						 | 
					764e199d67 | ||
| 
						 | 
					bfce55db3d | ||
| 
						 | 
					bab6f34dc0 | ||
| 
						 | 
					0682dae027 | ||
| 
						 | 
					1f6986e919 | ||
| 
						 | 
					4289c74359 | ||
| 
						 | 
					25248f4bd5 | ||
| 
						 | 
					a7e63b82be | ||
| 
						 | 
					b70fc4d51e | ||
| 
						 | 
					e2252d0fc6 | ||
| 
						 | 
					cae5d4d4ea | ||
| 
						 | 
					05a01fdecb | ||
| 
						 | 
					8fe6f69f28 | ||
| 
						 | 
					1fdb351c37 | ||
| 
						 | 
					7a01ad7614 | ||
| 
						 | 
					55ab9f371a | ||
| 
						 | 
					fefbf8f74b | ||
| 
						 | 
					b428ddd796 | ||
| 
						 | 
					ba7d31240e | ||
| 
						 | 
					d25efe3954 | ||
| 
						 | 
					36dfb906bb | ||
| 
						 | 
					a6f0f908b9 | ||
| 
						 | 
					3b1ddb2b3a | ||
| 
						 | 
					1579c4f06d | ||
| 
						 | 
					3519dd1c6e | ||
| 
						 | 
					e41c4cbea7 | ||
| 
						 | 
					ee048b76d4 | ||
| 
						 | 
					af68d60a58 | ||
| 
						 | 
					21aa666a1e | ||
| 
						 | 
					ee141cc821 | ||
| 
						 | 
					55e5776c44 | ||
| 
						 | 
					854a9195f3 | ||
| 
						 | 
					96a97adf9b | ||
| 
						 | 
					e75c6126e9 | ||
| 
						 | 
					cda6f5c66c | ||
| 
						 | 
					bebb6823c0 | ||
| 
						 | 
					31e472baa4 | ||
| 
						 | 
					657685e85d | ||
| 
						 | 
					a14912858e | ||
| 
						 | 
					eed11ded30 | ||
| 
						 | 
					b42aba40ed | ||
| 
						 | 
					25885e5335 | ||
| 
						 | 
					98d44fa39d | ||
| 
						 | 
					2099e2d267 | ||
| 
						 | 
					0c1041ad85 | ||
| 
						 | 
					c245b0406f | ||
| 
						 | 
					8b194b7520 | ||
| 
						 | 
					3e8b8a1933 | ||
| 
						 | 
					41dc280491 | ||
| 
						 | 
					53d2990d9b | ||
| 
						 | 
					e185c08ad9 | ||
| 
						 | 
					2412adf42b | ||
| 
						 | 
					be2ac1ed93 | ||
| 
						 | 
					dc13813a03 | ||
| 
						 | 
					d6af13efed | ||
| 
						 | 
					a59f665235 | ||
| 
						 | 
					688925aca9 | ||
| 
						 | 
					76e903cf9d | ||
| 
						 | 
					a5272130c4 | ||
| 
						 | 
					d7d7e99662 | ||
| 
						 | 
					2db96c18e7 | ||
| 
						 | 
					e12af460ed | ||
| 
						 | 
					3ad4bc8afe | ||
| 
						 | 
					0d694793f2 | ||
| 
						 | 
					e91ae3d47d | ||
| 
						 | 
					6ecd7f64ba | ||
| 
						 | 
					888855675e | ||
| 
						 | 
					b16367b4b2 | ||
| 
						 | 
					a499390648 | ||
| 
						 | 
					4df98f3eb5 | ||
| 
						 | 
					348b3e0983 | ||
| 
						 | 
					0b7e1676eb | ||
| 
						 | 
					314573bfe8 | ||
| 
						 | 
					4604b10306 | ||
| 
						 | 
					8c13cfa4dd | ||
| 
						 | 
					7cfd4aee4d | ||
| 
						 | 
					68bac1e0a6 | ||
| 
						 | 
					f53f4198c3 | ||
| 
						 | 
					2192a28eed | ||
| 
						 | 
					5d81c1a184 | ||
| 
						 | 
					5c5535c064 | ||
| 
						 | 
					e5bcc51ae1 | ||
| 
						 | 
					bd6a7d5e64 | ||
| 
						 | 
					14b5a9a150 | ||
| 
						 | 
					ba9ec3d05e | ||
| 
						 | 
					7c168b08c9 | ||
| 
						 | 
					3d4cc7833c | ||
| 
						 | 
					351a85d9ea | ||
| 
						 | 
					bda4ef6c56 | ||
| 
						 | 
					1e438b237c | ||
| 
						 | 
					d721a02e7d | ||
| 
						 | 
					778603a818 | ||
| 
						 | 
					3c874df46e | ||
| 
						 | 
					d2eb226c91 | ||
| 
						 | 
					e13e7c8d94 | ||
| 
						 | 
					78f403ff45 | ||
| 
						 | 
					5f8c03189e | ||
| 
						 | 
					08a299e1d0 | ||
| 
						 | 
					7b5d916a9a | ||
| 
						 | 
					33ad61b112 | ||
| 
						 | 
					716e365615 | ||
| 
						 | 
					3b4424ff98 | ||
| 
						 | 
					f9c7ead160 | ||
| 
						 | 
					5930aaeb1a | ||
| 
						 | 
					faf67db089 | ||
| 
						 | 
					0667baddc6 | ||
| 
						 | 
					d006e1e09b | ||
| 
						 | 
					df2680b4b9 | ||
| 
						 | 
					010313bb63 | ||
| 
						 | 
					5296f487a8 | ||
| 
						 | 
					f05774b04c | ||
| 
						 | 
					6600bd7d91 | ||
| 
						 | 
					ed443a0393 | ||
| 
						 | 
					6945617af5 | ||
| 
						 | 
					7916f55009 | ||
| 
						 | 
					d650ad398f | ||
| 
						 | 
					d223f3b697 | ||
| 
						 | 
					60830695c2 | ||
| 
						 | 
					01d9a46854 | ||
| 
						 | 
					d773b7d671 | ||
| 
						 | 
					4d4463b2bd | ||
| 
						 | 
					0e38297f87 | ||
| 
						 | 
					7e13f568dc | ||
| 
						 | 
					58245413f4 | ||
| 
						 | 
					8cf16063a5 | ||
| 
						 | 
					3a4449e2f1 | ||
| 
						 | 
					10d59d5f90 | ||
| 
						 | 
					a4f69a0191 | ||
| 
						 | 
					82658c3eec | ||
| 
						 | 
					378d6e1e6a | ||
| 
						 | 
					afa55bc70c | ||
| 
						 | 
					49df03da9a | ||
| 
						 | 
					0189bdd0b7 | ||
| 
						 | 
					f4711da7bd | ||
| 
						 | 
					38117fba83 | ||
| 
						 | 
					1f766c36fb | ||
| 
						 | 
					484a99e428 | ||
| 
						 | 
					ec6121c331 | ||
| 
						 | 
					b86c0a1500 | ||
| 
						 | 
					7e402ebb8c | ||
| 
						 | 
					b901a712c6 | ||
| 
						 | 
					abb8dd57f8 | ||
| 
						 | 
					a400df48c0 | ||
| 
						 | 
					6ab4ba4c26 | ||
| 
						 | 
					e8d4eb3e68 | ||
| 
						 | 
					ae7e368f75 | ||
| 
						 | 
					31acd1ebf9 | ||
| 
						 | 
					9a4757ae66 | ||
| 
						 | 
					7814019708 | ||
| 
						 | 
					b698f9a0d8 | ||
| 
						 | 
					32285a6d19 | ||
| 
						 | 
					1c198977ec | ||
| 
						 | 
					330b6c50b0 | ||
| 
						 | 
					928911bc68 | ||
| 
						 | 
					5b446cc815 | ||
| 
						 | 
					451c1596af | ||
| 
						 | 
					932bded12f | ||
| 
						 | 
					070ad913ac | ||
| 
						 | 
					8d8b9f83ae | ||
| 
						 | 
					f00d359a67 | ||
| 
						 | 
					291def6adb | ||
| 
						 | 
					cd3fbf1c49 | ||
| 
						 | 
					c852b8e021 | ||
| 
						 | 
					d8932c55e7 | ||
| 
						 | 
					63f0269f7f | ||
| 
						 | 
					4759ecae19 | ||
| 
						 | 
					65b7ecac7b | ||
| 
						 | 
					f9d2d89135 | ||
| 
						 | 
					669dc31cf3 | ||
| 
						 | 
					d4d338c224 | ||
| 
						 | 
					bfdeffc375 | ||
| 
						 | 
					e806184023 | ||
| 
						 | 
					50566113ac | ||
| 
						 | 
					ad22ace439 | ||
| 
						 | 
					f4321a421c | ||
| 
						 | 
					475333d533 | ||
| 
						 | 
					39fd89308c | ||
| 
						 | 
					548a9f56a6 | ||
| 
						 | 
					3f0cb36bdb | ||
| 
						 | 
					bea1f1fac6 | ||
| 
						 | 
					5d75d837ef | ||
| 
						 | 
					711648c9bb | ||
| 
						 | 
					dcfb7a105c | ||
| 
						 | 
					2ef3c803a1 | ||
| 
						 | 
					453e4d090b | ||
| 
						 | 
					ca2f9843c8 | ||
| 
						 | 
					294b6f5a22 | ||
| 
						 | 
					7bb356c680 | ||
| 
						 | 
					021817e59a | ||
| 
						 | 
					a420a453b4 | ||
| 
						 | 
					42cf4db601 | ||
| 
						 | 
					93a8daf285 | ||
| 
						 | 
					a041b4df7c | ||
| 
						 | 
					2539f2dbf9 | ||
| 
						 | 
					61676fb506 | ||
| 
						 | 
					f6f3713001 | ||
| 
						 | 
					a30f347201 | ||
| 
						 | 
					74ea4fb604 | ||
| 
						 | 
					6982e9cc96 | ||
| 
						 | 
					ab39872cb4 | ||
| 
						 | 
					84a2314463 | ||
| 
						 | 
					17fcdea698 | ||
| 
						 | 
					32bd37adf8 | ||
| 
						 | 
					9446c2c902 | ||
| 
						 | 
					9aa141d023 | ||
| 
						 | 
					8bccae4f92 | ||
| 
						 | 
					6ae2adc1af | ||
| 
						 | 
					1deafd8254 | ||
| 
						 | 
					57f038ec7b | ||
| 
						 | 
					cdf3a181dc | ||
| 
						 | 
					3919f4ba3d | ||
| 
						 | 
					2d33c4e97d | ||
| 
						 | 
					29a8975c66 | ||
| 
						 | 
					86a622cbdc | ||
| 
						 | 
					459d822b51 | ||
| 
						 | 
					844899440a | ||
| 
						 | 
					103db4216d | ||
| 
						 | 
					6daddcde01 | ||
| 
						 | 
					07f7e69b36 | ||
| 
						 | 
					b68e8e5727 | ||
| 
						 | 
					369fb529e2 | ||
| 
						 | 
					023e4bca14 | ||
| 
						 | 
					51af455f62 | ||
| 
						 | 
					ffe3549064 | ||
| 
						 | 
					928de9050e | ||
| 
						 | 
					36aea6154a | ||
| 
						 | 
					dd352ab27f | ||
| 
						 | 
					cb40d60469 | ||
| 
						 | 
					d8bab8ea44 | ||
| 
						 | 
					9ab62eb96f | ||
| 
						 | 
					290cf2040a | ||
| 
						 | 
					a72f2dce45 | ||
| 
						 | 
					08a832b482 | ||
| 
						 | 
					2ddc32d5c5 | ||
| 
						 | 
					2cde4b8817 | ||
| 
						 | 
					87f0a49fe6 | ||
| 
						 | 
					0f06a6daa7 | ||
| 
						 | 
					8f805dd74b | ||
| 
						 | 
					89d5e2f2fd | ||
| 
						 | 
					297ada6c87 | ||
| 
						 | 
					8c9fb8eb73 | ||
| 
						 | 
					b75ccfc5ec | ||
| 
						 | 
					7a81daf026 | ||
| 
						 | 
					60f75560a2 | ||
| 
						 | 
					e28f2d4900 | ||
| 
						 | 
					c216850523 | ||
| 
						 | 
					18f6a98bd6 | ||
| 
						 | 
					b1fd7fef86 | ||
| 
						 | 
					36d111e788 | ||
| 
						 | 
					9039c821a2 | ||
| 
						 | 
					581a4a5553 | ||
| 
						 | 
					cf4d7c52c4 | ||
| 
						 | 
					6a6328a5e9 | ||
| 
						 | 
					527cc97899 | ||
| 
						 | 
					a37f4a86a7 | ||
| 
						 | 
					46f74e0cb5 | ||
| 
						 | 
					7622ea21af | ||
| 
						 | 
					c5d3947084 | ||
| 
						 | 
					757eeacc1b | ||
| 
						 | 
					dd42acf737 | ||
| 
						 | 
					b9ccb3741e | ||
| 
						 | 
					abfdc4710f | ||
| 
						 | 
					82a02e18d9 | ||
| 
						 | 
					4879a234c4 | ||
| 
						 | 
					63269668c0 | ||
| 
						 | 
					900f64e6be | ||
| 
						 | 
					da09488fbf | ||
| 
						 | 
					7f0ccc8a9d | ||
| 
						 | 
					de52b6c2f9 | ||
| 
						 | 
					acd7d03266 | ||
| 
						 | 
					f6e87fd628 | ||
| 
						 | 
					aed1419c64 | ||
| 
						 | 
					c6c526275d | ||
| 
						 | 
					630e7dc6ff | ||
| 
						 | 
					eb8366d658 | ||
| 
						 | 
					4456012956 | ||
| 
						 | 
					539be43640 | ||
| 
						 | 
					1bdab9fdb1 | ||
| 
						 | 
					2b82c5a8a1 | ||
| 
						 | 
					55c3efa900 | ||
| 
						 | 
					1aedffad93 | ||
| 
						 | 
					ff6c2d6dc8 | ||
| 
						 | 
					d543b282a7 | ||
| 
						 | 
					5f8051180e | ||
| 
						 | 
					39e29ae5dd | ||
| 
						 | 
					30a9f063c9 | ||
| 
						 | 
					ce7455a8e1 | ||
| 
						 | 
					e3936d4fb3 | ||
| 
						 | 
					940e62772e | ||
| 
						 | 
					71e6a0d0d1 | ||
| 
						 | 
					2cd11ae365 | ||
| 
						 | 
					52bbad12f9 | ||
| 
						 | 
					30e88d7f31 | ||
| 
						 | 
					2b7ed61ca2 | ||
| 
						 | 
					647513a7d4 | ||
| 
						 | 
					a210ec74d2 | ||
| 
						 | 
					cfb1ddd6fc | ||
| 
						 | 
					3987acd7ec | ||
| 
						 | 
					fda1e6b563 | ||
| 
						 | 
					3440ffb37b | ||
| 
						 | 
					a820d2b267 | ||
| 
						 | 
					2ebdb54fb3 | ||
| 
						 | 
					bb52abfa55 | ||
| 
						 | 
					31cb1ca9e5 | ||
| 
						 | 
					78f779a323 | ||
| 
						 | 
					3478b2cf14 | ||
| 
						 | 
					7b5585b9cb | ||
| 
						 | 
					f0a351810c | ||
| 
						 | 
					b85520bfb9 | ||
| 
						 | 
					d88972ea48 | ||
| 
						 | 
					25c9339e2d | ||
| 
						 | 
					597072ef1b | ||
| 
						 | 
					84b3e07f1b | ||
| 
						 | 
					422d52858c | ||
| 
						 | 
					723f285813 | ||
| 
						 | 
					eaaf5d309d | ||
| 
						 | 
					27d9c749d5 | ||
| 
						 | 
					b7bddeebc1 | ||
| 
						 | 
					6a0c2ec50f | ||
| 
						 | 
					baa41be2aa | ||
| 
						 | 
					2157b1232e | ||
| 
						 | 
					37711578a2 | ||
| 
						 | 
					fb2c9594e0 | ||
| 
						 | 
					7fbcd55da3 | ||
| 
						 | 
					b4348bdd25 | ||
| 
						 | 
					155734e09a | ||
| 
						 | 
					883d80e097 | ||
| 
						 | 
					e4c9f75b23 | ||
| 
						 | 
					f5ec7cc872 | ||
| 
						 | 
					811bafba82 | ||
| 
						 | 
					431075fcbb | ||
| 
						 | 
					c4f27225ac | ||
| 
						 | 
					b7aa5ee06c | ||
| 
						 | 
					3f87f71755 | ||
| 
						 | 
					20623cec13 | ||
| 
						 | 
					0e5f31a86d | ||
| 
						 | 
					7e92091751 | ||
| 
						 | 
					1a742f54c9 | ||
| 
						 | 
					6a89dcf848 | ||
| 
						 | 
					c5e238e8e5 | ||
| 
						 | 
					fce30f407a | ||
| 
						 | 
					d863298210 | ||
| 
						 | 
					c4b34f2a2a | ||
| 
						 | 
					c3ff916431 | ||
| 
						 | 
					3fc1dc0e6f | ||
| 
						 | 
					7121dfa309 | ||
| 
						 | 
					5f68fcab12 | ||
| 
						 | 
					ecf41eed05 | ||
| 
						 | 
					b8c66d3307 | ||
| 
						 | 
					303f4bc79e | ||
| 
						 | 
					d2a25206b1 | ||
| 
						 | 
					2f0a8c8778 | ||
| 
						 | 
					bfd30f4286 | ||
| 
						 | 
					0ef17ede89 | ||
| 
						 | 
					909a88c5c0 | ||
| 
						 | 
					f602ab4de4 | ||
| 
						 | 
					807ace5b1f | ||
| 
						 | 
					4b8a2e341a | ||
| 
						 | 
					e66c29261a | ||
| 
						 | 
					712d63c3f0 | ||
| 
						 | 
					6cdf27d154 | ||
| 
						 | 
					5c18e66384 | ||
| 
						 | 
					35096a7eff | ||
| 
						 | 
					81d55d3e4d | ||
| 
						 | 
					a14f76491d | ||
| 
						 | 
					760cfa27e5 | ||
| 
						 | 
					c9a5aca3da | ||
| 
						 | 
					d5da2ab7e8 | ||
| 
						 | 
					1c04117114 | ||
| 
						 | 
					8b4b243f5f | ||
| 
						 | 
					b42a596425 | ||
| 
						 | 
					4759d879f2 | ||
| 
						 | 
					d875e99e46 | ||
| 
						 | 
					8a35bb926e | ||
| 
						 | 
					a0ea067b63 | ||
| 
						 | 
					4efb98cb4f | ||
| 
						 | 
					0679d491fe | ||
| 
						 | 
					c25ffde91d | ||
| 
						 | 
					17b386a891 | ||
| 
						 | 
					549c2bdfcf | ||
| 
						 | 
					67691e410d | ||
| 
						 | 
					5b3393b6a2 | ||
| 
						 | 
					d7eb05b936 | ||
| 
						 | 
					636a743c2b | ||
| 
						 | 
					df011054fa | ||
| 
						 | 
					ac07160c8d | ||
| 
						 | 
					6606e4243c | ||
| 
						 | 
					65973ceb64 | ||
| 
						 | 
					bebef1e50d | ||
| 
						 | 
					d48c1c5a44 | ||
| 
						 | 
					36a8372b28 | ||
| 
						 | 
					4e94227b5d | ||
| 
						 | 
					479d551766 | ||
| 
						 | 
					76b2b723b2 | ||
| 
						 | 
					b8d77cdeab | ||
| 
						 | 
					c2e8cbaa14 | ||
| 
						 | 
					771fab1dd8 | ||
| 
						 | 
					3a5239e6bf | ||
| 
						 | 
					3d25e7bf8c | ||
| 
						 | 
					1618700c5a | ||
| 
						 | 
					b111aa5a91 | ||
| 
						 | 
					9e83e550e1 | ||
| 
						 | 
					fc2a0715df | ||
| 
						 | 
					3020d2dc58 | ||
| 
						 | 
					a909417602 | ||
| 
						 | 
					6cd566872b | ||
| 
						 | 
					9d71bcc3e2 | ||
| 
						 | 
					a4c70fe157 | ||
| 
						 | 
					34a75102f7 | ||
| 
						 | 
					4157d1f7b6 | ||
| 
						 | 
					4ebfa2cb91 | ||
| 
						 | 
					046054fa3b | ||
| 
						 | 
					95483f348b | ||
| 
						 | 
					f247a6233e | ||
| 
						 | 
					44bd9e5994 | ||
| 
						 | 
					18237be9b2 | ||
| 
						 | 
					29ab9fa7d7 | ||
| 
						 | 
					b8d5036e33 | ||
| 
						 | 
					312d9de1d1 | ||
| 
						 | 
					a103dae01e | ||
| 
						 | 
					d07cf41a97 | ||
| 
						 | 
					8c238e70ab | ||
| 
						 | 
					8a9bb0d000 | ||
| 
						 | 
					26acdcf44e | ||
| 
						 | 
					921779bb10 | ||
| 
						 | 
					16f4eabe2d | ||
| 
						 | 
					c826e57475 | ||
| 
						 | 
					712e99d477 | ||
| 
						 | 
					b754f5a6a3 | ||
| 
						 | 
					a805e5947e | ||
| 
						 | 
					91dfbb1bba | ||
| 
						 | 
					db1842b9e1 | ||
| 
						 | 
					c9ca386131 | ||
| 
						 | 
					078f666f73 | ||
| 
						 | 
					de1557a0dc | ||
| 
						 | 
					084929c293 | ||
| 
						 | 
					abd5dfd06a | ||
| 
						 | 
					099f7077a1 | ||
| 
						 | 
					d7c94e0ca6 | ||
| 
						 | 
					35ec7f079f | ||
| 
						 | 
					5231ae52d9 | ||
| 
						 | 
					3085c47bea | ||
| 
						 | 
					0ccc73251a | ||
| 
						 | 
					dc6fe82051 | ||
| 
						 | 
					d78fb62056 | ||
| 
						 | 
					5c44461ccf | ||
| 
						 | 
					03e40efa51 | ||
| 
						 | 
					23f746508d | ||
| 
						 | 
					48708ca0d5 | ||
| 
						 | 
					c7cb0f0602 | ||
| 
						 | 
					bf4018b9ec | ||
| 
						 | 
					f86d00cd95 | ||
| 
						 | 
					f2890a4494 | ||
| 
						 | 
					05cd82ef94 | ||
| 
						 | 
					7d6eb0d4c3 | ||
| 
						 | 
					24636dfa87 | ||
| 
						 | 
					1d7fa3ad2d | ||
| 
						 | 
					09035b71cd | ||
| 
						 | 
					f3c8b898cd | ||
| 
						 | 
					5dd0477fd4 | ||
| 
						 | 
					c3d321d405 | ||
| 
						 | 
					7fe3902552 | ||
| 
						 | 
					0077e22d52 | ||
| 
						 | 
					03408f3437 | ||
| 
						 | 
					cd7e01e8b9 | ||
| 
						 | 
					7a962bd802 | ||
| 
						 | 
					f9584deba5 | ||
| 
						 | 
					96efd9052f | ||
| 
						 | 
					de982616f1 | ||
| 
						 | 
					defbf9425a | ||
| 
						 | 
					f40bb398f6 | ||
| 
						 | 
					79d3b1e2bd | ||
| 
						 | 
					03608cb46e | ||
| 
						 | 
					450acb71a6 | ||
| 
						 | 
					55ea963c9e | ||
| 
						 | 
					e9e9bdb8d9 | ||
| 
						 | 
					35bb6d32b3 | ||
| 
						 | 
					98701b58b3 | ||
| 
						 | 
					ad935f45ac | ||
| 
						 | 
					dbba73469d | ||
| 
						 | 
					6c2eb73a70 | ||
| 
						 | 
					2a038c1d7e | ||
| 
						 | 
					616c5eafee | ||
| 
						 | 
					f5ff917b1d | ||
| 
						 | 
					d632e23fba | ||
| 
						 | 
					5804cf1723 | ||
| 
						 | 
					bf7ee0f4d4 | ||
| 
						 | 
					504a410f02 | ||
| 
						 | 
					d05da29912 | ||
| 
						 | 
					72962c6e08 | ||
| 
						 | 
					7bd7b02712 | ||
| 
						 | 
					8f9ab5e14d | ||
| 
						 | 
					7717bb6a84 | ||
| 
						 | 
					0ec2915ea7 | ||
| 
						 | 
					c9a7541b9c | ||
| 
						 | 
					d81cfd7d6f | ||
| 
						 | 
					b330c830d3 | ||
| 
						 | 
					d889c6fd07 | ||
| 
						 | 
					56b9af336a | ||
| 
						 | 
					fda0d3be52 | ||
| 
						 | 
					cd5c8f6471 | ||
| 
						 | 
					fef257c5c5 | ||
| 
						 | 
					d066d9b8e0 | ||
| 
						 | 
					5a00dc9fc9 | ||
| 
						 | 
					c354e87809 | ||
| 
						 | 
					93ac3760cb | ||
| 
						 | 
					abed273de3 | ||
| 
						 | 
					034392624c | ||
| 
						 | 
					ecab6f1cc5 | ||
| 
						 | 
					7d6900827d | ||
| 
						 | 
					9246e6dd15 | ||
| 
						 | 
					735a0ca2e4 | ||
| 
						 | 
					dddb72e084 | ||
| 
						 | 
					83a9b5271a | ||
| 
						 | 
					4a8069f9c4 | ||
| 
						 | 
					84b84ce2db | ||
| 
						 | 
					bb6a086d63 | ||
| 
						 | 
					30c8f201cc | ||
| 
						 | 
					06d4fba851 | ||
| 
						 | 
					108fb6c1d1 | ||
| 
						 | 
					da915345d1 | ||
| 
						 | 
					8a027bc401 | ||
| 
						 | 
					5446903fbd | ||
| 
						 | 
					56318fb365 | ||
| 
						 | 
					fe91d7fff1 | ||
| 
						 | 
					608e87bf87 | ||
| 
						 | 
					48685c6ed0 | ||
| 
						 | 
					9565fa64a8 | ||
| 
						 | 
					6719097649 | ||
| 
						 | 
					b05c9e83d9 | ||
| 
						 | 
					a60d9b89ce | ||
| 
						 | 
					bf612cd608 | ||
| 
						 | 
					ef98e56122 | ||
| 
						 | 
					5f944baac7 | ||
| 
						 | 
					6fc9d22707 | ||
| 
						 | 
					f27c00d8c5 | ||
| 
						 | 
					c7c845ec52 | ||
| 
						 | 
					cf48603943 | ||
| 
						 | 
					6e67be09b6 | ||
| 
						 | 
					0f5f060d2b | ||
| 
						 | 
					b3554778bd | ||
| 
						 | 
					bbe7b96ded | ||
| 
						 | 
					c18ff18b2c | ||
| 
						 | 
					133770a548 | ||
| 
						 | 
					f36ebfb478 | ||
| 
						 | 
					5b55379651 | ||
| 
						 | 
					93eb43d020 | ||
| 
						 | 
					369479cc30 | ||
| 
						 | 
					7d89e48f5c | ||
| 
						 | 
					27bcce6d9f | ||
| 
						 | 
					491fc312ae | ||
| 
						 | 
					5e2653f9fe | ||
| 
						 | 
					f29b167e1a | ||
| 
						 | 
					037a4d103e | ||
| 
						 | 
					50c05d57e0 | ||
| 
						 | 
					35159de18a | ||
| 
						 | 
					94fff5805f | ||
| 
						 | 
					14d5093cd0 | ||
| 
						 | 
					9df5f0e8e4 | ||
| 
						 | 
					ad3eb00bee | ||
| 
						 | 
					bfc2d61549 | ||
| 
						 | 
					741affdfd6 | ||
| 
						 | 
					5f7b4a5e30 | ||
| 
						 | 
					1aad838707 | ||
| 
						 | 
					a1cef4d0a5 | ||
| 
						 | 
					c41f0b9e6c | ||
| 
						 | 
					142cbb722d | ||
| 
						 | 
					9468c6824a | ||
| 
						 | 
					11018196e0 | ||
| 
						 | 
					56346ccfa3 | ||
| 
						 | 
					8e4e509fa4 | ||
| 
						 | 
					47c2b947a9 | ||
| 
						 | 
					5eb77bf976 | ||
| 
						 | 
					e4d0a9c325 | ||
| 
						 | 
					7416ced70f | ||
| 
						 | 
					9cfd2dd3e3 | ||
| 
						 | 
					8e6da3cbc5 | ||
| 
						 | 
					d9d50c43cc | ||
| 
						 | 
					6c1c1ad6a9 | ||
| 
						 | 
					93ea9240ae | ||
| 
						 | 
					413ae39f3c | ||
| 
						 | 
					60e47573a6 | ||
| 
						 | 
					d13c3daa0b | ||
| 
						 | 
					1713eddcd0 | ||
| 
						 | 
					4e1c4f6e0b | ||
| 
						 | 
					397cae7962 | ||
| 
						 | 
					1c70a00f71 | ||
| 
						 | 
					eae3af6807 | ||
| 
						 | 
					3eb08377f8 | ||
| 
						 | 
					ac80010db8 | ||
| 
						 | 
					47fa0839b9 | ||
| 
						 | 
					0f92b19bec | ||
| 
						 | 
					69be940bf6 | ||
| 
						 | 
					9638c24c58 | ||
| 
						 | 
					bb362caf88 | ||
| 
						 | 
					386af6c1a0 | ||
| 
						 | 
					0c819e167b | ||
| 
						 | 
					7a1e1c1caf | ||
| 
						 | 
					0b03b9c32f | ||
| 
						 | 
					90ca84172c | ||
| 
						 | 
					6bd8a4b0a1 | ||
| 
						 | 
					77903ab8b4 | ||
| 
						 | 
					e22286c9e1 | ||
| 
						 | 
					107f695929 | ||
| 
						 | 
					4ecc70d3b4 | ||
| 
						 | 
					3546bbd08c | ||
| 
						 | 
					beb49eef65 | ||
| 
						 | 
					5a28b9cf5f | ||
| 
						 | 
					a017cf2fea | ||
| 
						 | 
					19e5a890f7 | ||
| 
						 | 
					f91c9e3709 | ||
| 
						 | 
					2df6905ede | ||
| 
						 | 
					d8be22e47d | ||
| 
						 | 
					652c273f0e | ||
| 
						 | 
					88e7705079 | ||
| 
						 | 
					f9e31da946 | ||
| 
						 | 
					88bb9e3328 | ||
| 
						 | 
					3b19cdba2a | ||
| 
						 | 
					927d98a6cd | ||
| 
						 | 
					f6c811b320 | ||
| 
						 | 
					4fe3a556fa | ||
| 
						 | 
					fc3b4cda89 | ||
| 
						 | 
					d470ebe78b | ||
| 
						 | 
					c7bcb00319 | ||
| 
						 | 
					74d45f0102 | ||
| 
						 | 
					9fddef3731 | ||
| 
						 | 
					885cf45087 | ||
| 
						 | 
					9352eeb752 | ||
| 
						 | 
					0ad0e738cd | ||
| 
						 | 
					bdc4308afb | ||
| 
						 | 
					d29cd4c2ed | ||
| 
						 | 
					a84c05cf91 | ||
| 
						 | 
					e3d7f32af7 | ||
| 
						 | 
					3a75e74e34 | ||
| 
						 | 
					237dccba1e | ||
| 
						 | 
					b3f75fc812 | ||
| 
						 | 
					8200c371ae | ||
| 
						 | 
					0a8d6ea86d | ||
| 
						 | 
					8e1050f366 | ||
| 
						 | 
					eda8a32a09 | ||
| 
						 | 
					a0a40aa20c | ||
| 
						 | 
					2697d7f5aa | ||
| 
						 | 
					1f32276178 | ||
| 
						 | 
					4c4fe3f87f | ||
| 
						 | 
					feedf49c71 | ||
| 
						 | 
					8b00a415ab | ||
| 
						 | 
					01b80e9ffc | ||
| 
						 | 
					bd5e432630 | ||
| 
						 | 
					aec77d6a05 | ||
| 
						 | 
					6ffb5cb017 | ||
| 
						 | 
					f7e3b9190f | ||
| 
						 | 
					980dd15f81 | ||
| 
						 | 
					01d544d373 | ||
| 
						 | 
					1dc3ef3aa9 | ||
| 
						 | 
					8aac22438e | ||
| 
						 | 
					15c2d8fe14 | ||
| 
						 | 
					25906d72d1 | ||
| 
						 | 
					023451ce47 | ||
| 
						 | 
					9b53e39d8e | ||
| 
						 | 
					97fae2df95 | ||
| 
						 | 
					160d9d4900 | ||
| 
						 | 
					d4e6407464 | ||
| 
						 | 
					b7f7d8cd15 | ||
| 
						 | 
					2fa1db4345 | ||
| 
						 | 
					71b0945fc6 | ||
| 
						 | 
					5bca2e60a7 | ||
| 
						 | 
					67472e0e89 | ||
| 
						 | 
					e9aa5117c4 | ||
| 
						 | 
					2473bdba5e | ||
| 
						 | 
					2003d60159 | ||
| 
						 | 
					7d1c0047fa | ||
| 
						 | 
					7b61eba471 | ||
| 
						 | 
					7edaf6e7e8 | ||
| 
						 | 
					97ec8cfd4e | ||
| 
						 | 
					5b3a21b578 | ||
| 
						 | 
					ad0c19dde4 | ||
| 
						 | 
					69eb06c40e | ||
| 
						 | 
					1829fb61bd | ||
| 
						 | 
					ce67706037 | ||
| 
						 | 
					685a53534b | ||
| 
						 | 
					de4fc29773 | ||
| 
						 | 
					e04c7012c2 | ||
| 
						 | 
					d4a7216c82 | ||
| 
						 | 
					a4fdd03c3b | ||
| 
						 | 
					fc85f50a2b | ||
| 
						 | 
					86b907f82a | ||
| 
						 | 
					10d49bce70 | ||
| 
						 | 
					7ed367419e | ||
| 
						 | 
					50ee8b5f56 | ||
| 
						 | 
					03bdac0595 | ||
| 
						 | 
					f457d63400 | ||
| 
						 | 
					04210aa6dd | ||
| 
						 | 
					43f9d92008 | ||
| 
						 | 
					ed6c8bfe57 | ||
| 
						 | 
					39f2bc6bfc | ||
| 
						 | 
					b73b0940ef | ||
| 
						 | 
					6a07344786 | ||
| 
						 | 
					8b920f35a4 | ||
| 
						 | 
					4221e39867 | ||
| 
						 | 
					a091fadfda | ||
| 
						 | 
					77ccbf04dc | ||
| 
						 | 
					4addf6b587 | ||
| 
						 | 
					85c7f11170 | ||
| 
						 | 
					df3802a65f | ||
| 
						 | 
					b732beba6a | ||
| 
						 | 
					ce1fb4447e | ||
| 
						 | 
					558a54b098 | ||
| 
						 | 
					ed52833bb1 | ||
| 
						 | 
					6f133a0bdd | ||
| 
						 | 
					f561eecfb8 | ||
| 
						 | 
					ff7c9060ec | ||
| 
						 | 
					0ff42e84b0 | ||
| 
						 | 
					8a9f946ca7 | ||
| 
						 | 
					3b5210548e | ||
| 
						 | 
					b0c216584c | ||
| 
						 | 
					49a5483139 | ||
| 
						 | 
					6bc5c13758 | ||
| 
						 | 
					3e614260af | ||
| 
						 | 
					d87b4a488e | ||
| 
						 | 
					4c14855ad7 | ||
| 
						 | 
					dc77bbcfa4 | ||
| 
						 | 
					d8e2664c33 | ||
| 
						 | 
					eafc607abb | ||
| 
						 | 
					781fc2d576 | ||
| 
						 | 
					df993fa37b | ||
| 
						 | 
					5e9db9fb0b | ||
| 
						 | 
					0f3271db88 | ||
| 
						 | 
					6b252918fb | ||
| 
						 | 
					c4c84b7a0d | ||
| 
						 | 
					5c1912769e | ||
| 
						 | 
					71399aa682 | ||
| 
						 | 
					463a8aa273 | ||
| 
						 | 
					3579b4966a | ||
| 
						 | 
					5d66578356 | ||
| 
						 | 
					afa8d6e9d5 | ||
| 
						 | 
					1b44d873e7 | ||
| 
						 | 
					cef2c6054d | ||
| 
						 | 
					345420998e | ||
| 
						 | 
					0be8baad2b | ||
| 
						 | 
					1a83581a8e | ||
| 
						 | 
					37926eb991 | ||
| 
						 | 
					3d4634fdff | ||
| 
						 | 
					365431d406 | ||
| 
						 | 
					161e12cecf | ||
| 
						 | 
					46e6327e0f | ||
| 
						 | 
					68ee42f995 | ||
| 
						 | 
					f26aef9a8b | ||
| 
						 | 
					38d9036b59 | ||
| 
						 | 
					6f26e9322f | ||
| 
						 | 
					0e4d653687 | ||
| 
						 | 
					2c01610616 | ||
| 
						 | 
					f3d7a481b7 | ||
| 
						 | 
					f2a96c7d77 | ||
| 
						 | 
					e8a66680d1 | ||
| 
						 | 
					079b2c3b03 | ||
| 
						 | 
					750c1c55f7 | ||
| 
						 | 
					a622c47bd3 | ||
| 
						 | 
					ec4c35fe99 | ||
| 
						 | 
					a250c2cb13 | ||
| 
						 | 
					3d9de805b7 | ||
| 
						 | 
					15af558423 | ||
| 
						 | 
					f5e3939220 | ||
| 
						 | 
					ae27d9dcfd | ||
| 
						 | 
					37096790a7 | ||
| 
						 | 
					997c903884 | ||
| 
						 | 
					c8af3c2d96 | ||
| 
						 | 
					455e61170d | ||
| 
						 | 
					4de1370a9d | ||
| 
						 | 
					bbf8f102ee | ||
| 
						 | 
					ce3c93b08f | ||
| 
						 | 
					6c2129d5d0 | ||
| 
						 | 
					7c2a157ca4 | ||
| 
						 | 
					bb46bbcf5e | ||
| 
						 | 
					ac33aa7d37 | ||
| 
						 | 
					830fdd2715 | ||
| 
						 | 
					a6cd8f6169 | ||
| 
						 | 
					c78089263a | ||
| 
						 | 
					3e5ea035d5 | ||
| 
						 | 
					5d604eec5b | ||
| 
						 | 
					db0968f30c | ||
| 
						 | 
					e12fff8810 | ||
| 
						 | 
					9b60a038e5 | ||
| 
						 | 
					83a0cb8d88 | ||
| 
						 | 
					c0648233f2 | ||
| 
						 | 
					d835368eb8 | ||
| 
						 | 
					85d9d73a72 | ||
| 
						 | 
					78140a712c | ||
| 
						 | 
					1954ec5917 | ||
| 
						 | 
					0f1910129f | ||
| 
						 | 
					e2c3f6b3e2 | ||
| 
						 | 
					8570c1c0ef | ||
| 
						 | 
					55cd3ddcca | ||
| 
						 | 
					66fe77f084 | ||
| 
						 | 
					d1a5227cad | ||
| 
						 | 
					4f1afd575d | ||
| 
						 | 
					35b89b2eab | ||
| 
						 | 
					5784c05397 | ||
| 
						 | 
					f14aa5435d | ||
| 
						 | 
					f8fedbda20 | ||
| 
						 | 
					b3e5491e41 | ||
| 
						 | 
					cc269ba094 | ||
| 
						 | 
					a3c20e3f18 | ||
| 
						 | 
					80ee9b5e47 | ||
| 
						 | 
					5534f2cc6a | ||
| 
						 | 
					d321297d8a | ||
| 
						 | 
					06e5d74e34 | ||
| 
						 | 
					5d707e6fd5 | ||
| 
						 | 
					283948c83b | ||
| 
						 | 
					1475eab95f | ||
| 
						 | 
					20090f3172 | ||
| 
						 | 
					69a2d4ccff | ||
| 
						 | 
					e8b954c646 | ||
| 
						 | 
					c57317cbf0 | ||
| 
						 | 
					51b2fd299c | ||
| 
						 | 
					d0634b1596 | ||
| 
						 | 
					43606d6d6a | ||
| 
						 | 
					70b1010fa5 | ||
| 
						 | 
					84e5721f3a | ||
| 
						 | 
					319fb1ce03 | ||
| 
						 | 
					b255445557 | ||
| 
						 | 
					f02f83660c | ||
| 
						 | 
					b23424bb3c | ||
| 
						 | 
					5fd6988126 | ||
| 
						 | 
					5b82960df8 | ||
| 
						 | 
					cc9a252d8c | ||
| 
						 | 
					d281a6e603 | ||
| 
						 | 
					154f6f45d4 | ||
| 
						 | 
					0d41623b52 | ||
| 
						 | 
					c279f96371 | ||
| 
						 | 
					499e87c9ba | ||
| 
						 | 
					cd0853f2d5 | ||
| 
						 | 
					d290e87513 | ||
| 
						 | 
					97c20ede33 | ||
| 
						 | 
					5a83f79afd | ||
| 
						 | 
					987dbab0b0 | ||
| 
						 | 
					a8388beb94 | ||
| 
						 | 
					5afbb60fc4 | ||
| 
						 | 
					4cb5d7decc | ||
| 
						 | 
					8eac50dd4f | ||
| 
						 | 
					4a565cbf94 | ||
| 
						 | 
					64039df6d7 | ||
| 
						 | 
					7ac6d462ec | ||
| 
						 | 
					ef5136a745 | ||
| 
						 | 
					8288ec8824 | ||
| 
						 | 
					d02bbebb11 | ||
| 
						 | 
					224337b32f | ||
| 
						 | 
					9e35d9bbee | ||
| 
						 | 
					b9f5e16c80 | ||
| 
						 | 
					e9f7f36029 | ||
| 
						 | 
					057d31861e | ||
| 
						 | 
					f7ee012300 | ||
| 
						 | 
					1ed0aa8fea | ||
| 
						 | 
					ef98803d63 | ||
| 
						 | 
					02fea420e5 | ||
| 
						 | 
					22c5451fc2 | ||
| 
						 | 
					ebc529cbb3 | ||
| 
						 | 
					23ebbaa46e | ||
| 
						 | 
					9ac0a7a50b | ||
| 
						 | 
					e5c65a85df | ||
| 
						 | 
					33627331a3 | ||
| 
						 | 
					36c87c433b | ||
| 
						 | 
					179737feb7 | ||
| 
						 | 
					47353f5ee4 | ||
| 
						 | 
					10e768826c | ||
| 
						 | 
					5056bb9c01 | ||
| 
						 | 
					c4cf8ad559 | ||
| 
						 | 
					57ec6901eb | ||
| 
						 | 
					e64f9ebb44 | ||
| 
						 | 
					791650ddef | ||
| 
						 | 
					efbf41ed81 | ||
| 
						 | 
					cf15589851 | ||
| 
						 | 
					19753c18c0 | ||
| 
						 | 
					41be28096a | ||
| 
						 | 
					37a570f962 | ||
| 
						 | 
					5a739ff4cb | ||
| 
						 | 
					4e262eb2a8 | ||
| 
						 | 
					4cfcbc328f | ||
| 
						 | 
					79292ff3e0 | ||
| 
						 | 
					8ea500441d | ||
| 
						 | 
					b50c818623 | ||
| 
						 | 
					b99e750b62 | ||
| 
						 | 
					1f50356e8e | ||
| 
						 | 
					22c81f62ec | ||
| 
						 | 
					73e2c8f68f | ||
| 
						 | 
					f4408219e9 | ||
| 
						 | 
					2d1e3c3229 | ||
| 
						 | 
					4918fae535 | ||
| 
						 | 
					0aff67877e | ||
| 
						 | 
					f6f759fc5f | ||
| 
						 | 
					9544a57ee4 | ||
| 
						 | 
					b51e3b63ac | ||
| 
						 | 
					6bbbc50f10 | ||
| 
						 | 
					9bbddc37a7 | ||
| 
						 | 
					e4ff73297d | ||
| 
						 | 
					b44320db13 | ||
| 
						 | 
					0bacb30007 | ||
| 
						 | 
					53da2c6965 | ||
| 
						 | 
					d8def1ff94 | ||
| 
						 | 
					571dc61955 | ||
| 
						 | 
					0e09c380fc | ||
| 
						 | 
					0ee87615c7 | ||
| 
						 | 
					f8241bfba3 | ||
| 
						 | 
					4607c70641 | ||
| 
						 | 
					c12f1c5b99 | ||
| 
						 | 
					a08f20d910 | ||
| 
						 | 
					6cea036027 | ||
| 
						 | 
					5796bfc401 | ||
| 
						 | 
					f1a379aa56 | ||
| 
						 | 
					9ae146993e | ||
| 
						 | 
					e0348d3fe8 | ||
| 
						 | 
					2cc854f8cb | ||
| 
						 | 
					5304b765b2 | ||
| 
						 | 
					fb6cbc02fb | ||
| 
						 | 
					4fd5f3526a | ||
| 
						 | 
					842f85f758 | ||
| 
						 | 
					9d30f9f8b3 | ||
| 
						 | 
					631cfd9e62 | ||
| 
						 | 
					326363b3a7 | ||
| 
						 | 
					ac7a842e55 | ||
| 
						 | 
					2c3fe1fd97 | ||
| 
						 | 
					269ed6e6a2 | ||
| 
						 | 
					78fb33dd07 | ||
| 
						 | 
					8f8e736b13 | ||
| 
						 | 
					d89454de80 | ||
| 
						 | 
					af28b94533 | ||
| 
						 | 
					e9188e971a | ||
| 
						 | 
					78eddfc068 | ||
| 
						 | 
					02c24d3d01 | ||
| 
						 | 
					52abc8acb7 | ||
| 
						 | 
					4d71c559b2 | ||
| 
						 | 
					0d16eb310e | ||
| 
						 | 
					8072e205ff | ||
| 
						 | 
					955f2a4e03 | ||
| 
						 | 
					3c75113e37 | ||
| 
						 | 
					ccd7785859 | ||
| 
						 | 
					3b5a4a77f3 | ||
| 
						 | 
					daed0634a9 | ||
| 
						 | 
					0d4dd707bc | ||
| 
						 | 
					0e982bc1f4 | ||
| 
						 | 
					6298f49816 | ||
| 
						 | 
					ef757da2c9 | ||
| 
						 | 
					e5352297d9 | ||
| 
						 | 
					65a5040e09 | ||
| 
						 | 
					d626b99b54 | ||
| 
						 | 
					dddb58a38b | ||
| 
						 | 
					400056e154 | ||
| 
						 | 
					d2f19024d0 | ||
| 
						 | 
					69c04eecc4 | ||
| 
						 | 
					996bb1b85e | ||
| 
						 | 
					422dcc3856 | ||
| 
						 | 
					020bd60ab2 | ||
| 
						 | 
					8e277b72bb | ||
| 
						 | 
					4f67b39d26 | ||
| 
						 | 
					2425281317 | ||
| 
						 | 
					0403e9860e | ||
| 
						 | 
					33a65e3ba3 | ||
| 
						 | 
					88bcd79bb9 | ||
| 
						 | 
					7e571f95f0 | ||
| 
						 | 
					da8e2a0447 | ||
| 
						 | 
					a30915bde1 | ||
| 
						 | 
					58e3fff311 | ||
| 
						 | 
					3f0b309ad4 | ||
| 
						 | 
					e70610ef06 | ||
| 
						 | 
					dfded7e075 | ||
| 
						 | 
					173b550438 | ||
| 
						 | 
					cff3f44f4a | ||
| 
						 | 
					26e4e66faf | ||
| 
						 | 
					97c9e11768 | ||
| 
						 | 
					3518aaef33 | ||
| 
						 | 
					1963c00201 | ||
| 
						 | 
					27402cb7a2 | ||
| 
						 | 
					c1218199cf | ||
| 
						 | 
					717f7229eb | ||
| 
						 | 
					aae56abb7c | ||
| 
						 | 
					5f034f5b63 | ||
| 
						 | 
					b910fa9010 | ||
| 
						 | 
					6d4219083c | ||
| 
						 | 
					1ed4f521c4 | ||
| 
						 | 
					de2163dafd | ||
| 
						 | 
					9bd00041fa | ||
| 
						 | 
					4e986a823c | ||
| 
						 | 
					2cc7d05012 | ||
| 
						 | 
					123a722a6f | ||
| 
						 | 
					4d311eb731 | ||
| 
						 | 
					cb42e607c5 | ||
| 
						 | 
					2aa91a937b | ||
| 
						 | 
					ccef9431c8 | ||
| 
						 | 
					642cee1342 | ||
| 
						 | 
					9a9e7d83c4 | ||
| 
						 | 
					9929751cc8 | ||
| 
						 | 
					17b7186cd7 | ||
| 
						 | 
					189a43caa2 | ||
| 
						 | 
					e835ef1836 | ||
| 
						 | 
					7e7749224c | ||
| 
						 | 
					c7c2f3bc22 | ||
| 
						 | 
					54a79d6a8a | ||
| 
						 | 
					5bf5aeec01 | ||
| 
						 | 
					e01e535cbb | ||
| 
						 | 
					0195d6a2f8 | ||
| 
						 | 
					8e0641a9bf | ||
| 
						 | 
					662568d453 | ||
| 
						 | 
					4ebb66c662 | ||
| 
						 | 
					23e899f32d | ||
| 
						 | 
					fedf71635e | ||
| 
						 | 
					97c59be653 | ||
| 
						 | 
					9d8a4988e8 | ||
| 
						 | 
					1ae0750a21 | ||
| 
						 | 
					9d91e5e587 | ||
| 
						 | 
					96624aa412 | ||
| 
						 | 
					10f33b8537 | ||
| 
						 | 
					4a633cc295 | ||
| 
						 | 
					d34d88e417 | ||
| 
						 | 
					52ce350b7a | ||
| 
						 | 
					2abebb2cbe | ||
| 
						 | 
					380e06e5be | ||
| 
						 | 
					badf975e45 | ||
| 
						 | 
					755b4e4fc2 | ||
| 
						 | 
					1a1c99e334 | ||
| 
						 | 
					21adf8b6d2 | ||
| 
						 | 
					784bf88b0d | ||
| 
						 | 
					e873841cbb | ||
| 
						 | 
					26d0bf9236 | ||
| 
						 | 
					359b15a597 | ||
| 
						 | 
					b55958a587 | ||
| 
						 | 
					7784ca33ce | ||
| 
						 | 
					c9c8c98bf6 | ||
| 
						 | 
					171796791f | ||
| 
						 | 
					176d0f7075 | ||
| 
						 | 
					8ed51cac37 | ||
| 
						 | 
					c9e6f0542d | ||
| 
						 | 
					b0930626c5 | ||
| 
						 | 
					e890be4814 | ||
| 
						 | 
					b2799f111b | ||
| 
						 | 
					152fc202f5 | ||
| 
						 | 
					4ad0d4d6d3 | ||
| 
						 | 
					163cd3e77c | ||
| 
						 | 
					4c2c8f93dd | ||
| 
						 | 
					fd1e6e0590 | ||
| 
						 | 
					89c79bec8c | ||
| 
						 | 
					c7b77004e3 | ||
| 
						 | 
					07d143f412 | ||
| 
						 | 
					a12283e2ff | ||
| 
						 | 
					4b0050cf0e | ||
| 
						 | 
					0577af98f4 | ||
| 
						 | 
					17ce203a26 | ||
| 
						 | 
					d76555ffb5 | ||
| 
						 | 
					2786dff5d3 | ||
| 
						 | 
					225f0d1219 | ||
| 
						 | 
					532db58311 | ||
| 
						 | 
					6be309e1bd | ||
| 
						 | 
					da3bf23354 | ||
| 
						 | 
					26ab67732b | ||
| 
						 | 
					45cacbaf05 | ||
| 
						 | 
					17df6520c8 | ||
| 
						 | 
					6f351bf586 | ||
| 
						 | 
					ff4f0cbd1d | ||
| 
						 | 
					fc37c192ae | ||
| 
						 | 
					434dfe30c5 | ||
| 
						 | 
					4e2b7e181d | ||
| 
						 | 
					48702dd149 | ||
| 
						 | 
					68dfc6236a | ||
| 
						 | 
					5e8ff556cb | ||
| 
						 | 
					6fd04ca922 | ||
| 
						 | 
					206797bda4 | ||
| 
						 | 
					43ed358f9a | ||
| 
						 | 
					b32ebb4f29 | ||
| 
						 | 
					fb9cdfa723 | ||
| 
						 | 
					efac488675 | ||
| 
						 | 
					6b800aa7b7 | ||
| 
						 | 
					dd7c9ebeaf | ||
| 
						 | 
					4dc7fb9525 | ||
| 
						 | 
					c39761c552 | ||
| 
						 | 
					aac367636d | ||
| 
						 | 
					15a687ae4b | ||
| 
						 | 
					d528e1af75 | ||
| 
						 | 
					cd234ce22c | ||
| 
						 | 
					94618b2365 | ||
| 
						 | 
					1fd236d177 | ||
| 
						 | 
					e87fc7200d | ||
| 
						 | 
					20b9f8e6f4 | ||
| 
						 | 
					c69bc19e46 | ||
| 
						 | 
					bba5d177aa | ||
| 
						 | 
					c16f8af911 | ||
| 
						 | 
					217f60c3d9 | ||
| 
						 | 
					7bdcd1da94 | ||
| 
						 | 
					ead259d877 | ||
| 
						 | 
					2ff45d571d | ||
| 
						 | 
					157f09acdf | ||
| 
						 | 
					0f3cf1d42e | ||
| 
						 | 
					5bc029c529 | ||
| 
						 | 
					e9a9c6a8e8 | ||
| 
						 | 
					515f497e6d | ||
| 
						 | 
					b27268aaef | ||
| 
						 | 
					f5f245cc15 | ||
| 
						 | 
					94d37fdcae | ||
| 
						 | 
					b84aea1685 | ||
| 
						 | 
					896495de7b | ||
| 
						 | 
					5528dd9d11 | ||
| 
						 | 
					943172cbf4 | ||
| 
						 | 
					85169e8d6f | ||
| 
						 | 
					34f142797a | ||
| 
						 | 
					46a7f1e74a | ||
| 
						 | 
					620d5c569e | ||
| 
						 | 
					b9ce7bf75e | ||
| 
						 | 
					cddc63381c | ||
| 
						 | 
					385a32ecb5 | ||
| 
						 | 
					030e765e76 | ||
| 
						 | 
					ab8c929e20 | ||
| 
						 | 
					ce0dc33cb8 | ||
| 
						 | 
					78f81fc0e5 | ||
| 
						 | 
					9b6c2e6eb6 | ||
| 
						 | 
					1a29e9a879 | ||
| 
						 | 
					4bf1da4944 | ||
| 
						 | 
					de5beb06b3 | ||
| 
						 | 
					98e65929dc | ||
| 
						 | 
					66ab48772f | ||
| 
						 | 
					22fcf8f7de | ||
| 
						 | 
					28c7813ac4 | ||
| 
						 | 
					1d8616d30f | ||
| 
						 | 
					d61ef8b954 | ||
| 
						 | 
					89d9900152 | ||
| 
						 | 
					4a048715b6 | ||
| 
						 | 
					6297f85606 | ||
| 
						 | 
					ed56428dd7 | ||
| 
						 | 
					ad40b92b6a | ||
| 
						 | 
					8ce4032e72 | ||
| 
						 | 
					42660466f8 | ||
| 
						 | 
					e919f6811f | ||
| 
						 | 
					bf7edb0d5d | ||
| 
						 | 
					f38353d6b9 | ||
| 
						 | 
					201d853fdf | ||
| 
						 | 
					e40145a39d | ||
| 
						 | 
					c895a7d13f | ||
| 
						 | 
					dad7a987ae | ||
| 
						 | 
					8ffb51749f | ||
| 
						 | 
					55f6eba049 | ||
| 
						 | 
					04f3c12bb7 | ||
| 
						 | 
					60323e0805 | ||
| 
						 | 
					d4a86102fd | ||
| 
						 | 
					476fb8e892 | ||
| 
						 | 
					829ff87bd1 | ||
| 
						 | 
					f6b622c4b3 | ||
| 
						 | 
					2e4da8eec2 | ||
| 
						 | 
					763bb65dbb | ||
| 
						 | 
					7ca9605f54 | ||
| 
						 | 
					eb2c443a79 | ||
| 
						 | 
					278e25ea44 | ||
| 
						 | 
					a50a87a7b8 | ||
| 
						 | 
					98085015d5 | ||
| 
						 | 
					bf54c845e9 | ||
| 
						 | 
					c365f195a8 | ||
| 
						 | 
					e91d0ef737 | ||
| 
						 | 
					22f5c12ced | ||
| 
						 | 
					298c996e54 | ||
| 
						 | 
					0fc0cfc6d2 | ||
| 
						 | 
					914f68f021 | ||
| 
						 | 
					bd1d119ba9 | ||
| 
						 | 
					a03be18189 | ||
| 
						 | 
					96bc232b43 | ||
| 
						 | 
					bca7b12284 | ||
| 
						 | 
					32cb1960c1 | ||
| 
						 | 
					de781b37c8 | ||
| 
						 | 
					3e21799377 | ||
| 
						 | 
					26a00a0410 | ||
| 
						 | 
					646371f56d | ||
| 
						 | 
					1f5008544b | ||
| 
						 | 
					45cbfc5aee | ||
| 
						 | 
					6d423b383b | ||
| 
						 | 
					ad897080a2 | ||
| 
						 | 
					b7d316d98d | ||
| 
						 | 
					d7339fad52 | ||
| 
						 | 
					92c81e8117 | ||
| 
						 | 
					9db0996ed4 | ||
| 
						 | 
					6f43898b17 | ||
| 
						 | 
					7487229c34 | ||
| 
						 | 
					8a8e7afa96 | ||
| 
						 | 
					c79f8c9c39 | ||
| 
						 | 
					485016bfbb | ||
| 
						 | 
					0165ba1651 | ||
| 
						 | 
					c4209d6d21 | ||
| 
						 | 
					6adca97f37 | ||
| 
						 | 
					9a3c8003c8 | ||
| 
						 | 
					d51f15257c | ||
| 
						 | 
					8f440d579a | ||
| 
						 | 
					4cc3be3035 | ||
| 
						 | 
					db2ffa79f1 | ||
| 
						 | 
					afd2b058b4 | ||
| 
						 | 
					fd5971be0b | ||
| 
						 | 
					89bf98bcf2 | ||
| 
						 | 
					1b2d156094 | ||
| 
						 | 
					714adb8bd1 | ||
| 
						 | 
					95b1133d0c | ||
| 
						 | 
					b37b496a12 | ||
| 
						 | 
					d6f692ad1a | ||
| 
						 | 
					f77713bf1f | ||
| 
						 | 
					38255d2af1 | ||
| 
						 | 
					73630a7e85 | ||
| 
						 | 
					955c317cab | ||
| 
						 | 
					9f18b88a06 | ||
| 
						 | 
					353f83a9c7 | ||
| 
						 | 
					3bade04e10 | ||
| 
						 | 
					a6d0f443eb | ||
| 
						 | 
					96236b7968 | ||
| 
						 | 
					4434d7f447 | ||
| 
						 | 
					171eb040fc | ||
| 
						 | 
					3591bbe56f | ||
| 
						 | 
					34d5ef29b3 | ||
| 
						 | 
					bbbd9f20f3 | ||
| 
						 | 
					547132e820 | ||
| 
						 | 
					2d315ba9a9 | ||
| 
						 | 
					d355d2020f | ||
| 
						 | 
					c8cf0d94ed | ||
| 
						 | 
					4730762e5c | ||
| 
						 | 
					d88582dffd | ||
| 
						 | 
					2f81b3dce2 | ||
| 
						 | 
					5cab13739e | ||
| 
						 | 
					8aadad9c72 | ||
| 
						 | 
					807d092761 | ||
| 
						 | 
					f36f1d6be9 | ||
| 
						 | 
					8800c8a59b | ||
| 
						 | 
					b4dce13309 | ||
| 
						 | 
					e15307fdf4 | ||
| 
						 | 
					3520c0e4d5 | ||
| 
						 | 
					ccdf0b2a44 | ||
| 
						 | 
					63a453554d | ||
| 
						 | 
					105186aa17 | ||
| 
						 | 
					ba04afc9a4 | ||
| 
						 | 
					7e1e0086e7 | ||
| 
						 | 
					02b31c9dc8 | ||
| 
						 | 
					7f2fbad736 | ||
| 
						 | 
					5bece94509 | ||
| 
						 | 
					3d90156e99 | ||
| 
						 | 
					5e46c5c435 | ||
| 
						 | 
					583c1f472c | ||
| 
						 | 
					26bfc1c443 | ||
| 
						 | 
					799aa9883c | ||
| 
						 | 
					84ed77cbd8 | ||
| 
						 | 
					c9e584fb90 | ||
| 
						 | 
					17b1e81ca1 | ||
| 
						 | 
					7e9a2da097 | ||
| 
						 | 
					c48c1d7c46 | ||
| 
						 | 
					d1692fd3e0 | ||
| 
						 | 
					5fa36a0833 | ||
| 
						 | 
					853ae490e1 | ||
| 
						 | 
					f2cf97d6f1 | ||
| 
						 | 
					c344da4c5a | ||
| 
						 | 
					85a57006d1 | ||
| 
						 | 
					c5e892cb3e | ||
| 
						 | 
					81fb06f530 | ||
| 
						 | 
					a385382ff5 | ||
| 
						 | 
					b8772a353f | ||
| 
						 | 
					c2714fcbfd | ||
| 
						 | 
					a2fc933fed | ||
| 
						 | 
					0e331c7168 | ||
| 
						 | 
					ac145f75ca | ||
| 
						 | 
					a4b8d1f89a | ||
| 
						 | 
					798b107f19 | ||
| 
						 | 
					6a1b471365 | ||
| 
						 | 
					ec231a7923 | ||
| 
						 | 
					7ca71a6b0f | ||
| 
						 | 
					7607e6e902 | ||
| 
						 | 
					f1548ef62d | ||
| 
						 | 
					6845988807 | ||
| 
						 | 
					9eed4a90ce | ||
| 
						 | 
					f8464785a6 | ||
| 
						 | 
					1d359e737e | ||
| 
						 | 
					50b9056e09 | ||
| 
						 | 
					91a090a485 | ||
| 
						 | 
					9c76b30d72 | ||
| 
						 | 
					93f19910c5 | ||
| 
						 | 
					4ec7445a6f | ||
| 
						 | 
					0372c51f82 | ||
| 
						 | 
					0fec3525ad | ||
| 
						 | 
					41ba3017fd | ||
| 
						 | 
					8080fbce35 | ||
| 
						 | 
					ec14f6ceda | ||
| 
						 | 
					c60a086635 | ||
| 
						 | 
					92ca2cca95 | ||
| 
						 | 
					1e1634daca | ||
| 
						 | 
					824ee5446f | ||
| 
						 | 
					879e2caf8c | ||
| 
						 | 
					c4014e73a2 | ||
| 
						 | 
					be9efdb981 | ||
| 
						 | 
					074dc3b9d8 | ||
| 
						 | 
					86f9b582d5 | ||
| 
						 | 
					4142c3ef7c | ||
| 
						 | 
					6602e793c0 | ||
| 
						 | 
					ea0fdaed28 | ||
| 
						 | 
					1eb382da5a | ||
| 
						 | 
					bb6fd02298 | ||
| 
						 | 
					7e2bceceee | ||
| 
						 | 
					30a7d7096c | ||
| 
						 | 
					200a18820e | ||
| 
						 | 
					e03637176d | ||
| 
						 | 
					c02db93243 | ||
| 
						 | 
					ffa4d5134a | ||
| 
						 | 
					302d7fdbf3 | ||
| 
						 | 
					cf442cd57e | ||
| 
						 | 
					0e1ba65855 | ||
| 
						 | 
					6aad333c63 | ||
| 
						 | 
					4fcc84e67a | ||
| 
						 | 
					3ae2f441e0 | ||
| 
						 | 
					2abb3f6424 | ||
| 
						 | 
					ce3b212d12 | ||
| 
						 | 
					83d6d46e29 | ||
| 
						 | 
					354ad9254e | ||
| 
						 | 
					58876091f7 | ||
| 
						 | 
					dc18eee39d | ||
| 
						 | 
					8727a9c140 | ||
| 
						 | 
					d0425f26cf | ||
| 
						 | 
					cfa84b8470 | ||
| 
						 | 
					1580ed4c06 | ||
| 
						 | 
					a7ee84fc31 | ||
| 
						 | 
					84ac7ce139 | ||
| 
						 | 
					788b092c49 | ||
| 
						 | 
					5cde17a096 | ||
| 
						 | 
					c3837eb08c | ||
| 
						 | 
					8cc0ee2efe | ||
| 
						 | 
					d5eec16d23 | ||
| 
						 | 
					daa1a032f7 | ||
| 
						 | 
					6042e8bc57 | ||
| 
						 | 
					920a4b0794 | ||
| 
						 | 
					ee49844d09 | ||
| 
						 | 
					8a516ac862 | ||
| 
						 | 
					bee2f4a3b0 | ||
| 
						 | 
					cef45feaa4 | ||
| 
						 | 
					2687f02c96 | ||
| 
						 | 
					b25976aeb8 | ||
| 
						 | 
					001f167aad | ||
| 
						 | 
					486a2c1d94 | ||
| 
						 | 
					88cf154483 | ||
| 
						 | 
					8cbd3e7510 | ||
| 
						 | 
					eeb695261f | ||
| 
						 | 
					dc9b1111e0 | ||
| 
						 | 
					06ac829e70 | ||
| 
						 | 
					72700279e2 | ||
| 
						 | 
					5d3f7fff26 | ||
| 
						 | 
					d77c1c5f9d | ||
| 
						 | 
					2a5302a1cf | ||
| 
						 | 
					ffbd3d173f | ||
| 
						 | 
					1e0a669f75 | ||
| 
						 | 
					527e9be058 | ||
| 
						 | 
					34bea2e272 | ||
| 
						 | 
					fe44ae3371 | ||
| 
						 | 
					adeb40eaf2 | ||
| 
						 | 
					d7d33e5255 | ||
| 
						 | 
					63bc884e25 | ||
| 
						 | 
					ef4e095d24 | ||
| 
						 | 
					4d4f75a8a8 | ||
| 
						 | 
					3f71ba406a | ||
| 
						 | 
					88a67127d8 | ||
| 
						 | 
					f7dc7dcc64 | ||
| 
						 | 
					04f971c84b | ||
| 
						 | 
					548a7df014 | ||
| 
						 | 
					70edb9bc4d | ||
| 
						 | 
					3f0ed03856 | ||
| 
						 | 
					4736391bfb | ||
| 
						 | 
					7c5330413b | ||
| 
						 | 
					39d9d22ca3 | ||
| 
						 | 
					af47413dba | ||
| 
						 | 
					b2f00aa977 | ||
| 
						 | 
					6694be5e50 | ||
| 
						 | 
					f5e8b207fb | ||
| 
						 | 
					d245460362 | ||
| 
						 | 
					4d0d0fa383 | ||
| 
						 | 
					7ffe45734d | ||
| 
						 | 
					01811c176a | ||
| 
						 | 
					a7248f6ea8 | ||
| 
						 | 
					9685c34509 | ||
| 
						 | 
					d091fe3c21 | ||
| 
						 | 
					ee02f548c8 | ||
| 
						 | 
					b08870aff3 | ||
| 
						 | 
					3ecae420ac | ||
| 
						 | 
					4cbbf0e13b | ||
| 
						 | 
					380378cc80 | ||
| 
						 | 
					0963c65027 | ||
| 
						 | 
					ed740a2504 | ||
| 
						 | 
					c9f98622b1 | ||
| 
						 | 
					0a954e5066 | ||
| 
						 | 
					aa93423fbf | ||
| 
						 | 
					01c9386267 | ||
| 
						 | 
					af9eb36f9f | ||
| 
						 | 
					06093fd396 | ||
| 
						 | 
					86b7fcac32 | ||
| 
						 | 
					fb8ddc564e | ||
| 
						 | 
					242efe6611 | ||
| 
						 | 
					1b0e6c9c0e | ||
| 
						 | 
					dfa2f32ca0 | ||
| 
						 | 
					840424a2c4 | ||
| 
						 | 
					f56aa20014 | ||
| 
						 | 
					6707768ebd | ||
| 
						 | 
					c78bb76a12 | ||
| 
						 | 
					942c979232 | ||
| 
						 | 
					06164911dd | ||
| 
						 | 
					2a21363bb7 | ||
| 
						 | 
					026869915f | ||
| 
						 | 
					45d61aaaa3 | ||
| 
						 | 
					20f6c06569 | ||
| 
						 | 
					371f5e52aa | ||
| 
						 | 
					e006480e49 | ||
| 
						 | 
					aed545872d | ||
| 
						 | 
					44869c59d6 | ||
| 
						 | 
					52663284cf | ||
| 
						 | 
					42fa9d7f0a | ||
| 
						 | 
					b7a87a22b6 | ||
| 
						 | 
					e8aaea030e | ||
| 
						 | 
					b1ad3a43cb | ||
| 
						 | 
					267e25a750 | ||
| 
						 | 
					9a32c514cb | ||
| 
						 | 
					e9ae607ece | ||
| 
						 | 
					93707fa3f2 | ||
| 
						 | 
					94c369095f | ||
| 
						 | 
					9164b0161b | ||
| 
						 | 
					e592e8fccb | ||
| 
						 | 
					bf4fc25f7b | ||
| 
						 | 
					5b806d8d24 | ||
| 
						 | 
					cb1e072643 | ||
| 
						 | 
					45b6a12e45 | ||
| 
						 | 
					68755f1f5e | ||
| 
						 | 
					997a455039 | ||
| 
						 | 
					88775e1ff9 | ||
| 
						 | 
					8867e744ff | ||
| 
						 | 
					4fd064bea6 | ||
| 
						 | 
					59fbceedcc | ||
| 
						 | 
					321d57e1a0 | ||
| 
						 | 
					ba26c7aa00 | ||
| 
						 | 
					63c763685f | ||
| 
						 | 
					34a4a94f13 | ||
| 
						 | 
					f4a73d57a4 | ||
| 
						 | 
					948114e3e3 | ||
| 
						 | 
					a3e60d9058 | ||
| 
						 | 
					8acb233668 | ||
| 
						 | 
					119589fcb3 | ||
| 
						 | 
					5ea844964e | ||
| 
						 | 
					bd8eed57fc | ||
| 
						 | 
					9cf0f2e973 | ||
| 
						 | 
					176ad3aa6e | ||
| 
						 | 
					4d08363580 | ||
| 
						 | 
					8907bf51d2 | ||
| 
						 | 
					abe614c705 | ||
| 
						 | 
					238715037d | ||
| 
						 | 
					c0a00f68ae | ||
| 
						 | 
					f0c454ab57 | ||
| 
						 | 
					089daaeabc | ||
| 
						 | 
					b9f74ff3d6 | ||
| 
						 | 
					fcf4d60eee | ||
| 
						 | 
					e33d5c2dbc | ||
| 
						 | 
					18d9a7e1f1 | ||
| 
						 | 
					8488388cbd | ||
| 
						 | 
					588901f449 | ||
| 
						 | 
					0a7fdbe533 | ||
| 
						 | 
					5950c176ca | ||
| 
						 | 
					23d23409a0 | ||
| 
						 | 
					9009bedf13 | ||
| 
						 | 
					d4ac57e240 | ||
| 
						 | 
					7b59d1770f | ||
| 
						 | 
					95ead8ffba | ||
| 
						 | 
					7aa08a77ca | ||
| 
						 | 
					7e432cdfac | ||
| 
						 | 
					586672f490 | ||
| 
						 | 
					b03408de74 | ||
| 
						 | 
					1e6a28bf5b | ||
| 
						 | 
					d6e3b64582 | ||
| 
						 | 
					114c932a8e | ||
| 
						 | 
					7f7103de06 | ||
| 
						 | 
					c631a9c726 | ||
| 
						 | 
					8fd9e56804 | ||
| 
						 | 
					8a65717f55 | ||
| 
						 | 
					6d3152a98a | ||
| 
						 | 
					b438d485f1 | ||
| 
						 | 
					204349b17b | ||
| 
						 | 
					86e67fc4a9 | ||
| 
						 | 
					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 | ||
| 
						 | 
					c496967e56 | ||
| 
						 | 
					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 | ||
| 
						 | 
					c942e4a07b | ||
| 
						 | 
					bd54b08261 | ||
| 
						 | 
					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 | ||
| 
						 | 
					b99c291f47 | 
@@ -1,8 +1,11 @@
 | 
			
		||||
.vscode
 | 
			
		||||
ollama
 | 
			
		||||
app
 | 
			
		||||
macapp
 | 
			
		||||
dist
 | 
			
		||||
llm/llama.cpp
 | 
			
		||||
build
 | 
			
		||||
.env
 | 
			
		||||
.cache
 | 
			
		||||
test_data
 | 
			
		||||
.git
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										24
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
llama/**/*.cpp linguist-vendored
 | 
			
		||||
llama/**/*.hpp linguist-vendored
 | 
			
		||||
llama/**/*.h linguist-vendored
 | 
			
		||||
llama/**/*.c linguist-vendored
 | 
			
		||||
llama/**/*.cu linguist-vendored
 | 
			
		||||
llama/**/*.cuh linguist-vendored
 | 
			
		||||
llama/**/*.m linguist-vendored
 | 
			
		||||
llama/**/*.metal linguist-vendored
 | 
			
		||||
 | 
			
		||||
ml/backend/**/*.c linguist-vendored
 | 
			
		||||
ml/backend/**/*.h linguist-vendored
 | 
			
		||||
ml/backend/**/*.cpp linguist-vendored
 | 
			
		||||
ml/backend/**/*.hpp linguist-vendored
 | 
			
		||||
ml/backend/**/*.cu linguist-vendored
 | 
			
		||||
ml/backend/**/*.cuh linguist-vendored
 | 
			
		||||
ml/backend/**/*.m linguist-vendored
 | 
			
		||||
ml/backend/**/*.metal linguist-vendored
 | 
			
		||||
ml/backend/**/CMakeLists.txt linguist-vendored
 | 
			
		||||
 | 
			
		||||
llama/build-info.cpp linguist-generated
 | 
			
		||||
ml/backend/ggml/ggml/src/ggml-metal/ggml-metal-embed.s linguist-generated
 | 
			
		||||
 | 
			
		||||
* text=auto
 | 
			
		||||
*.go text eol=lf
 | 
			
		||||
							
								
								
									
										68
									
								
								.github/ISSUE_TEMPLATE/10_bug_report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								.github/ISSUE_TEMPLATE/10_bug_report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
			
		||||
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: textarea
 | 
			
		||||
    id: logs
 | 
			
		||||
    attributes:
 | 
			
		||||
      label: Relevant log output
 | 
			
		||||
      description: Please copy and paste any relevant log output. See [Troubleshooting Guide](https://github.com/ollama/ollama/blob/main/docs/troubleshooting.md#how-to-troubleshoot-issues) for details.
 | 
			
		||||
      render: shell
 | 
			
		||||
    validations:
 | 
			
		||||
      required: false
 | 
			
		||||
  - 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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
 | 
			
		||||
							
								
								
									
										495
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										495
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,495 @@
 | 
			
		||||
name: release
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    tags:
 | 
			
		||||
      - 'v*'
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  CGO_CFLAGS: '-O3'
 | 
			
		||||
  CGO_CXXFLAGS: '-O3'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  setup-environment:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    environment: release
 | 
			
		||||
    outputs:
 | 
			
		||||
      GOFLAGS: ${{ steps.goflags.outputs.GOFLAGS }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - name: Set environment
 | 
			
		||||
        id: goflags
 | 
			
		||||
        run: |
 | 
			
		||||
          echo GOFLAGS="'-ldflags=-w -s \"-X=github.com/ollama/ollama/version.Version=${GITHUB_REF_NAME#v}\" \"-X=github.com/ollama/ollama/server.mode=release\"'" >>$GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
  darwin-build:
 | 
			
		||||
    runs-on: macos-13
 | 
			
		||||
    environment: release
 | 
			
		||||
    needs: setup-environment
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        os: [darwin]
 | 
			
		||||
        arch: [amd64, arm64]
 | 
			
		||||
    env:
 | 
			
		||||
      GOFLAGS: ${{ needs.setup-environment.outputs.GOFLAGS }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
      - run: |
 | 
			
		||||
          go build -o dist/ .
 | 
			
		||||
        env:
 | 
			
		||||
          GOOS: ${{ matrix.os }}
 | 
			
		||||
          GOARCH: ${{ matrix.arch }}
 | 
			
		||||
          CGO_ENABLED: 1
 | 
			
		||||
          CGO_CPPFLAGS: '-mmacosx-version-min=11.3'
 | 
			
		||||
      - if: matrix.arch == 'amd64'
 | 
			
		||||
        run: |
 | 
			
		||||
          cmake --preset CPU -DCMAKE_OSX_DEPLOYMENT_TARGET=11.3 -DCMAKE_SYSTEM_PROCESSOR=x86_64 -DCMAKE_OSX_ARCHITECTURES=x86_64
 | 
			
		||||
          cmake --build --parallel --preset CPU
 | 
			
		||||
          cmake --install build --component CPU --strip --parallel 8
 | 
			
		||||
      - uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: build-${{ matrix.os }}-${{ matrix.arch }}
 | 
			
		||||
          path: dist/*
 | 
			
		||||
 | 
			
		||||
  darwin-sign:
 | 
			
		||||
    runs-on: macos-13
 | 
			
		||||
    environment: release
 | 
			
		||||
    needs: darwin-build
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - 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
 | 
			
		||||
          security set-keychain-settings -lut 3600 build.keychain
 | 
			
		||||
        env:
 | 
			
		||||
          MACOS_SIGNING_KEY: ${{ secrets.MACOS_SIGNING_KEY }}
 | 
			
		||||
          MACOS_SIGNING_KEY_PASSWORD: ${{ secrets.MACOS_SIGNING_KEY_PASSWORD }}
 | 
			
		||||
      - uses: actions/download-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: build-darwin-amd64
 | 
			
		||||
          path: dist/darwin-amd64
 | 
			
		||||
      - uses: actions/download-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: build-darwin-arm64
 | 
			
		||||
          path: dist/darwin-arm64
 | 
			
		||||
      - run: |
 | 
			
		||||
          export VERSION=${GITHUB_REF_NAME#v}
 | 
			
		||||
          ./scripts/build_darwin.sh sign macapp
 | 
			
		||||
        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_14.1.0.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
 | 
			
		||||
          DEVELOPER_DIR: /Applications/Xcode_14.1.0.app/Contents/Developer
 | 
			
		||||
      - uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: dist-darwin
 | 
			
		||||
          path: |
 | 
			
		||||
            dist/Ollama-darwin.zip
 | 
			
		||||
            dist/ollama-darwin.tgz
 | 
			
		||||
 | 
			
		||||
  windows-depends:
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        os: [windows]
 | 
			
		||||
        arch: [amd64]
 | 
			
		||||
        preset: ['CPU']
 | 
			
		||||
        include:
 | 
			
		||||
          - os: windows
 | 
			
		||||
            arch: amd64
 | 
			
		||||
            preset: 'CUDA 11'
 | 
			
		||||
            install: https://developer.download.nvidia.com/compute/cuda/11.3.1/local_installers/cuda_11.3.1_465.89_win10.exe
 | 
			
		||||
            cuda-version: '11.3'
 | 
			
		||||
          - os: windows
 | 
			
		||||
            arch: amd64
 | 
			
		||||
            preset: 'CUDA 12'
 | 
			
		||||
            install: https://developer.download.nvidia.com/compute/cuda/12.8.0/local_installers/cuda_12.8.0_571.96_windows.exe
 | 
			
		||||
            cuda-version: '12.8'
 | 
			
		||||
          - os: windows
 | 
			
		||||
            arch: amd64
 | 
			
		||||
            preset: 'ROCm 6'
 | 
			
		||||
            install: https://download.amd.com/developer/eula/rocm-hub/AMD-Software-PRO-Edition-24.Q4-WinSvr2022-For-HIP.exe
 | 
			
		||||
            rocm-version: '6.2'
 | 
			
		||||
    runs-on: ${{ matrix.arch == 'arm64' && format('{0}-{1}', matrix.os, matrix.arch) || matrix.os }}
 | 
			
		||||
    environment: release
 | 
			
		||||
    env:
 | 
			
		||||
      GOFLAGS: ${{ needs.setup-environment.outputs.GOFLAGS }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Install system dependencies
 | 
			
		||||
        run: |
 | 
			
		||||
          choco install -y --no-progress ccache ninja
 | 
			
		||||
          ccache -o cache_dir=${{ github.workspace }}\.ccache
 | 
			
		||||
      - if: startsWith(matrix.preset, 'CUDA ') || startsWith(matrix.preset, 'ROCm ')
 | 
			
		||||
        id: cache-install
 | 
			
		||||
        uses: actions/cache/restore@v4
 | 
			
		||||
        with:
 | 
			
		||||
          path: |
 | 
			
		||||
            C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA
 | 
			
		||||
            C:\Program Files\AMD\ROCm
 | 
			
		||||
          key: ${{ matrix.install }}
 | 
			
		||||
      - if: startsWith(matrix.preset, 'CUDA ')
 | 
			
		||||
        name: Install CUDA ${{ matrix.cuda-version }}
 | 
			
		||||
        run: |
 | 
			
		||||
          $ErrorActionPreference = "Stop"
 | 
			
		||||
          if ("${{ steps.cache-install.outputs.cache-hit }}" -ne 'true') {
 | 
			
		||||
            Invoke-WebRequest -Uri "${{ matrix.install }}" -OutFile "install.exe"
 | 
			
		||||
            $subpackages = @("cudart", "nvcc", "cublas", "cublas_dev") | Foreach-Object {"${_}_${{ matrix.cuda-version }}"}
 | 
			
		||||
            Start-Process -FilePath .\install.exe -ArgumentList (@("-s") + $subpackages) -NoNewWindow -Wait
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          $cudaPath = (Resolve-Path "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\*").path
 | 
			
		||||
          echo "$cudaPath\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
 | 
			
		||||
      - if: startsWith(matrix.preset, 'ROCm')
 | 
			
		||||
        name: Install ROCm ${{ matrix.rocm-version }}
 | 
			
		||||
        run: |
 | 
			
		||||
          $ErrorActionPreference = "Stop"
 | 
			
		||||
          if ("${{ steps.cache-install.outputs.cache-hit }}" -ne 'true') {
 | 
			
		||||
            Invoke-WebRequest -Uri "${{ matrix.install }}" -OutFile "install.exe"
 | 
			
		||||
            Start-Process -FilePath .\install.exe -ArgumentList '-install' -NoNewWindow -Wait
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          $hipPath = (Resolve-Path "C:\Program Files\AMD\ROCm\*").path
 | 
			
		||||
          echo "$hipPath\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
 | 
			
		||||
          echo "CC=$hipPath\bin\clang.exe" | Out-File -FilePath $env:GITHUB_ENV -Append
 | 
			
		||||
          echo "CXX=$hipPath\bin\clang++.exe" | Out-File -FilePath $env:GITHUB_ENV -Append
 | 
			
		||||
      - if: matrix.preset == 'CPU'
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "CC=clang.exe" | Out-File -FilePath $env:GITHUB_ENV -Append
 | 
			
		||||
          echo "CXX=clang++.exe" | Out-File -FilePath $env:GITHUB_ENV -Append
 | 
			
		||||
      - if: ${{ !cancelled() && steps.cache-install.outputs.cache-hit != 'true' }}
 | 
			
		||||
        uses: actions/cache/save@v4
 | 
			
		||||
        with:
 | 
			
		||||
          path: |
 | 
			
		||||
            C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA
 | 
			
		||||
            C:\Program Files\AMD\ROCm
 | 
			
		||||
          key: ${{ matrix.install }}
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/cache@v4
 | 
			
		||||
        with:
 | 
			
		||||
          path: ${{ github.workspace }}\.ccache
 | 
			
		||||
          key: ccache-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.preset }}
 | 
			
		||||
      - name: Build target "${{ matrix.preset }}"
 | 
			
		||||
        run: |
 | 
			
		||||
          Import-Module 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Microsoft.VisualStudio.DevShell.dll'
 | 
			
		||||
          Enter-VsDevShell -VsInstallPath 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise' -SkipAutomaticLocation  -DevCmdArguments '-arch=x64 -no_logo'
 | 
			
		||||
          cmake --preset "${{ matrix.preset }}"
 | 
			
		||||
          cmake --build --parallel --preset "${{ matrix.preset }}"
 | 
			
		||||
          cmake --install build --component "${{ startsWith(matrix.preset, 'CUDA ') && 'CUDA' || startsWith(matrix.preset, 'ROCm ') && 'HIP' || 'CPU' }}" --strip --parallel 8
 | 
			
		||||
        env:
 | 
			
		||||
          CMAKE_GENERATOR: Ninja
 | 
			
		||||
      - uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: depends-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.preset }}
 | 
			
		||||
          path: dist\*
 | 
			
		||||
 | 
			
		||||
  windows-build:
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        os: [windows]
 | 
			
		||||
        arch: [amd64, arm64]
 | 
			
		||||
    runs-on: ${{ matrix.arch == 'arm64' && format('{0}-{1}', matrix.os, matrix.arch) || matrix.os }}
 | 
			
		||||
    environment: release
 | 
			
		||||
    needs: [setup-environment]
 | 
			
		||||
    env:
 | 
			
		||||
      GOFLAGS: ${{ needs.setup-environment.outputs.GOFLAGS }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Install AMD64 system dependencies
 | 
			
		||||
        if: matrix.arch == 'amd64'
 | 
			
		||||
        run: |
 | 
			
		||||
          $ErrorActionPreference = "Stop"
 | 
			
		||||
          Start-Process "C:\msys64\usr\bin\pacman.exe" -ArgumentList @("-S", "--noconfirm", "mingw-w64-clang-x86_64-gcc-compat", "mingw-w64-clang-x86_64-clang") -NoNewWindow -Wait
 | 
			
		||||
          echo "C:\msys64\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
 | 
			
		||||
          echo "C:\msys64\clang64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
 | 
			
		||||
      - name: Install ARM64 system dependencies
 | 
			
		||||
        if: matrix.arch == 'arm64'
 | 
			
		||||
        run: |
 | 
			
		||||
          $ErrorActionPreference = "Stop"
 | 
			
		||||
          Set-ExecutionPolicy Bypass -Scope Process -Force
 | 
			
		||||
          [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
 | 
			
		||||
          iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
 | 
			
		||||
          echo "C:\ProgramData\chocolatey\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
 | 
			
		||||
 | 
			
		||||
          choco install -y --no-progress git gzip
 | 
			
		||||
          echo "C:\Program Files\Git\cmd" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
 | 
			
		||||
 | 
			
		||||
          Invoke-WebRequest -Uri "https://github.com/mstorsjo/llvm-mingw/releases/download/20240619/llvm-mingw-20240619-ucrt-aarch64.zip" -OutFile "${{ runner.temp }}\llvm-mingw-ucrt-aarch64.zip"
 | 
			
		||||
          Expand-Archive -Path ${{ runner.temp }}\llvm-mingw-ucrt-aarch64.zip -DestinationPath "C:\Program Files\"
 | 
			
		||||
          $installPath=(Resolve-Path -Path "C:\Program Files\llvm-mingw-*-ucrt-aarch64").path
 | 
			
		||||
          echo $installPath\bin | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
      - run: |
 | 
			
		||||
          go build -o dist/${{ matrix.os }}-${{ matrix.arch }}/ .
 | 
			
		||||
      - if: matrix.arch == 'arm64'
 | 
			
		||||
        run: |
 | 
			
		||||
          Invoke-WebRequest -Uri "https://aka.ms/vs/17/release/vc_redist.arm64.exe" -OutFile "dist\windows-arm64\vc_redist.arm64.exe"
 | 
			
		||||
      - run: |
 | 
			
		||||
          $env:VERSION='${{ github.ref_name }}' -Replace "v(.*)", '$1'
 | 
			
		||||
          & .\scripts\build_windows.ps1 buildApp
 | 
			
		||||
        env:
 | 
			
		||||
          VCToolsRedistDir: stub
 | 
			
		||||
      - uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: build-${{ matrix.os }}-${{ matrix.arch }}
 | 
			
		||||
          path: |
 | 
			
		||||
            dist\${{ matrix.os }}-${{ matrix.arch }}\*.exe
 | 
			
		||||
            dist\${{ matrix.os }}-${{ matrix.arch }}-app.exe
 | 
			
		||||
 | 
			
		||||
  windows-sign:
 | 
			
		||||
    runs-on: windows-2022
 | 
			
		||||
    environment: release
 | 
			
		||||
    needs: [windows-depends, windows-build]
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: google-github-actions/auth@v2
 | 
			
		||||
        with:
 | 
			
		||||
          project_id: ollama
 | 
			
		||||
          credentials_json: ${{ secrets.GOOGLE_SIGNING_CREDENTIALS }}
 | 
			
		||||
      - run: |
 | 
			
		||||
          $ErrorActionPreference = "Stop"
 | 
			
		||||
          Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=323507" -OutFile "${{ runner.temp }}\sdksetup.exe"
 | 
			
		||||
          Start-Process "${{ runner.temp }}\sdksetup.exe" -ArgumentList @("/q") -NoNewWindow -Wait
 | 
			
		||||
 | 
			
		||||
          Invoke-WebRequest -Uri "https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/cng-v1.0/kmscng-1.0-windows-amd64.zip" -OutFile "${{ runner.temp }}\plugin.zip"
 | 
			
		||||
          Expand-Archive -Path "${{ runner.temp }}\plugin.zip" -DestinationPath "${{ runner.temp }}\plugin\"
 | 
			
		||||
          & "${{ runner.temp }}\plugin\*\kmscng.msi" /quiet
 | 
			
		||||
 | 
			
		||||
          echo "${{ vars.OLLAMA_CERT }}" >ollama_inc.crt
 | 
			
		||||
      - uses: actions/download-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          pattern: build-windows-*
 | 
			
		||||
          path: dist\
 | 
			
		||||
          merge-multiple: true
 | 
			
		||||
      - uses: actions/download-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          pattern: depends-windows-amd64-*
 | 
			
		||||
          path: dist\windows-amd64\
 | 
			
		||||
          merge-multiple: true
 | 
			
		||||
      - run: |
 | 
			
		||||
          & .\scripts\build_windows.ps1 gatherDependencies sign buildInstaller distZip
 | 
			
		||||
        env:
 | 
			
		||||
          KEY_CONTAINER: ${{ vars.KEY_CONTAINER }}
 | 
			
		||||
      - uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: dist-windows
 | 
			
		||||
          path: |
 | 
			
		||||
            dist\OllamaSetup.exe
 | 
			
		||||
            dist\ollama-windows-*.zip
 | 
			
		||||
 | 
			
		||||
  linux-build:
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        include:
 | 
			
		||||
          - os: linux
 | 
			
		||||
            arch: amd64
 | 
			
		||||
            target: archive
 | 
			
		||||
          - os: linux
 | 
			
		||||
            arch: amd64
 | 
			
		||||
            target: rocm
 | 
			
		||||
          - os: linux
 | 
			
		||||
            arch: arm64
 | 
			
		||||
            target: archive
 | 
			
		||||
    runs-on: ${{ matrix.arch == 'arm64' && format('{0}-{1}', matrix.os, matrix.arch) || matrix.os }}
 | 
			
		||||
    environment: release
 | 
			
		||||
    needs: setup-environment
 | 
			
		||||
    env:
 | 
			
		||||
      GOFLAGS: ${{ needs.setup-environment.outputs.GOFLAGS }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: docker/setup-buildx-action@v3
 | 
			
		||||
      - uses: docker/build-push-action@v6
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: ${{ matrix.os }}/${{ matrix.arch }}
 | 
			
		||||
          target: ${{ matrix.target }}
 | 
			
		||||
          build-args: |
 | 
			
		||||
            GOFLAGS=${{ env.GOFLAGS }}
 | 
			
		||||
            CGO_CFLAGS=${{ env.CGO_CFLAGS }}
 | 
			
		||||
            CGO_CXXFLAGS=${{ env.CGO_CXXFLAGS }}
 | 
			
		||||
          outputs: type=local,dest=dist/${{ matrix.os }}-${{ matrix.arch }}
 | 
			
		||||
          cache-from: type=registry,ref=ollama/ollama:latest
 | 
			
		||||
          cache-to: type=inline
 | 
			
		||||
      - run: |
 | 
			
		||||
          for COMPONENT in bin/* lib/ollama/*; do
 | 
			
		||||
            case "$COMPONENT" in
 | 
			
		||||
              bin/ollama)               echo $COMPONENT >>ollama-${{ matrix.os }}-${{ matrix.arch }}.tar.in ;;
 | 
			
		||||
              lib/ollama/*.so)          echo $COMPONENT >>ollama-${{ matrix.os }}-${{ matrix.arch }}.tar.in ;;
 | 
			
		||||
              lib/ollama/cuda_v11)      echo $COMPONENT >>ollama-${{ matrix.os }}-${{ matrix.arch }}.tar.in ;;
 | 
			
		||||
              lib/ollama/cuda_v12)      echo $COMPONENT >>ollama-${{ matrix.os }}-${{ matrix.arch }}.tar.in ;;
 | 
			
		||||
              lib/ollama/cuda_jetpack5) echo $COMPONENT >>ollama-${{ matrix.os }}-${{ matrix.arch }}-jetpack5.tar.in ;;
 | 
			
		||||
              lib/ollama/cuda_jetpack6) echo $COMPONENT >>ollama-${{ matrix.os }}-${{ matrix.arch }}-jetpack6.tar.in ;;
 | 
			
		||||
              lib/ollama/rocm)          echo $COMPONENT >>ollama-${{ matrix.os }}-${{ matrix.arch }}-rocm.tar.in ;;
 | 
			
		||||
            esac
 | 
			
		||||
          done
 | 
			
		||||
        working-directory: dist/${{ matrix.os }}-${{ matrix.arch }}
 | 
			
		||||
      - run: |
 | 
			
		||||
          for ARCHIVE in dist/${{ matrix.os }}-${{ matrix.arch }}/*.tar.in; do
 | 
			
		||||
            tar c -C dist/${{ matrix.os }}-${{ matrix.arch }} -T $ARCHIVE --owner 0 --group 0 | pigz -9vc >$(basename ${ARCHIVE//.*/}.tgz);
 | 
			
		||||
          done
 | 
			
		||||
      - uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: dist-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.target }}
 | 
			
		||||
          path: |
 | 
			
		||||
            *.tgz
 | 
			
		||||
 | 
			
		||||
  # Build each Docker variant (OS, arch, and flavor) separately. Using QEMU is unreliable and slower.
 | 
			
		||||
  docker-build-push:
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        include:
 | 
			
		||||
          - os: linux
 | 
			
		||||
            arch: arm64
 | 
			
		||||
            build-args: |
 | 
			
		||||
              CGO_CFLAGS
 | 
			
		||||
              CGO_CXXFLAGS
 | 
			
		||||
              GOFLAGS
 | 
			
		||||
          - os: linux
 | 
			
		||||
            arch: amd64
 | 
			
		||||
            build-args: |
 | 
			
		||||
              CGO_CFLAGS
 | 
			
		||||
              CGO_CXXFLAGS
 | 
			
		||||
              GOFLAGS
 | 
			
		||||
          - os: linux
 | 
			
		||||
            arch: amd64
 | 
			
		||||
            suffix: '-rocm'
 | 
			
		||||
            build-args: |
 | 
			
		||||
              CGO_CFLAGS
 | 
			
		||||
              CGO_CXXFLAGS
 | 
			
		||||
              GOFLAGS
 | 
			
		||||
              FLAVOR=rocm
 | 
			
		||||
    runs-on: ${{ matrix.arch == 'arm64' && format('{0}-{1}', matrix.os, matrix.arch) || matrix.os }}
 | 
			
		||||
    environment: release
 | 
			
		||||
    needs: setup-environment
 | 
			
		||||
    env:
 | 
			
		||||
      GOFLAGS: ${{ needs.setup-environment.outputs.GOFLAGS }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: docker/setup-buildx-action@v3
 | 
			
		||||
      - uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ vars.DOCKER_USER }}
 | 
			
		||||
          password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
 | 
			
		||||
      - id: build-push
 | 
			
		||||
        uses: docker/build-push-action@v6
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          platforms: ${{ matrix.os }}/${{ matrix.arch }}
 | 
			
		||||
          build-args: ${{ matrix.build-args }}
 | 
			
		||||
          outputs: type=image,name=ollama/ollama,push-by-digest=true,name-canonical=true,push=true
 | 
			
		||||
          cache-from: type=registry,ref=ollama/ollama:latest
 | 
			
		||||
          cache-to: type=inline
 | 
			
		||||
      - run: |
 | 
			
		||||
          mkdir -p ${{ matrix.os }}-${{ matrix.arch }}
 | 
			
		||||
          echo "${{ steps.build-push.outputs.digest }}" >${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.suffix }}.txt
 | 
			
		||||
        working-directory: ${{ runner.temp }}
 | 
			
		||||
      - uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: digest-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.suffix }}
 | 
			
		||||
          path: |
 | 
			
		||||
            ${{ runner.temp }}/${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.suffix }}.txt
 | 
			
		||||
 | 
			
		||||
  # Merge Docker images for the same flavor into a single multi-arch manifest
 | 
			
		||||
  docker-merge-push:
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        suffix: ['', '-rocm']
 | 
			
		||||
    runs-on: linux
 | 
			
		||||
    environment: release
 | 
			
		||||
    needs: [docker-build-push]
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: docker/login-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ vars.DOCKER_USER }}
 | 
			
		||||
          password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
 | 
			
		||||
      - id: metadata
 | 
			
		||||
        uses: docker/metadata-action@v4
 | 
			
		||||
        with:
 | 
			
		||||
          flavor: |
 | 
			
		||||
            latest=false
 | 
			
		||||
            suffix=${{ matrix.suffix }}
 | 
			
		||||
          images: |
 | 
			
		||||
            ollama/ollama
 | 
			
		||||
          tags: |
 | 
			
		||||
            type=ref,enable=true,priority=600,prefix=pr-,event=pr
 | 
			
		||||
            type=semver,pattern={{version}}
 | 
			
		||||
      - uses: actions/download-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          pattern: digest-*
 | 
			
		||||
          path: ${{ runner.temp }}
 | 
			
		||||
          merge-multiple: true
 | 
			
		||||
      - run: |
 | 
			
		||||
          docker buildx imagetools create $(echo '${{ steps.metadata.outputs.json }}' | jq -cr '.tags | map("-t", .) | join(" ")') $(cat *-${{ matrix.suffix }}.txt | xargs printf 'ollama/ollama@%s ')
 | 
			
		||||
          docker buildx imagetools inspect ollama/ollama:${{ steps.metadata.outputs.version }}
 | 
			
		||||
        working-directory: ${{ runner.temp }}
 | 
			
		||||
 | 
			
		||||
  # Trigger downstream release process
 | 
			
		||||
  trigger:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    environment: release
 | 
			
		||||
    needs: [darwin-build, windows-build, windows-depends]
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Trigger downstream release process
 | 
			
		||||
        run: |
 | 
			
		||||
          curl -L \
 | 
			
		||||
            -X POST \
 | 
			
		||||
            -H "Accept: application/vnd.github+json" \
 | 
			
		||||
            -H "Authorization: Bearer ${{ secrets.RELEASE_TOKEN }}" \
 | 
			
		||||
            -H "X-GitHub-Api-Version: 2022-11-28" \
 | 
			
		||||
            https://api.github.com/repos/ollama/${{ vars.RELEASE_REPO }}/dispatches \
 | 
			
		||||
            -d "{\"event_type\": \"trigger-workflow\", \"client_payload\": {\"run_id\": \"${GITHUB_RUN_ID}\", \"version\": \"${GITHUB_REF_NAME#v}\"}}"
 | 
			
		||||
 | 
			
		||||
  # Aggregate all the assets and ship a release
 | 
			
		||||
  release:
 | 
			
		||||
    needs: [darwin-sign, windows-sign, linux-build]
 | 
			
		||||
    runs-on: linux
 | 
			
		||||
    environment: release
 | 
			
		||||
    permissions:
 | 
			
		||||
      contents: write
 | 
			
		||||
    env:
 | 
			
		||||
      GH_TOKEN: ${{ github.token }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/download-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: dist-darwin
 | 
			
		||||
          path: dist
 | 
			
		||||
      - uses: actions/download-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: dist-windows
 | 
			
		||||
          path: dist
 | 
			
		||||
      - uses: actions/download-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          pattern: dist-linux-*
 | 
			
		||||
          path: dist
 | 
			
		||||
          merge-multiple: true
 | 
			
		||||
      - run: find . -type f -not -name 'sha256sum.txt' | xargs sha256sum | tee sha256sum.txt
 | 
			
		||||
        working-directory: dist
 | 
			
		||||
      - name: Create or update Release
 | 
			
		||||
        run: |
 | 
			
		||||
          RELEASE_VERSION="$(echo ${GITHUB_REF_NAME} | cut -f1 -d-)"
 | 
			
		||||
 | 
			
		||||
          echo "Looking for existing release for ${RELEASE_VERSION}"
 | 
			
		||||
          OLD_TAG=$(gh release ls --json name,tagName | jq -r ".[] | select(.name == \"${RELEASE_VERSION}\") | .tagName")
 | 
			
		||||
          if [ -n "$OLD_TAG" ]; then
 | 
			
		||||
            echo "Updating release ${RELEASE_VERSION} to point to new tag ${GITHUB_REF_NAME}"
 | 
			
		||||
            gh release edit ${OLD_TAG} --tag ${GITHUB_REF_NAME}
 | 
			
		||||
          else
 | 
			
		||||
            echo "Creating new release ${RELEASE_VERSION} pointing to tag ${GITHUB_REF_NAME}"
 | 
			
		||||
            gh release create ${GITHUB_REF_NAME} \
 | 
			
		||||
              --title ${RELEASE_VERSION} \
 | 
			
		||||
              --draft \
 | 
			
		||||
              --generate-notes \
 | 
			
		||||
              --prerelease
 | 
			
		||||
          fi
 | 
			
		||||
          echo "Uploading artifacts for tag ${GITHUB_REF_NAME}"
 | 
			
		||||
          gh release upload ${GITHUB_REF_NAME} dist/* --clobber
 | 
			
		||||
							
								
								
									
										299
									
								
								.github/workflows/test.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										299
									
								
								.github/workflows/test.yaml
									
									
									
									
										vendored
									
									
								
							@@ -1,106 +1,241 @@
 | 
			
		||||
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:
 | 
			
		||||
  pull_request:
 | 
			
		||||
    paths:
 | 
			
		||||
      - '**/*'
 | 
			
		||||
      - '!docs/**'
 | 
			
		||||
      - '!README.md'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  generate:
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        os: [ubuntu-latest, macos-latest, windows-latest]
 | 
			
		||||
        arch: [amd64, arm64]
 | 
			
		||||
        exclude:
 | 
			
		||||
          - os: ubuntu-latest
 | 
			
		||||
            arch: arm64
 | 
			
		||||
          - os: windows-latest
 | 
			
		||||
            arch: arm64
 | 
			
		||||
    runs-on: ${{ matrix.os }}
 | 
			
		||||
    env:
 | 
			
		||||
      GOARCH: ${{ matrix.arch }}
 | 
			
		||||
  changes:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    outputs:
 | 
			
		||||
      changed: ${{ steps.changes.outputs.changed }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version: '1.21'
 | 
			
		||||
          cache: true
 | 
			
		||||
      - if: ${{ startsWith(matrix.os, 'windows-') }}
 | 
			
		||||
        shell: pwsh
 | 
			
		||||
          fetch-depth: 0
 | 
			
		||||
      - id: changes
 | 
			
		||||
        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
 | 
			
		||||
                  }
 | 
			
		||||
              }
 | 
			
		||||
          changed() {
 | 
			
		||||
            local BASE=${{ github.event.pull_request.base.sha }}
 | 
			
		||||
            local HEAD=${{ github.event.pull_request.head.sha }}
 | 
			
		||||
            local MERGE_BASE=$(git merge-base $BASE $HEAD)
 | 
			
		||||
            git diff-tree -r --no-commit-id --name-only "$MERGE_BASE" "$HEAD" \
 | 
			
		||||
              | xargs python3 -c "import sys; from pathlib import Path; print(any(Path(x).match(glob) for x in sys.argv[1:] for glob in '$*'.split(' ')))"
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          echo "C:\Program Files\Git\usr\bin" | Out-File -FilePath $Env:GITHUB_PATH -Encoding utf8 -Append
 | 
			
		||||
      - run: go get ./...
 | 
			
		||||
      - run: go generate -x ./...
 | 
			
		||||
      - uses: actions/upload-artifact@v4
 | 
			
		||||
          echo changed=$(changed 'llama/llama.cpp/**' 'ml/backend/ggml/ggml/**') | tee -a $GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
  linux:
 | 
			
		||||
    needs: [changes]
 | 
			
		||||
    if: needs.changes.outputs.changed == 'True'
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        include:
 | 
			
		||||
          - preset: CPU
 | 
			
		||||
          - preset: CUDA
 | 
			
		||||
            container: nvidia/cuda:11.8.0-devel-ubuntu22.04
 | 
			
		||||
            flags: '-DCMAKE_CUDA_ARCHITECTURES=87'
 | 
			
		||||
          - preset: ROCm
 | 
			
		||||
            container: rocm/dev-ubuntu-22.04:6.1.2
 | 
			
		||||
            extra-packages: rocm-libs
 | 
			
		||||
            flags: '-DAMDGPU_TARGETS=gfx1010 -DCMAKE_PREFIX_PATH=/opt/rocm'
 | 
			
		||||
    runs-on: linux
 | 
			
		||||
    container: ${{ matrix.container }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - run: |
 | 
			
		||||
          [ -n "${{ matrix.container }}" ] || sudo=sudo
 | 
			
		||||
          $sudo apt-get update
 | 
			
		||||
          $sudo apt-get install -y cmake ccache ${{ matrix.extra-packages }}
 | 
			
		||||
        env:
 | 
			
		||||
          DEBIAN_FRONTEND: noninteractive
 | 
			
		||||
      - uses: actions/cache@v4
 | 
			
		||||
        with:
 | 
			
		||||
          path: /github/home/.cache/ccache
 | 
			
		||||
          key: ccache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.preset }}
 | 
			
		||||
      - run: |
 | 
			
		||||
          cmake --preset ${{ matrix.preset }} ${{ matrix.flags }}
 | 
			
		||||
          cmake --build --preset ${{ matrix.preset }} --parallel
 | 
			
		||||
 | 
			
		||||
  windows:
 | 
			
		||||
    needs: [changes]
 | 
			
		||||
    if: needs.changes.outputs.changed == 'True'
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        include:
 | 
			
		||||
          - preset: CPU
 | 
			
		||||
          - preset: CUDA
 | 
			
		||||
            install: https://developer.download.nvidia.com/compute/cuda/11.3.1/local_installers/cuda_11.3.1_465.89_win10.exe
 | 
			
		||||
            flags: '-DCMAKE_CUDA_ARCHITECTURES=80'
 | 
			
		||||
          - preset: ROCm
 | 
			
		||||
            install: https://download.amd.com/developer/eula/rocm-hub/AMD-Software-PRO-Edition-24.Q4-WinSvr2022-For-HIP.exe
 | 
			
		||||
            flags: '-DAMDGPU_TARGETS=gfx1010'
 | 
			
		||||
    runs-on: windows
 | 
			
		||||
    steps:
 | 
			
		||||
      - run: |
 | 
			
		||||
          choco install -y --no-progress ccache ninja
 | 
			
		||||
          ccache -o cache_dir=${{ github.workspace }}\.ccache
 | 
			
		||||
      - if: matrix.preset == 'CUDA' || matrix.preset == 'ROCm'
 | 
			
		||||
        id: cache-install
 | 
			
		||||
        uses: actions/cache/restore@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: ${{ matrix.os }}-${{ matrix.arch }}-libraries
 | 
			
		||||
          path: |
 | 
			
		||||
            llm/llama.cpp/build/**/lib/*
 | 
			
		||||
  lint:
 | 
			
		||||
    needs: generate
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        os: [ubuntu-latest, macos-latest, windows-latest]
 | 
			
		||||
        arch: [amd64, arm64]
 | 
			
		||||
        exclude:
 | 
			
		||||
          - os: ubuntu-latest
 | 
			
		||||
            arch: arm64
 | 
			
		||||
          - os: windows-latest
 | 
			
		||||
            arch: arm64
 | 
			
		||||
          - os: macos-latest
 | 
			
		||||
            arch: amd64
 | 
			
		||||
    runs-on: ${{ matrix.os }}
 | 
			
		||||
    env:
 | 
			
		||||
      GOARCH: ${{ matrix.arch }}
 | 
			
		||||
      CGO_ENABLED: "1"
 | 
			
		||||
            C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA
 | 
			
		||||
            C:\Program Files\AMD\ROCm
 | 
			
		||||
          key: ${{ matrix.install }}
 | 
			
		||||
      - if: matrix.preset == 'CUDA'
 | 
			
		||||
        name: Install CUDA ${{ matrix.cuda-version }}
 | 
			
		||||
        run: |
 | 
			
		||||
          $ErrorActionPreference = "Stop"
 | 
			
		||||
          if ("${{ steps.cache-install.outputs.cache-hit }}" -ne 'true') {
 | 
			
		||||
            Invoke-WebRequest -Uri "${{ matrix.install }}" -OutFile "install.exe"
 | 
			
		||||
            Start-Process -FilePath .\install.exe -ArgumentList (@("-s", "cudart_11.3", "nvcc_11.3", "cublas_11.3", "cublas_dev_11.3")) -NoNewWindow -Wait
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          $cudaPath = (Resolve-Path "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\*").path
 | 
			
		||||
          echo "$cudaPath\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
 | 
			
		||||
      - if: matrix.preset == 'ROCm'
 | 
			
		||||
        name: Install ROCm ${{ matrix.rocm-version }}
 | 
			
		||||
        run: |
 | 
			
		||||
          $ErrorActionPreference = "Stop"
 | 
			
		||||
          if ("${{ steps.cache-install.outputs.cache-hit }}" -ne 'true') {
 | 
			
		||||
            Invoke-WebRequest -Uri "${{ matrix.install }}" -OutFile "install.exe"
 | 
			
		||||
            Start-Process -FilePath .\install.exe -ArgumentList '-install' -NoNewWindow -Wait
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          $hipPath = (Resolve-Path "C:\Program Files\AMD\ROCm\*").path
 | 
			
		||||
          echo "$hipPath\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
 | 
			
		||||
          echo "CC=$hipPath\bin\clang.exe" | Out-File -FilePath $env:GITHUB_ENV -Append
 | 
			
		||||
          echo "CXX=$hipPath\bin\clang++.exe" | Out-File -FilePath $env:GITHUB_ENV -Append
 | 
			
		||||
      - if: ${{ !cancelled() && steps.cache-install.outputs.cache-hit != 'true' }}
 | 
			
		||||
        uses: actions/cache/save@v4
 | 
			
		||||
        with:
 | 
			
		||||
          path: |
 | 
			
		||||
            C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA
 | 
			
		||||
            C:\Program Files\AMD\ROCm
 | 
			
		||||
          key: ${{ matrix.install }}
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/cache@v4
 | 
			
		||||
        with:
 | 
			
		||||
          path: ${{ github.workspace }}\.ccache
 | 
			
		||||
          key: ccache-${{ runner.os }}-${{ runner.arch }}-${{ matrix.preset }}
 | 
			
		||||
      - run: |
 | 
			
		||||
          Import-Module 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Microsoft.VisualStudio.DevShell.dll'
 | 
			
		||||
          Enter-VsDevShell -VsInstallPath 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise' -SkipAutomaticLocation  -DevCmdArguments '-arch=x64 -no_logo'
 | 
			
		||||
          cmake --preset "${{ matrix.preset }}" ${{ matrix.flags }}
 | 
			
		||||
          cmake --build --parallel --preset "${{ matrix.preset }}"
 | 
			
		||||
        env:
 | 
			
		||||
          CMAKE_GENERATOR: Ninja
 | 
			
		||||
 | 
			
		||||
  go_mod_tidy:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
        with:
 | 
			
		||||
          submodules: recursive
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version: '1.21'
 | 
			
		||||
          cache: false
 | 
			
		||||
      - uses: actions/download-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: ${{ matrix.os }}-${{ matrix.arch }}-libraries
 | 
			
		||||
          path: llm/llama.cpp/build
 | 
			
		||||
      - uses: golangci/golangci-lint-action@v3
 | 
			
		||||
      - name: check that 'go mod tidy' is clean
 | 
			
		||||
        run: go mod tidy --diff || (echo "Please run 'go mod tidy'." && exit 1)
 | 
			
		||||
 | 
			
		||||
  test:
 | 
			
		||||
    needs: generate
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        os: [ubuntu-latest, macos-latest, windows-latest]
 | 
			
		||||
        arch: [amd64]
 | 
			
		||||
        exclude:
 | 
			
		||||
          - os: ubuntu-latest
 | 
			
		||||
            arch: arm64
 | 
			
		||||
          - os: windows-latest
 | 
			
		||||
            arch: arm64
 | 
			
		||||
    runs-on: ${{ matrix.os }}
 | 
			
		||||
    env:
 | 
			
		||||
      GOARCH: ${{ matrix.arch }}
 | 
			
		||||
      CGO_ENABLED: "1"
 | 
			
		||||
      CGO_ENABLED: '1'
 | 
			
		||||
      GOEXPERIMENT: 'synctest'
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: checkout
 | 
			
		||||
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
 | 
			
		||||
 | 
			
		||||
      - name: cache restore
 | 
			
		||||
        uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
 | 
			
		||||
        with:
 | 
			
		||||
          # Note: unlike the other setups, this is only grabbing the mod download
 | 
			
		||||
          # cache, rather than the whole mod directory, as the download cache
 | 
			
		||||
          # contains zips that can be unpacked in parallel faster than they can be
 | 
			
		||||
          # fetched and extracted by tar
 | 
			
		||||
          path: |
 | 
			
		||||
            ~/.cache/go-build
 | 
			
		||||
            ~/go/pkg/mod/cache
 | 
			
		||||
            ~\AppData\Local\go-build
 | 
			
		||||
          # NOTE: The -3- here should be incremented when the scheme of data to be
 | 
			
		||||
          # cached changes (e.g. path above changes).
 | 
			
		||||
          key: ${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-3-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            ${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-3-${{ hashFiles('**/go.sum') }}
 | 
			
		||||
            ${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-3-
 | 
			
		||||
 | 
			
		||||
      - name: Setup Go
 | 
			
		||||
        uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          # The caching strategy of setup-go is less than ideal, and wastes
 | 
			
		||||
          # time by not saving artifacts due to small failures like the linter
 | 
			
		||||
          # complaining, etc. This means subsequent have to rebuild their world
 | 
			
		||||
          # again until all checks pass. For instance, if you mispell a word,
 | 
			
		||||
          # you're punished until you fix it. This is more hostile than
 | 
			
		||||
          # helpful.
 | 
			
		||||
          cache: false
 | 
			
		||||
 | 
			
		||||
          go-version-file: go.mod
 | 
			
		||||
 | 
			
		||||
      # It is tempting to run this in a platform independent way, but the past
 | 
			
		||||
      # shows this codebase will see introductions of platform specific code
 | 
			
		||||
      # generation, and so we need to check this per platform to ensure we
 | 
			
		||||
      # don't abuse go generate on specific platforms.
 | 
			
		||||
      - name: check that 'go generate' is clean
 | 
			
		||||
        if: always()
 | 
			
		||||
        run: |
 | 
			
		||||
          go generate ./...
 | 
			
		||||
          git diff --name-only --exit-code || (echo "Please run 'go generate ./...'." && exit 1)
 | 
			
		||||
 | 
			
		||||
      - name: go test
 | 
			
		||||
        if: always()
 | 
			
		||||
        run: go test -count=1 -benchtime=1x ./...
 | 
			
		||||
 | 
			
		||||
      # TODO(bmizerany): replace this heavy tool with just the
 | 
			
		||||
      # tools/checks/binaries we want and then make them all run in parallel
 | 
			
		||||
      # across jobs, not on a single tiny vm on Github Actions.
 | 
			
		||||
      - uses: golangci/golangci-lint-action@v6
 | 
			
		||||
        with:
 | 
			
		||||
          args: --timeout 10m0s -v
 | 
			
		||||
 | 
			
		||||
      - name: cache save
 | 
			
		||||
        # Always save the cache, even if the job fails. The artifacts produced
 | 
			
		||||
        # during the building of test binaries are not all for naught. They can
 | 
			
		||||
        # be used to speed up subsequent runs.
 | 
			
		||||
        if: always()
 | 
			
		||||
 | 
			
		||||
        uses: actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
 | 
			
		||||
        with:
 | 
			
		||||
          # Note: unlike the other setups, this is only grabbing the mod download
 | 
			
		||||
          # cache, rather than the whole mod directory, as the download cache
 | 
			
		||||
          # contains zips that can be unpacked in parallel faster than they can be
 | 
			
		||||
          # fetched and extracted by tar
 | 
			
		||||
          path: |
 | 
			
		||||
            ~/.cache/go-build
 | 
			
		||||
            ~/go/pkg/mod/cache
 | 
			
		||||
            ~\AppData\Local\go-build
 | 
			
		||||
          # NOTE: The -3- here should be incremented when the scheme of data to be
 | 
			
		||||
          # cached changes (e.g. path above changes).
 | 
			
		||||
          key: ${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-3-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
 | 
			
		||||
 | 
			
		||||
  patches:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
        with:
 | 
			
		||||
          submodules: recursive
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version: '1.21'
 | 
			
		||||
          cache: true
 | 
			
		||||
      - run: go get
 | 
			
		||||
      - uses: actions/download-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: ${{ matrix.os }}-${{ matrix.arch }}-libraries
 | 
			
		||||
          path: llm/llama.cpp/build
 | 
			
		||||
      - run: go build
 | 
			
		||||
      - run: go test -v ./...
 | 
			
		||||
      - name: Verify patches apply cleanly and do not change files
 | 
			
		||||
        run: |
 | 
			
		||||
          make -f Makefile.sync clean checkout apply-patches sync
 | 
			
		||||
          git diff --compact-summary --exit-code
 | 
			
		||||
							
								
								
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -4,9 +4,13 @@
 | 
			
		||||
.venv
 | 
			
		||||
.swp
 | 
			
		||||
dist
 | 
			
		||||
ollama
 | 
			
		||||
ggml-metal.metal
 | 
			
		||||
build
 | 
			
		||||
.cache
 | 
			
		||||
*.exe
 | 
			
		||||
.idea
 | 
			
		||||
test_data
 | 
			
		||||
*.crt
 | 
			
		||||
__debug_bin*
 | 
			
		||||
llama/build
 | 
			
		||||
llama/vendor
 | 
			
		||||
/ollama
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							@@ -1,4 +0,0 @@
 | 
			
		||||
[submodule "llama.cpp"]
 | 
			
		||||
	path = llm/llama.cpp
 | 
			
		||||
	url = https://github.com/ggerganov/llama.cpp.git
 | 
			
		||||
	shallow = true
 | 
			
		||||
@@ -6,22 +6,36 @@ linters:
 | 
			
		||||
    - bidichk
 | 
			
		||||
    - bodyclose
 | 
			
		||||
    - containedctx
 | 
			
		||||
    - contextcheck
 | 
			
		||||
    - exportloopref
 | 
			
		||||
    - gocheckcompilerdirectives
 | 
			
		||||
    # FIXME: for some reason this errors on windows
 | 
			
		||||
    # - gofmt
 | 
			
		||||
    # - goimports
 | 
			
		||||
    - gofmt
 | 
			
		||||
    - gofumpt
 | 
			
		||||
    - gosimple
 | 
			
		||||
    - govet
 | 
			
		||||
    - ineffassign
 | 
			
		||||
    - intrange
 | 
			
		||||
    - makezero
 | 
			
		||||
    - misspell
 | 
			
		||||
    - nilerr
 | 
			
		||||
    - unused
 | 
			
		||||
    - nolintlint
 | 
			
		||||
    - nosprintfhostport
 | 
			
		||||
    - staticcheck
 | 
			
		||||
    - unconvert
 | 
			
		||||
    - usetesting
 | 
			
		||||
    - wastedassign
 | 
			
		||||
    - whitespace
 | 
			
		||||
  disable:
 | 
			
		||||
    - usestdlibvars
 | 
			
		||||
    - errcheck
 | 
			
		||||
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
 | 
			
		||||
  staticcheck:
 | 
			
		||||
    checks:
 | 
			
		||||
      - all
 | 
			
		||||
      - -SA1019 # omit Deprecated check
 | 
			
		||||
severity:
 | 
			
		||||
  default-severity: error
 | 
			
		||||
  rules:
 | 
			
		||||
    - linters:
 | 
			
		||||
        - gofmt
 | 
			
		||||
        - goimports
 | 
			
		||||
        - intrange
 | 
			
		||||
      severity: info
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "trailingComma": "es5",
 | 
			
		||||
  "tabWidth": 2,
 | 
			
		||||
  "useTabs": false,
 | 
			
		||||
  "semi": false,
 | 
			
		||||
  "singleQuote": true,
 | 
			
		||||
  "jsxSingleQuote": true,
 | 
			
		||||
  "printWidth": 120,
 | 
			
		||||
  "arrowParens": "avoid"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										133
									
								
								CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								CMakeLists.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,133 @@
 | 
			
		||||
cmake_minimum_required(VERSION 3.21)
 | 
			
		||||
 | 
			
		||||
project(Ollama C CXX)
 | 
			
		||||
 | 
			
		||||
include(CheckLanguage)
 | 
			
		||||
 | 
			
		||||
find_package(Threads REQUIRED)
 | 
			
		||||
 | 
			
		||||
set(CMAKE_BUILD_TYPE Release)
 | 
			
		||||
set(BUILD_SHARED_LIBS ON)
 | 
			
		||||
 | 
			
		||||
set(CMAKE_CXX_STANDARD 17)
 | 
			
		||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
 | 
			
		||||
set(CMAKE_CXX_EXTENSIONS OFF)
 | 
			
		||||
 | 
			
		||||
set(GGML_BUILD ON)
 | 
			
		||||
set(GGML_SHARED ON)
 | 
			
		||||
set(GGML_CCACHE ON)
 | 
			
		||||
set(GGML_BACKEND_DL ON)
 | 
			
		||||
set(GGML_BACKEND_SHARED ON)
 | 
			
		||||
set(GGML_SCHED_MAX_COPIES 4)
 | 
			
		||||
 | 
			
		||||
set(GGML_LLAMAFILE ON)
 | 
			
		||||
set(GGML_CUDA_PEER_MAX_BATCH_SIZE 128)
 | 
			
		||||
set(GGML_CUDA_GRAPHS ON)
 | 
			
		||||
set(GGML_CUDA_FA ON)
 | 
			
		||||
set(GGML_CUDA_COMPRESSION_MODE default)
 | 
			
		||||
 | 
			
		||||
if((CMAKE_OSX_ARCHITECTURES AND NOT CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
 | 
			
		||||
    OR (NOT CMAKE_OSX_ARCHITECTURES AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "arm|aarch64|ARM64|ARMv[0-9]+"))
 | 
			
		||||
    set(GGML_CPU_ALL_VARIANTS ON)
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
if (CMAKE_OSX_ARCHITECTURES MATCHES "x86_64")
 | 
			
		||||
    set(CMAKE_BUILD_RPATH "@loader_path")
 | 
			
		||||
    set(CMAKE_INSTALL_RPATH "@loader_path")
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
set(OLLAMA_BUILD_DIR ${CMAKE_BINARY_DIR}/lib/ollama)
 | 
			
		||||
set(OLLAMA_INSTALL_DIR ${CMAKE_INSTALL_PREFIX}/lib/ollama)
 | 
			
		||||
 | 
			
		||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY         ${OLLAMA_BUILD_DIR})
 | 
			
		||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG   ${OLLAMA_BUILD_DIR})
 | 
			
		||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${OLLAMA_BUILD_DIR})
 | 
			
		||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY         ${OLLAMA_BUILD_DIR})
 | 
			
		||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG   ${OLLAMA_BUILD_DIR})
 | 
			
		||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE ${OLLAMA_BUILD_DIR})
 | 
			
		||||
 | 
			
		||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/ml/backend/ggml/ggml/src)
 | 
			
		||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/ml/backend/ggml/ggml/src/include)
 | 
			
		||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/ml/backend/ggml/ggml/src/ggml-cpu)
 | 
			
		||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/ml/backend/ggml/ggml/src/ggml-cpu/amx)
 | 
			
		||||
 | 
			
		||||
set(GGML_CPU ON)
 | 
			
		||||
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/ml/backend/ggml/ggml/src)
 | 
			
		||||
set_property(TARGET ggml PROPERTY EXCLUDE_FROM_ALL TRUE)
 | 
			
		||||
 | 
			
		||||
get_target_property(CPU_VARIANTS ggml-cpu MANUALLY_ADDED_DEPENDENCIES)
 | 
			
		||||
if(NOT CPU_VARIANTS)
 | 
			
		||||
    set(CPU_VARIANTS "ggml-cpu")
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
install(TARGETS ggml-base ${CPU_VARIANTS}
 | 
			
		||||
    RUNTIME_DEPENDENCIES
 | 
			
		||||
        PRE_EXCLUDE_REGEXES ".*"
 | 
			
		||||
    RUNTIME DESTINATION ${OLLAMA_INSTALL_DIR} COMPONENT CPU
 | 
			
		||||
    LIBRARY DESTINATION ${OLLAMA_INSTALL_DIR} COMPONENT CPU
 | 
			
		||||
    FRAMEWORK DESTINATION ${OLLAMA_INSTALL_DIR} COMPONENT CPU
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
check_language(CUDA)
 | 
			
		||||
if(CMAKE_CUDA_COMPILER)
 | 
			
		||||
    if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24" AND NOT CMAKE_CUDA_ARCHITECTURES)
 | 
			
		||||
        set(CMAKE_CUDA_ARCHITECTURES "native")
 | 
			
		||||
    endif()
 | 
			
		||||
 | 
			
		||||
    find_package(CUDAToolkit)
 | 
			
		||||
    add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/ml/backend/ggml/ggml/src/ggml-cuda)
 | 
			
		||||
    set(OLLAMA_CUDA_INSTALL_DIR ${OLLAMA_INSTALL_DIR}/cuda_v${CUDAToolkit_VERSION_MAJOR})
 | 
			
		||||
    install(TARGETS ggml-cuda
 | 
			
		||||
        RUNTIME_DEPENDENCIES
 | 
			
		||||
            DIRECTORIES ${CUDAToolkit_BIN_DIR} ${CUDAToolkit_LIBRARY_DIR}
 | 
			
		||||
            PRE_INCLUDE_REGEXES cublas cublasLt cudart
 | 
			
		||||
            PRE_EXCLUDE_REGEXES ".*"
 | 
			
		||||
        RUNTIME DESTINATION ${OLLAMA_CUDA_INSTALL_DIR} COMPONENT CUDA
 | 
			
		||||
        LIBRARY DESTINATION ${OLLAMA_CUDA_INSTALL_DIR} COMPONENT CUDA
 | 
			
		||||
    )
 | 
			
		||||
endif()
 | 
			
		||||
 | 
			
		||||
set(WINDOWS_AMDGPU_TARGETS_EXCLUDE_REGEX "^gfx(906|908|90a|1200|1201):xnack[+-]$"
 | 
			
		||||
    CACHE STRING
 | 
			
		||||
    "Regular expression describing AMDGPU_TARGETS not supported on Windows. Override to force building these targets. Default \"^gfx(906|908|90a|1200|1201):xnack[+-]$\"."
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
check_language(HIP)
 | 
			
		||||
if(CMAKE_HIP_COMPILER)
 | 
			
		||||
    set(HIP_PLATFORM "amd")
 | 
			
		||||
 | 
			
		||||
    find_package(hip REQUIRED)
 | 
			
		||||
    if(NOT AMDGPU_TARGETS)
 | 
			
		||||
        list(FILTER AMDGPU_TARGETS INCLUDE REGEX "^gfx(900|94[012]|101[02]|1030|110[012]|120[01])$")
 | 
			
		||||
    elseif(WIN32 AND WINDOWS_AMDGPU_TARGETS_EXCLUDE_REGEX)
 | 
			
		||||
        list(FILTER AMDGPU_TARGETS EXCLUDE REGEX ${WINDOWS_AMDGPU_TARGETS_EXCLUDE_REGEX})
 | 
			
		||||
    endif()
 | 
			
		||||
 | 
			
		||||
    if(AMDGPU_TARGETS)
 | 
			
		||||
        add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/ml/backend/ggml/ggml/src/ggml-hip)
 | 
			
		||||
 | 
			
		||||
        if (WIN32)
 | 
			
		||||
            target_compile_definitions(ggml-hip PRIVATE GGML_CUDA_NO_PEER_COPY)
 | 
			
		||||
        endif()
 | 
			
		||||
 | 
			
		||||
        target_compile_definitions(ggml-hip PRIVATE GGML_HIP_NO_VMM)
 | 
			
		||||
 | 
			
		||||
        set(OLLAMA_HIP_INSTALL_DIR ${OLLAMA_INSTALL_DIR}/rocm)
 | 
			
		||||
        install(TARGETS ggml-hip
 | 
			
		||||
            RUNTIME_DEPENDENCIES
 | 
			
		||||
                DIRECTORIES ${HIP_BIN_INSTALL_DIR} ${HIP_LIB_INSTALL_DIR}
 | 
			
		||||
                PRE_INCLUDE_REGEXES hipblas rocblas amdhip64 rocsolver amd_comgr hsa-runtime64 rocsparse tinfo rocprofiler-register drm drm_amdgpu numa elf
 | 
			
		||||
                PRE_EXCLUDE_REGEXES ".*"
 | 
			
		||||
                POST_EXCLUDE_REGEXES "system32"
 | 
			
		||||
            RUNTIME DESTINATION ${OLLAMA_HIP_INSTALL_DIR} COMPONENT HIP
 | 
			
		||||
            LIBRARY DESTINATION ${OLLAMA_HIP_INSTALL_DIR} COMPONENT HIP
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        foreach(HIP_LIB_BIN_INSTALL_DIR IN ITEMS ${HIP_BIN_INSTALL_DIR} ${HIP_LIB_INSTALL_DIR})
 | 
			
		||||
            if(EXISTS ${HIP_LIB_BIN_INSTALL_DIR}/rocblas)
 | 
			
		||||
                install(DIRECTORY ${HIP_LIB_BIN_INSTALL_DIR}/rocblas DESTINATION ${OLLAMA_HIP_INSTALL_DIR} COMPONENT HIP)
 | 
			
		||||
                break()
 | 
			
		||||
            endif()
 | 
			
		||||
        endforeach()
 | 
			
		||||
    endif()
 | 
			
		||||
endif()
 | 
			
		||||
							
								
								
									
										112
									
								
								CMakePresets.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								CMakePresets.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,112 @@
 | 
			
		||||
{
 | 
			
		||||
  "version": 3,
 | 
			
		||||
  "configurePresets": [
 | 
			
		||||
    {
 | 
			
		||||
      "name": "Default",
 | 
			
		||||
      "binaryDir": "${sourceDir}/build",
 | 
			
		||||
      "installDir": "${sourceDir}/dist",
 | 
			
		||||
      "cacheVariables": {
 | 
			
		||||
        "CMAKE_BUILD_TYPE": "Release"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "CPU",
 | 
			
		||||
      "inherits": [ "Default" ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "CUDA",
 | 
			
		||||
      "inherits": [ "Default" ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "CUDA 11",
 | 
			
		||||
      "inherits": [ "CUDA" ],
 | 
			
		||||
      "cacheVariables": {
 | 
			
		||||
        "CMAKE_CUDA_ARCHITECTURES": "50;52;53;60;61;70;75;80;86",
 | 
			
		||||
        "CMAKE_CUDA_FLAGS": "-Wno-deprecated-gpu-targets"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "CUDA 12",
 | 
			
		||||
      "inherits": [ "CUDA" ],
 | 
			
		||||
      "cacheVariables": {
 | 
			
		||||
        "CMAKE_CUDA_ARCHITECTURES": "50;60;61;70;75;80;86;87;89;90;90a;120",
 | 
			
		||||
        "CMAKE_CUDA_FLAGS": "-Wno-deprecated-gpu-targets"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "JetPack 5",
 | 
			
		||||
      "inherits": [ "CUDA" ],
 | 
			
		||||
      "cacheVariables": {
 | 
			
		||||
        "CMAKE_CUDA_ARCHITECTURES": "72;87"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "JetPack 6",
 | 
			
		||||
      "inherits": [ "CUDA" ],
 | 
			
		||||
      "cacheVariables": {
 | 
			
		||||
        "CMAKE_CUDA_ARCHITECTURES": "87"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "ROCm",
 | 
			
		||||
      "inherits": [ "Default" ],
 | 
			
		||||
      "cacheVariables": {
 | 
			
		||||
        "CMAKE_HIP_PLATFORM": "amd"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "ROCm 6",
 | 
			
		||||
      "inherits": [ "ROCm" ],
 | 
			
		||||
      "cacheVariables": {
 | 
			
		||||
        "AMDGPU_TARGETS": "gfx900;gfx940;gfx941;gfx942;gfx1010;gfx1012;gfx1030;gfx1100;gfx1101;gfx1102;gfx1151;gfx1200;gfx1201;gfx906:xnack-;gfx908:xnack-;gfx90a:xnack+;gfx90a:xnack-"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "buildPresets": [
 | 
			
		||||
    {
 | 
			
		||||
      "name": "Default",
 | 
			
		||||
      "configurePreset": "Default",
 | 
			
		||||
      "configuration": "Release"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "CPU",
 | 
			
		||||
      "configurePreset": "Default",
 | 
			
		||||
      "targets": [ "ggml-cpu" ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "CUDA",
 | 
			
		||||
      "configurePreset": "CUDA",
 | 
			
		||||
      "targets": [ "ggml-cuda" ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "CUDA 11",
 | 
			
		||||
      "inherits": [ "CUDA" ],
 | 
			
		||||
      "configurePreset": "CUDA 11"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "CUDA 12",
 | 
			
		||||
      "inherits": [ "CUDA" ],
 | 
			
		||||
      "configurePreset": "CUDA 12"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "JetPack 5",
 | 
			
		||||
      "inherits": [ "CUDA" ],
 | 
			
		||||
      "configurePreset": "JetPack 5"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "JetPack 6",
 | 
			
		||||
      "inherits": [ "CUDA" ],
 | 
			
		||||
      "configurePreset": "JetPack 6"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "ROCm",
 | 
			
		||||
      "configurePreset": "ROCm",
 | 
			
		||||
      "targets": [ "ggml-hip" ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "name": "ROCm 6",
 | 
			
		||||
      "inherits": [ "ROCm" ],
 | 
			
		||||
      "configurePreset": "ROCm 6"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										88
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
# Contributing to Ollama
 | 
			
		||||
 | 
			
		||||
Thank you for your interest in contributing to Ollama! Here are a few guidelines to help get you started.
 | 
			
		||||
 | 
			
		||||
## Set up
 | 
			
		||||
 | 
			
		||||
See the [development documentation](./docs/development.md) for instructions on how to build and run Ollama locally.
 | 
			
		||||
 | 
			
		||||
### Ideal issues
 | 
			
		||||
 | 
			
		||||
* [Bugs](https://github.com/ollama/ollama/issues?q=is%3Aissue+is%3Aopen+label%3Abug): issues where Ollama stops working or where it results in an unexpected error.
 | 
			
		||||
* [Performance](https://github.com/ollama/ollama/issues?q=is%3Aissue+is%3Aopen+label%3Aperformance): issues to make Ollama faster at model inference, downloading or uploading.
 | 
			
		||||
* [Security](https://github.com/ollama/ollama/blob/main/SECURITY.md): issues that could lead to a security vulnerability. As mentioned in [SECURITY.md](https://github.com/ollama/ollama/blob/main/SECURITY.md), please do not disclose security vulnerabilities publicly.
 | 
			
		||||
 | 
			
		||||
### Issues that are harder to review
 | 
			
		||||
 | 
			
		||||
* New features: new features (e.g. API fields, environment variables) add surface area to Ollama and make it harder to maintain in the long run as they cannot be removed without potentially breaking users in the future.
 | 
			
		||||
* Refactoring: large code improvements are important, but can be harder or take longer to review and merge.
 | 
			
		||||
* Documentation: small updates to fill in or correct missing documentation is helpful, however large documentation additions can be hard to maintain over time.
 | 
			
		||||
 | 
			
		||||
### Issues that may not be accepted
 | 
			
		||||
 | 
			
		||||
* Changes that break backwards compatibility in Ollama's API (including the OpenAI-compatible API)
 | 
			
		||||
* Changes that add significant friction to the user experience
 | 
			
		||||
* Changes that create a large future maintenance burden for maintainers and contributors
 | 
			
		||||
 | 
			
		||||
## Proposing a (non-trivial) change
 | 
			
		||||
 | 
			
		||||
> By "non-trivial", we mean a change that is not a bug fix or small
 | 
			
		||||
> documentation update. If you are unsure, please ask us on our [Discord
 | 
			
		||||
> server](https://discord.gg/ollama).
 | 
			
		||||
 | 
			
		||||
Before opening a non-trivial Pull Request, please open an issue to discuss the change and
 | 
			
		||||
get feedback from the maintainers. This helps us understand the context of the
 | 
			
		||||
change and how it fits into Ollama's roadmap and prevents us from duplicating
 | 
			
		||||
work or you from spending time on a change that we may not be able to accept.
 | 
			
		||||
 | 
			
		||||
Tips for proposals:
 | 
			
		||||
 | 
			
		||||
* Explain the problem you are trying to solve, not what you are trying to do.
 | 
			
		||||
* Explain why the change is important.
 | 
			
		||||
* Explain how the change will be used.
 | 
			
		||||
* Explain how the change will be tested.
 | 
			
		||||
 | 
			
		||||
Additionally, for bonus points: Provide draft documentation you would expect to
 | 
			
		||||
see if the change were accepted.
 | 
			
		||||
 | 
			
		||||
## Pull requests
 | 
			
		||||
 | 
			
		||||
**Commit messages**
 | 
			
		||||
 | 
			
		||||
The title should look like:
 | 
			
		||||
 | 
			
		||||
    <package>: <short description>
 | 
			
		||||
 | 
			
		||||
The package is the most affected Go package. If the change does not affect Go
 | 
			
		||||
code, then use the directory name instead. Changes to a single well-known
 | 
			
		||||
file in the root directory may use the file name.
 | 
			
		||||
 | 
			
		||||
The short description should start with a lowercase letter and be a
 | 
			
		||||
continuation of the sentence:
 | 
			
		||||
 | 
			
		||||
      "This changes Ollama to..."
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
      llm/backend/mlx: support the llama architecture
 | 
			
		||||
      CONTRIBUTING: provide clairity on good commit messages, and bad
 | 
			
		||||
 | 
			
		||||
Bad Examples:
 | 
			
		||||
 | 
			
		||||
      feat: add more emoji
 | 
			
		||||
      fix: was not using famous web framework
 | 
			
		||||
      chore: generify code
 | 
			
		||||
 | 
			
		||||
**Tests**
 | 
			
		||||
 | 
			
		||||
Please include tests. Strive to test behavior, not implementation.
 | 
			
		||||
 | 
			
		||||
**New dependencies**
 | 
			
		||||
 | 
			
		||||
Dependencies should be added sparingly. If you are adding a new dependency,
 | 
			
		||||
please explain why it is necessary and what other ways you attempted that
 | 
			
		||||
did not work without it.
 | 
			
		||||
 | 
			
		||||
## Need help?
 | 
			
		||||
 | 
			
		||||
If you need help with anything, feel free to reach out to us on our [Discord server](https://discord.gg/ollama).
 | 
			
		||||
							
								
								
									
										140
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										140
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -1,29 +1,131 @@
 | 
			
		||||
FROM nvidia/cuda:11.8.0-devel-ubuntu22.04
 | 
			
		||||
# vim: filetype=dockerfile
 | 
			
		||||
 | 
			
		||||
ARG TARGETARCH
 | 
			
		||||
ARG GOFLAGS="'-ldflags=-w -s'"
 | 
			
		||||
ARG FLAVOR=${TARGETARCH}
 | 
			
		||||
 | 
			
		||||
WORKDIR /go/src/github.com/jmorganca/ollama
 | 
			
		||||
RUN apt-get update && apt-get install -y git build-essential cmake
 | 
			
		||||
ADD https://dl.google.com/go/go1.21.3.linux-$TARGETARCH.tar.gz /tmp/go1.21.3.tar.gz
 | 
			
		||||
RUN mkdir -p /usr/local && tar xz -C /usr/local </tmp/go1.21.3.tar.gz
 | 
			
		||||
ARG ROCMVERSION=6.3.3
 | 
			
		||||
ARG JETPACK5VERSION=r35.4.1
 | 
			
		||||
ARG JETPACK6VERSION=r36.4.0
 | 
			
		||||
ARG CMAKEVERSION=3.31.2
 | 
			
		||||
 | 
			
		||||
# CUDA v11 requires gcc v10.  v10.3 has regressions, so the rockylinux 8.5 AppStream has the latest compatible version
 | 
			
		||||
FROM --platform=linux/amd64 rocm/dev-almalinux-8:${ROCMVERSION}-complete AS base-amd64
 | 
			
		||||
RUN yum install -y yum-utils \
 | 
			
		||||
    && yum-config-manager --add-repo https://dl.rockylinux.org/vault/rocky/8.5/AppStream/\$basearch/os/ \
 | 
			
		||||
    && rpm --import https://dl.rockylinux.org/pub/rocky/RPM-GPG-KEY-Rocky-8 \
 | 
			
		||||
    && dnf install -y yum-utils ccache gcc-toolset-10-gcc-10.2.1-8.2.el8 gcc-toolset-10-gcc-c++-10.2.1-8.2.el8 gcc-toolset-10-binutils-2.35-11.el8 \
 | 
			
		||||
    && yum-config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel8/x86_64/cuda-rhel8.repo
 | 
			
		||||
ENV PATH=/opt/rh/gcc-toolset-10/root/usr/bin:$PATH
 | 
			
		||||
 | 
			
		||||
FROM --platform=linux/arm64 almalinux:8 AS base-arm64
 | 
			
		||||
# install epel-release for ccache
 | 
			
		||||
RUN yum install -y yum-utils epel-release \
 | 
			
		||||
    && dnf install -y clang ccache \
 | 
			
		||||
    && yum-config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel8/sbsa/cuda-rhel8.repo
 | 
			
		||||
ENV CC=clang CXX=clang++
 | 
			
		||||
 | 
			
		||||
FROM base-${TARGETARCH} AS base
 | 
			
		||||
ARG CMAKEVERSION
 | 
			
		||||
RUN curl -fsSL https://github.com/Kitware/CMake/releases/download/v${CMAKEVERSION}/cmake-${CMAKEVERSION}-linux-$(uname -m).tar.gz | tar xz -C /usr/local --strip-components 1
 | 
			
		||||
COPY CMakeLists.txt CMakePresets.json .
 | 
			
		||||
COPY ml/backend/ggml/ggml ml/backend/ggml/ggml
 | 
			
		||||
ENV LDFLAGS=-s
 | 
			
		||||
 | 
			
		||||
FROM base AS cpu
 | 
			
		||||
RUN dnf install -y gcc-toolset-11-gcc gcc-toolset-11-gcc-c++
 | 
			
		||||
ENV PATH=/opt/rh/gcc-toolset-11/root/usr/bin:$PATH
 | 
			
		||||
RUN --mount=type=cache,target=/root/.ccache \
 | 
			
		||||
    cmake --preset 'CPU' \
 | 
			
		||||
        && cmake --build --parallel --preset 'CPU' \
 | 
			
		||||
        && cmake --install build --component CPU --strip --parallel 8
 | 
			
		||||
 | 
			
		||||
FROM base AS cuda-11
 | 
			
		||||
ARG CUDA11VERSION=11.3
 | 
			
		||||
RUN dnf install -y cuda-toolkit-${CUDA11VERSION//./-}
 | 
			
		||||
ENV PATH=/usr/local/cuda-11/bin:$PATH
 | 
			
		||||
RUN --mount=type=cache,target=/root/.ccache \
 | 
			
		||||
    cmake --preset 'CUDA 11' \
 | 
			
		||||
        && cmake --build --parallel --preset 'CUDA 11' \
 | 
			
		||||
        && cmake --install build --component CUDA --strip --parallel 8
 | 
			
		||||
 | 
			
		||||
FROM base AS cuda-12
 | 
			
		||||
ARG CUDA12VERSION=12.8
 | 
			
		||||
RUN dnf install -y cuda-toolkit-${CUDA12VERSION//./-}
 | 
			
		||||
ENV PATH=/usr/local/cuda-12/bin:$PATH
 | 
			
		||||
RUN --mount=type=cache,target=/root/.ccache \
 | 
			
		||||
    cmake --preset 'CUDA 12' \
 | 
			
		||||
        && cmake --build --parallel --preset 'CUDA 12' \
 | 
			
		||||
        && cmake --install build --component CUDA --strip --parallel 8
 | 
			
		||||
 | 
			
		||||
FROM base AS rocm-6
 | 
			
		||||
ENV PATH=/opt/rocm/hcc/bin:/opt/rocm/hip/bin:/opt/rocm/bin:/opt/rocm/hcc/bin:$PATH
 | 
			
		||||
RUN --mount=type=cache,target=/root/.ccache \
 | 
			
		||||
    cmake --preset 'ROCm 6' \
 | 
			
		||||
        && cmake --build --parallel --preset 'ROCm 6' \
 | 
			
		||||
        && cmake --install build --component HIP --strip --parallel 8
 | 
			
		||||
 | 
			
		||||
FROM --platform=linux/arm64 nvcr.io/nvidia/l4t-jetpack:${JETPACK5VERSION} AS jetpack-5
 | 
			
		||||
ARG CMAKEVERSION
 | 
			
		||||
RUN apt-get update && apt-get install -y curl ccache \
 | 
			
		||||
    && curl -fsSL https://github.com/Kitware/CMake/releases/download/v${CMAKEVERSION}/cmake-${CMAKEVERSION}-linux-$(uname -m).tar.gz | tar xz -C /usr/local --strip-components 1
 | 
			
		||||
COPY CMakeLists.txt CMakePresets.json .
 | 
			
		||||
COPY ml/backend/ggml/ggml ml/backend/ggml/ggml
 | 
			
		||||
RUN --mount=type=cache,target=/root/.ccache \
 | 
			
		||||
    cmake --preset 'JetPack 5' \
 | 
			
		||||
        && cmake --build --parallel --preset 'JetPack 5' \
 | 
			
		||||
        && cmake --install build --component CUDA --strip --parallel 8
 | 
			
		||||
 | 
			
		||||
FROM --platform=linux/arm64 nvcr.io/nvidia/l4t-jetpack:${JETPACK6VERSION} AS jetpack-6
 | 
			
		||||
ARG CMAKEVERSION
 | 
			
		||||
RUN apt-get update && apt-get install -y curl ccache \
 | 
			
		||||
    && curl -fsSL https://github.com/Kitware/CMake/releases/download/v${CMAKEVERSION}/cmake-${CMAKEVERSION}-linux-$(uname -m).tar.gz | tar xz -C /usr/local --strip-components 1
 | 
			
		||||
COPY CMakeLists.txt CMakePresets.json .
 | 
			
		||||
COPY ml/backend/ggml/ggml ml/backend/ggml/ggml
 | 
			
		||||
RUN --mount=type=cache,target=/root/.ccache \
 | 
			
		||||
    cmake --preset 'JetPack 6' \
 | 
			
		||||
        && cmake --build --parallel --preset 'JetPack 6' \
 | 
			
		||||
        && cmake --install build --component CUDA --strip --parallel 8
 | 
			
		||||
 | 
			
		||||
FROM base AS build
 | 
			
		||||
WORKDIR /go/src/github.com/ollama/ollama
 | 
			
		||||
COPY go.mod go.sum .
 | 
			
		||||
RUN curl -fsSL https://golang.org/dl/go$(awk '/^go/ { print $2 }' go.mod).linux-$(case $(uname -m) in x86_64) echo amd64 ;; aarch64) echo arm64 ;; esac).tar.gz | tar xz -C /usr/local
 | 
			
		||||
ENV PATH=/usr/local/go/bin:$PATH
 | 
			
		||||
RUN go mod download
 | 
			
		||||
COPY . .
 | 
			
		||||
ENV GOARCH=$TARGETARCH
 | 
			
		||||
ENV GOFLAGS=$GOFLAGS
 | 
			
		||||
RUN /usr/local/go/bin/go generate ./... \
 | 
			
		||||
    && /usr/local/go/bin/go build .
 | 
			
		||||
ARG GOFLAGS="'-ldflags=-w -s'"
 | 
			
		||||
ENV CGO_ENABLED=1
 | 
			
		||||
RUN --mount=type=cache,target=/root/.cache/go-build \
 | 
			
		||||
    go build -trimpath -buildmode=pie -o /bin/ollama .
 | 
			
		||||
 | 
			
		||||
FROM ubuntu:22.04
 | 
			
		||||
RUN apt-get update && apt-get install -y ca-certificates
 | 
			
		||||
COPY --from=0 /go/src/github.com/jmorganca/ollama/ollama /bin/ollama
 | 
			
		||||
EXPOSE 11434
 | 
			
		||||
ENV OLLAMA_HOST 0.0.0.0
 | 
			
		||||
FROM --platform=linux/amd64 scratch AS amd64
 | 
			
		||||
COPY --from=cuda-11 dist/lib/ollama/cuda_v11 /lib/ollama/cuda_v11
 | 
			
		||||
COPY --from=cuda-12 dist/lib/ollama/cuda_v12 /lib/ollama/cuda_v12
 | 
			
		||||
 | 
			
		||||
# set some environment variable for better NVIDIA compatibility
 | 
			
		||||
ENV PATH=/usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
 | 
			
		||||
FROM --platform=linux/arm64 scratch AS arm64
 | 
			
		||||
COPY --from=cuda-11 dist/lib/ollama/cuda_v11 /lib/ollama/cuda_v11
 | 
			
		||||
COPY --from=cuda-12 dist/lib/ollama/cuda_v12 /lib/ollama/cuda_v12
 | 
			
		||||
COPY --from=jetpack-5 dist/lib/ollama/cuda_v11 /lib/ollama/cuda_jetpack5
 | 
			
		||||
COPY --from=jetpack-6 dist/lib/ollama/cuda_v12 /lib/ollama/cuda_jetpack6
 | 
			
		||||
 | 
			
		||||
FROM scratch AS rocm
 | 
			
		||||
COPY --from=rocm-6 dist/lib/ollama/rocm /lib/ollama/rocm
 | 
			
		||||
 | 
			
		||||
FROM ${FLAVOR} AS archive
 | 
			
		||||
COPY --from=cpu dist/lib/ollama /lib/ollama
 | 
			
		||||
COPY --from=build /bin/ollama /bin/ollama
 | 
			
		||||
 | 
			
		||||
FROM ubuntu:20.04
 | 
			
		||||
RUN apt-get update \
 | 
			
		||||
    && apt-get install -y ca-certificates \
 | 
			
		||||
    && apt-get clean \
 | 
			
		||||
    && rm -rf /var/lib/apt/lists/*
 | 
			
		||||
COPY --from=archive /bin /usr/bin
 | 
			
		||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
 | 
			
		||||
COPY --from=archive /lib/ollama /usr/lib/ollama
 | 
			
		||||
ENV LD_LIBRARY_PATH=/usr/local/nvidia/lib:/usr/local/nvidia/lib64
 | 
			
		||||
ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility
 | 
			
		||||
 | 
			
		||||
ENV NVIDIA_VISIBLE_DEVICES=all
 | 
			
		||||
ENV OLLAMA_HOST=0.0.0.0:11434
 | 
			
		||||
EXPOSE 11434
 | 
			
		||||
ENTRYPOINT ["/bin/ollama"]
 | 
			
		||||
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
 | 
			
		||||
							
								
								
									
										63
									
								
								Makefile.sync
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								Makefile.sync
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
UPSTREAM=https://github.com/ggerganov/llama.cpp.git
 | 
			
		||||
WORKDIR=llama/vendor
 | 
			
		||||
FETCH_HEAD=de4c07f93783a1a96456a44dc16b9db538ee1618
 | 
			
		||||
 | 
			
		||||
.PHONY: help
 | 
			
		||||
help:
 | 
			
		||||
	@echo "Available targets:"
 | 
			
		||||
	@echo "    sync                 Sync with upstream repositories"
 | 
			
		||||
	@echo "    checkout             Checkout upstream repository"
 | 
			
		||||
	@echo "    apply-patches        Apply patches to local repository"
 | 
			
		||||
	@echo "    format-patches       Format patches from local repository"
 | 
			
		||||
	@echo "    clean                Clean local repository"
 | 
			
		||||
	@echo
 | 
			
		||||
	@echo "Example:"
 | 
			
		||||
	@echo "    make -f $(lastword $(MAKEFILE_LIST)) clean sync"
 | 
			
		||||
 | 
			
		||||
.PHONY: sync
 | 
			
		||||
sync: llama/build-info.cpp ml/backend/ggml/ggml/src/ggml-metal/ggml-metal-embed.metal
 | 
			
		||||
 | 
			
		||||
llama/build-info.cpp: llama/build-info.cpp.in llama/llama.cpp
 | 
			
		||||
	sed -e 's|@FETCH_HEAD@|$(FETCH_HEAD)|' <$< >$@
 | 
			
		||||
 | 
			
		||||
ml/backend/ggml/ggml/src/ggml-metal/ggml-metal-embed.metal: ml/backend/ggml/ggml
 | 
			
		||||
	go generate ./$(@D)
 | 
			
		||||
 | 
			
		||||
.PHONY: llama/llama.cpp
 | 
			
		||||
llama/llama.cpp: llama/vendor/
 | 
			
		||||
	rsync -arvzc -f "merge $@/.rsync-filter" $< $@
 | 
			
		||||
 | 
			
		||||
.PHONY: ml/backend/ggml/ggml
 | 
			
		||||
ml/backend/ggml/ggml: llama/vendor/ggml/
 | 
			
		||||
	rsync -arvzc -f "merge $@/.rsync-filter" $< $@
 | 
			
		||||
 | 
			
		||||
PATCHES=$(wildcard llama/patches/*.patch)
 | 
			
		||||
PATCHED=$(join $(dir $(PATCHES)), $(addsuffix ed, $(addprefix ., $(notdir $(PATCHES)))))
 | 
			
		||||
 | 
			
		||||
.PHONY: apply-patches
 | 
			
		||||
.NOTPARALLEL:
 | 
			
		||||
apply-patches: $(PATCHED)
 | 
			
		||||
 | 
			
		||||
llama/patches/.%.patched: llama/patches/%.patch
 | 
			
		||||
	@if git -c user.name=nobody -c 'user.email=<>' -C $(WORKDIR) am -3 $(realpath $<); then touch $@; else git -C $(WORKDIR) am --abort; exit 1; fi
 | 
			
		||||
 | 
			
		||||
.PHONY: checkout
 | 
			
		||||
checkout: $(WORKDIR)
 | 
			
		||||
	git -C $(WORKDIR) fetch
 | 
			
		||||
	git -C $(WORKDIR) checkout -f $(FETCH_HEAD)
 | 
			
		||||
 | 
			
		||||
$(WORKDIR):
 | 
			
		||||
	git clone $(UPSTREAM) $(WORKDIR)
 | 
			
		||||
 | 
			
		||||
.PHONE: format-patches
 | 
			
		||||
format-patches: llama/patches
 | 
			
		||||
	git -C $(WORKDIR) format-patch \
 | 
			
		||||
		--no-signature \
 | 
			
		||||
		--no-numbered \
 | 
			
		||||
		--zero-commit \
 | 
			
		||||
		-o $(realpath $<) \
 | 
			
		||||
		$(FETCH_HEAD)
 | 
			
		||||
 | 
			
		||||
.PHONE: clean
 | 
			
		||||
clean: checkout
 | 
			
		||||
	$(RM) llama/patches/.*.patched
 | 
			
		||||
							
								
								
									
										451
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										451
									
								
								README.md
									
									
									
									
									
								
							@@ -1,67 +1,88 @@
 | 
			
		||||
<div align="center">
 | 
			
		||||
  <picture>
 | 
			
		||||
    <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>
 | 
			
		||||
  <a href="https://ollama.com">
 | 
			
		||||
    <img alt="ollama" height="200px" src="https://github.com/ollama/ollama/assets/3325447/0d0b44e2-8f4a-4e99-9b52-a5c1c741c8f7">
 | 
			
		||||
  </a>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
# Ollama
 | 
			
		||||
 | 
			
		||||
[](https://discord.gg/ollama)
 | 
			
		||||
 | 
			
		||||
Get up and running with large language models locally.
 | 
			
		||||
Get up and running with large language models.
 | 
			
		||||
 | 
			
		||||
### macOS
 | 
			
		||||
 | 
			
		||||
[Download](https://ollama.ai/download/Ollama-darwin.zip)
 | 
			
		||||
[Download](https://ollama.com/download/Ollama-darwin.zip)
 | 
			
		||||
 | 
			
		||||
### Windows
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
```shell
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
### Community
 | 
			
		||||
 | 
			
		||||
- [Discord](https://discord.gg/ollama)
 | 
			
		||||
- [Reddit](https://reddit.com/r/ollama)
 | 
			
		||||
 | 
			
		||||
## Quickstart
 | 
			
		||||
 | 
			
		||||
To run and chat with [Llama 2](https://ollama.ai/library/llama2):
 | 
			
		||||
To run and chat with [Llama 3.2](https://ollama.com/library/llama3.2):
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
ollama run llama2
 | 
			
		||||
```shell
 | 
			
		||||
ollama run llama3.2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 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                       |
 | 
			
		||||
| ------------------ | ---------- | ----- | ------------------------------ |
 | 
			
		||||
| Llama 2            | 7B         | 3.8GB | `ollama run llama2`            |
 | 
			
		||||
| 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`       |
 | 
			
		||||
| Starling           | 7B         | 4.1GB | `ollama run starling-lm`       |
 | 
			
		||||
| Code Llama         | 7B         | 3.8GB | `ollama run codellama`         |
 | 
			
		||||
| 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`             |
 | 
			
		||||
| Model              | Parameters | Size  | Download                         |
 | 
			
		||||
| ------------------ | ---------- | ----- | -------------------------------- |
 | 
			
		||||
| Gemma 3            | 1B         | 815MB | `ollama run gemma3:1b`           |
 | 
			
		||||
| Gemma 3            | 4B         | 3.3GB | `ollama run gemma3`              |
 | 
			
		||||
| Gemma 3            | 12B        | 8.1GB | `ollama run gemma3:12b`          |
 | 
			
		||||
| Gemma 3            | 27B        | 17GB  | `ollama run gemma3:27b`          |
 | 
			
		||||
| QwQ                | 32B        | 20GB  | `ollama run qwq`                 |
 | 
			
		||||
| DeepSeek-R1        | 7B         | 4.7GB | `ollama run deepseek-r1`         |
 | 
			
		||||
| DeepSeek-R1        | 671B       | 404GB | `ollama run deepseek-r1:671b`    |
 | 
			
		||||
| Llama 4            | 109B       | 67GB  | `ollama run llama4:scout`        |
 | 
			
		||||
| Llama 4            | 400B       | 245GB | `ollama run llama4:maverick`     |
 | 
			
		||||
| Llama 3.3          | 70B        | 43GB  | `ollama run llama3.3`            |
 | 
			
		||||
| Llama 3.2          | 3B         | 2.0GB | `ollama run llama3.2`            |
 | 
			
		||||
| Llama 3.2          | 1B         | 1.3GB | `ollama run llama3.2:1b`         |
 | 
			
		||||
| Llama 3.2 Vision   | 11B        | 7.9GB | `ollama run llama3.2-vision`     |
 | 
			
		||||
| Llama 3.2 Vision   | 90B        | 55GB  | `ollama run llama3.2-vision:90b` |
 | 
			
		||||
| Llama 3.1          | 8B         | 4.7GB | `ollama run llama3.1`            |
 | 
			
		||||
| Llama 3.1          | 405B       | 231GB | `ollama run llama3.1:405b`       |
 | 
			
		||||
| Phi 4              | 14B        | 9.1GB | `ollama run phi4`                |
 | 
			
		||||
| Phi 4 Mini         | 3.8B       | 2.5GB | `ollama run phi4-mini`           |
 | 
			
		||||
| Mistral            | 7B         | 4.1GB | `ollama run mistral`             |
 | 
			
		||||
| Moondream 2        | 1.4B       | 829MB | `ollama run moondream`           |
 | 
			
		||||
| Neural Chat        | 7B         | 4.1GB | `ollama run neural-chat`         |
 | 
			
		||||
| Starling           | 7B         | 4.1GB | `ollama run starling-lm`         |
 | 
			
		||||
| Code Llama         | 7B         | 3.8GB | `ollama run codellama`           |
 | 
			
		||||
| Llama 2 Uncensored | 7B         | 3.8GB | `ollama run llama2-uncensored`   |
 | 
			
		||||
| LLaVA              | 7B         | 4.5GB | `ollama run llava`               |
 | 
			
		||||
| Granite-3.3         | 8B         | 4.9GB | `ollama run granite3.3`          |
 | 
			
		||||
 | 
			
		||||
> 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.
 | 
			
		||||
 | 
			
		||||
## Customize a model
 | 
			
		||||
 | 
			
		||||
@@ -77,32 +98,32 @@ Ollama supports importing GGUF models in the Modelfile:
 | 
			
		||||
 | 
			
		||||
2. Create the model in Ollama
 | 
			
		||||
 | 
			
		||||
   ```
 | 
			
		||||
   ```shell
 | 
			
		||||
   ollama create example -f Modelfile
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
3. Run the model
 | 
			
		||||
 | 
			
		||||
   ```
 | 
			
		||||
   ```shell
 | 
			
		||||
   ollama run example
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
### Import from PyTorch or Safetensors
 | 
			
		||||
### Import from Safetensors
 | 
			
		||||
 | 
			
		||||
See the [guide](docs/import.md) on importing models for more information.
 | 
			
		||||
 | 
			
		||||
### 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.2` model:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
ollama pull llama2
 | 
			
		||||
```shell
 | 
			
		||||
ollama pull llama3.2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Create a `Modelfile`:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
FROM llama2
 | 
			
		||||
FROM llama3.2
 | 
			
		||||
 | 
			
		||||
# set the temperature to 1 [higher is more creative, lower is more coherent]
 | 
			
		||||
PARAMETER temperature 1
 | 
			
		||||
@@ -122,7 +143,7 @@ ollama run mario
 | 
			
		||||
Hello! It's your friend Mario.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
For more examples, see the [examples](examples) directory. For more information on working with a Modelfile, see the [Modelfile](docs/modelfile.md) documentation.
 | 
			
		||||
For more information on working with a Modelfile, see the [Modelfile](docs/modelfile.md) documentation.
 | 
			
		||||
 | 
			
		||||
## CLI Reference
 | 
			
		||||
 | 
			
		||||
@@ -130,28 +151,28 @@ For more examples, see the [examples](examples) directory. For more information
 | 
			
		||||
 | 
			
		||||
`ollama create` is used to create a model from a Modelfile.
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
```shell
 | 
			
		||||
ollama create mymodel -f ./Modelfile
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Pull a model
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
ollama pull llama2
 | 
			
		||||
```shell
 | 
			
		||||
ollama pull llama3.2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
> This command can also be used to update a local model. Only the diff will be pulled.
 | 
			
		||||
 | 
			
		||||
### Remove a model
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
ollama rm llama2
 | 
			
		||||
```shell
 | 
			
		||||
ollama rm llama3.2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Copy a model
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
ollama cp llama2 my-llama2
 | 
			
		||||
```shell
 | 
			
		||||
ollama cp llama3.2 my-model
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Multiline input
 | 
			
		||||
@@ -168,58 +189,63 @@ I'm a basic program that prints the famous "Hello, world!" message to the consol
 | 
			
		||||
### Multimodal models
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
>>> What's in this image? /Users/jmorgan/Desktop/smile.png
 | 
			
		||||
The image features a yellow smiley face, which is likely the central focus of the picture.
 | 
			
		||||
ollama run llava "What's in this image? /Users/jmorgan/Desktop/smile.png"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Pass in prompt as arguments
 | 
			
		||||
> **Output**: The image features a yellow smiley face, which is likely the central focus of the picture.
 | 
			
		||||
 | 
			
		||||
### Pass the prompt as an argument
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
ollama run llama3.2 "Summarize this file: $(cat README.md)"
 | 
			
		||||
```
 | 
			
		||||
$ ollama run llama2 "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.
 | 
			
		||||
 | 
			
		||||
> **Output**: 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.
 | 
			
		||||
 | 
			
		||||
### Show model information
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
ollama show llama3.2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### List models on your computer
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
```shell
 | 
			
		||||
ollama list
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### List which models are currently loaded
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
ollama ps
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Stop a model which is currently running
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
ollama stop llama3.2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Start Ollama
 | 
			
		||||
 | 
			
		||||
`ollama serve` is used when you want to start ollama without running the desktop application.
 | 
			
		||||
 | 
			
		||||
## Building
 | 
			
		||||
 | 
			
		||||
Install `cmake` and `go`:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
brew install cmake go
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Then generate dependencies:
 | 
			
		||||
```
 | 
			
		||||
go generate ./...
 | 
			
		||||
```
 | 
			
		||||
Then build the binary:
 | 
			
		||||
```
 | 
			
		||||
go build .
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
More detailed instructions can be found in the [developer guide](https://github.com/jmorganca/ollama/blob/main/docs/development.md)
 | 
			
		||||
 | 
			
		||||
See the [developer guide](https://github.com/ollama/ollama/blob/main/docs/development.md)
 | 
			
		||||
 | 
			
		||||
### Running local builds
 | 
			
		||||
 | 
			
		||||
Next, start the server:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
```shell
 | 
			
		||||
./ollama serve
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Finally, in a separate shell, run a model:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
./ollama run llama2
 | 
			
		||||
```shell
 | 
			
		||||
./ollama run llama3.2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## REST API
 | 
			
		||||
@@ -228,18 +254,18 @@ Ollama has a REST API for running and managing models.
 | 
			
		||||
 | 
			
		||||
### Generate a response
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
```shell
 | 
			
		||||
curl http://localhost:11434/api/generate -d '{
 | 
			
		||||
  "model": "llama2",
 | 
			
		||||
  "model": "llama3.2",
 | 
			
		||||
  "prompt":"Why is the sky blue?"
 | 
			
		||||
}'
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Chat with a model
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
```shell
 | 
			
		||||
curl http://localhost:11434/api/chat -d '{
 | 
			
		||||
  "model": "mistral",
 | 
			
		||||
  "model": "llama3.2",
 | 
			
		||||
  "messages": [
 | 
			
		||||
    { "role": "user", "content": "why is the sky blue?" }
 | 
			
		||||
  ]
 | 
			
		||||
@@ -248,57 +274,224 @@ curl http://localhost:11434/api/chat -d '{
 | 
			
		||||
 | 
			
		||||
See the [API documentation](./docs/api.md) for all endpoints.
 | 
			
		||||
 | 
			
		||||
## Integrations
 | 
			
		||||
 | 
			
		||||
- [ollama-python](https://github.com/jmorganca/ollama-python)
 | 
			
		||||
 | 
			
		||||
## Community Integrations
 | 
			
		||||
 | 
			
		||||
### Web & Desktop
 | 
			
		||||
 | 
			
		||||
- [Open WebUI](https://github.com/open-webui/open-webui)
 | 
			
		||||
- [SwiftChat (macOS with ReactNative)](https://github.com/aws-samples/swift-chat)
 | 
			
		||||
- [Enchanted (macOS native)](https://github.com/AugustDev/enchanted)
 | 
			
		||||
- [Hollama](https://github.com/fmaclen/hollama)
 | 
			
		||||
- [Lollms-Webui](https://github.com/ParisNeo/lollms-webui)
 | 
			
		||||
- [LibreChat](https://github.com/danny-avila/LibreChat)
 | 
			
		||||
- [Bionic GPT](https://github.com/bionic-gpt/bionic-gpt)
 | 
			
		||||
- [HTML UI](https://github.com/rtcfirefly/ollama-ui)
 | 
			
		||||
- [Saddle](https://github.com/jikkuatwork/saddle)
 | 
			
		||||
- [TagSpaces](https://www.tagspaces.org) (A platform for file-based apps, [utilizing Ollama](https://docs.tagspaces.org/ai/) for the generation of tags and descriptions)
 | 
			
		||||
- [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)
 | 
			
		||||
- [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)
 | 
			
		||||
- [big-AGI](https://github.com/enricoros/big-agi/blob/main/docs/config-ollama.md)
 | 
			
		||||
- [big-AGI](https://github.com/enricoros/big-AGI)
 | 
			
		||||
- [Cheshire Cat assistant framework](https://github.com/cheshire-cat-ai/core)
 | 
			
		||||
- [Amica](https://github.com/semperai/amica)
 | 
			
		||||
- [chatd](https://github.com/BruceMacD/chatd)
 | 
			
		||||
- [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](https://github.com/mrdjohnson/llm-x) (Progressive Web App)
 | 
			
		||||
- [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)
 | 
			
		||||
- [IntelliBar](https://intellibar.app/) (AI-powered assistant for macOS)
 | 
			
		||||
- [Jirapt](https://github.com/AliAhmedNada/jirapt) (Jira Integration to generate issues, tasks, epics)
 | 
			
		||||
- [ojira](https://github.com/AliAhmedNada/ojira) (Jira chrome plugin to easily generate descriptions for tasks)
 | 
			
		||||
- [QA-Pilot](https://github.com/reid41/QA-Pilot) (Interactive chat tool that can leverage Ollama models for rapid understanding and navigation of GitHub code repositories)
 | 
			
		||||
- [ChatOllama](https://github.com/sugarforever/chat-ollama) (Open Source Chatbot based on Ollama with Knowledge Bases)
 | 
			
		||||
- [CRAG Ollama Chat](https://github.com/Nagi-ovo/CRAG-Ollama-Chat) (Simple Web Search with Corrective RAG)
 | 
			
		||||
- [RAGFlow](https://github.com/infiniflow/ragflow) (Open-source Retrieval-Augmented Generation engine based on deep document understanding)
 | 
			
		||||
- [StreamDeploy](https://github.com/StreamDeploy-DevRel/streamdeploy-llm-app-scaffold) (LLM Application Scaffold)
 | 
			
		||||
- [chat](https://github.com/swuecho/chat) (chat web app for teams)
 | 
			
		||||
- [Lobe Chat](https://github.com/lobehub/lobe-chat) with [Integrating Doc](https://lobehub.com/docs/self-hosting/examples/ollama)
 | 
			
		||||
- [Ollama RAG Chatbot](https://github.com/datvodinh/rag-chatbot.git) (Local Chat with multiple PDFs using Ollama and RAG)
 | 
			
		||||
- [BrainSoup](https://www.nurgo-software.com/products/brainsoup) (Flexible native client with RAG & multi-agent automation)
 | 
			
		||||
- [macai](https://github.com/Renset/macai) (macOS client for Ollama, ChatGPT, and other compatible API back-ends)
 | 
			
		||||
- [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) (RWKV offline LLM deployment tool, also usable as a client for ChatGPT and Ollama)
 | 
			
		||||
- [Ollama Grid Search](https://github.com/dezoito/ollama-grid-search) (app to evaluate and compare models)
 | 
			
		||||
- [Olpaka](https://github.com/Otacon/olpaka) (User-friendly Flutter Web App for Ollama)
 | 
			
		||||
- [Casibase](https://casibase.org) (An open source AI knowledge base and dialogue system combining the latest RAG, SSO, ollama support, and multiple large language models.)
 | 
			
		||||
- [OllamaSpring](https://github.com/CrazyNeil/OllamaSpring) (Ollama Client for macOS)
 | 
			
		||||
- [LLocal.in](https://github.com/kartikm7/llocal) (Easy to use Electron Desktop Client for Ollama)
 | 
			
		||||
- [Shinkai Desktop](https://github.com/dcSpark/shinkai-apps) (Two click install Local AI using Ollama + Files + RAG)
 | 
			
		||||
- [AiLama](https://github.com/zeyoyt/ailama) (A Discord User App that allows you to interact with Ollama anywhere in Discord)
 | 
			
		||||
- [Ollama with Google Mesop](https://github.com/rapidarchitect/ollama_mesop/) (Mesop Chat Client implementation with Ollama)
 | 
			
		||||
- [R2R](https://github.com/SciPhi-AI/R2R) (Open-source RAG engine)
 | 
			
		||||
- [Ollama-Kis](https://github.com/elearningshow/ollama-kis) (A simple easy-to-use GUI with sample custom LLM for Drivers Education)
 | 
			
		||||
- [OpenGPA](https://opengpa.org) (Open-source offline-first Enterprise Agentic Application)
 | 
			
		||||
- [Painting Droid](https://github.com/mateuszmigas/painting-droid) (Painting app with AI integrations)
 | 
			
		||||
- [Kerlig AI](https://www.kerlig.com/) (AI writing assistant for macOS)
 | 
			
		||||
- [AI Studio](https://github.com/MindWorkAI/AI-Studio)
 | 
			
		||||
- [Sidellama](https://github.com/gyopak/sidellama) (browser-based LLM client)
 | 
			
		||||
- [LLMStack](https://github.com/trypromptly/LLMStack) (No-code multi-agent framework to build LLM agents and workflows)
 | 
			
		||||
- [BoltAI for Mac](https://boltai.com) (AI Chat Client for Mac)
 | 
			
		||||
- [Harbor](https://github.com/av/harbor) (Containerized LLM Toolkit with Ollama as default backend)
 | 
			
		||||
- [PyGPT](https://github.com/szczyglis-dev/py-gpt) (AI desktop assistant for Linux, Windows, and Mac)
 | 
			
		||||
- [Alpaca](https://github.com/Jeffser/Alpaca) (An Ollama client application for Linux and macOS made with GTK4 and Adwaita)
 | 
			
		||||
- [AutoGPT](https://github.com/Significant-Gravitas/AutoGPT/blob/master/docs/content/platform/ollama.md) (AutoGPT Ollama integration)
 | 
			
		||||
- [Go-CREW](https://www.jonathanhecl.com/go-crew/) (Powerful Offline RAG in Golang)
 | 
			
		||||
- [PartCAD](https://github.com/openvmp/partcad/) (CAD model generation with OpenSCAD and CadQuery)
 | 
			
		||||
- [Ollama4j Web UI](https://github.com/ollama4j/ollama4j-web-ui) - Java-based Web UI for Ollama built with Vaadin, Spring Boot, and Ollama4j
 | 
			
		||||
- [PyOllaMx](https://github.com/kspviswa/pyOllaMx) - macOS application capable of chatting with both Ollama and Apple MLX models.
 | 
			
		||||
- [Cline](https://github.com/cline/cline) - Formerly known as Claude Dev is a VSCode extension for multi-file/whole-repo coding
 | 
			
		||||
- [Cherry Studio](https://github.com/kangfenmao/cherry-studio) (Desktop client with Ollama support)
 | 
			
		||||
- [ConfiChat](https://github.com/1runeberg/confichat) (Lightweight, standalone, multi-platform, and privacy-focused LLM chat interface with optional encryption)
 | 
			
		||||
- [Archyve](https://github.com/nickthecook/archyve) (RAG-enabling document library)
 | 
			
		||||
- [crewAI with Mesop](https://github.com/rapidarchitect/ollama-crew-mesop) (Mesop Web Interface to run crewAI with Ollama)
 | 
			
		||||
- [Tkinter-based client](https://github.com/chyok/ollama-gui) (Python tkinter-based Client for Ollama)
 | 
			
		||||
- [LLMChat](https://github.com/trendy-design/llmchat) (Privacy focused, 100% local, intuitive all-in-one chat interface)
 | 
			
		||||
- [Local Multimodal AI Chat](https://github.com/Leon-Sander/Local-Multimodal-AI-Chat) (Ollama-based LLM Chat with support for multiple features, including PDF RAG, voice chat, image-based interactions, and integration with OpenAI.)
 | 
			
		||||
- [ARGO](https://github.com/xark-argo/argo) (Locally download and run Ollama and Huggingface models with RAG on Mac/Windows/Linux)
 | 
			
		||||
- [OrionChat](https://github.com/EliasPereirah/OrionChat) - OrionChat is a web interface for chatting with different AI providers
 | 
			
		||||
- [G1](https://github.com/bklieger-groq/g1) (Prototype of using prompting strategies to improve the LLM's reasoning through o1-like reasoning chains.)
 | 
			
		||||
- [Web management](https://github.com/lemonit-eric-mao/ollama-web-management) (Web management page)
 | 
			
		||||
- [Promptery](https://github.com/promptery/promptery) (desktop client for Ollama.)
 | 
			
		||||
- [Ollama App](https://github.com/JHubi1/ollama-app) (Modern and easy-to-use multi-platform client for Ollama)
 | 
			
		||||
- [chat-ollama](https://github.com/annilq/chat-ollama) (a React Native client for Ollama)
 | 
			
		||||
- [SpaceLlama](https://github.com/tcsenpai/spacellama) (Firefox and Chrome extension to quickly summarize web pages with ollama in a sidebar)
 | 
			
		||||
- [YouLama](https://github.com/tcsenpai/youlama) (Webapp to quickly summarize any YouTube video, supporting Invidious as well)
 | 
			
		||||
- [DualMind](https://github.com/tcsenpai/dualmind) (Experimental app allowing two models to talk to each other in the terminal or in a web interface)
 | 
			
		||||
- [ollamarama-matrix](https://github.com/h1ddenpr0cess20/ollamarama-matrix) (Ollama chatbot for the Matrix chat protocol)
 | 
			
		||||
- [ollama-chat-app](https://github.com/anan1213095357/ollama-chat-app) (Flutter-based chat app)
 | 
			
		||||
- [Perfect Memory AI](https://www.perfectmemory.ai/) (Productivity AI assists personalized by what you have seen on your screen, heard, and said in the meetings)
 | 
			
		||||
- [Hexabot](https://github.com/hexastack/hexabot) (A conversational AI builder)
 | 
			
		||||
- [Reddit Rate](https://github.com/rapidarchitect/reddit_analyzer) (Search and Rate Reddit topics with a weighted summation)
 | 
			
		||||
- [OpenTalkGpt](https://github.com/adarshM84/OpenTalkGpt) (Chrome Extension to manage open-source models supported by Ollama, create custom models, and chat with models from a user-friendly UI)
 | 
			
		||||
- [VT](https://github.com/vinhnx/vt.ai) (A minimal multimodal AI chat app, with dynamic conversation routing. Supports local models via Ollama)
 | 
			
		||||
- [Nosia](https://github.com/nosia-ai/nosia) (Easy to install and use RAG platform based on Ollama)
 | 
			
		||||
- [Witsy](https://github.com/nbonamy/witsy) (An AI Desktop application available for Mac/Windows/Linux)
 | 
			
		||||
- [Abbey](https://github.com/US-Artificial-Intelligence/abbey) (A configurable AI interface server with notebooks, document storage, and YouTube support)
 | 
			
		||||
- [Minima](https://github.com/dmayboroda/minima) (RAG with on-premises or fully local workflow)
 | 
			
		||||
- [aidful-ollama-model-delete](https://github.com/AidfulAI/aidful-ollama-model-delete) (User interface for simplified model cleanup)
 | 
			
		||||
- [Perplexica](https://github.com/ItzCrazyKns/Perplexica) (An AI-powered search engine & an open-source alternative to Perplexity AI)
 | 
			
		||||
- [Ollama Chat WebUI for Docker ](https://github.com/oslook/ollama-webui) (Support for local docker deployment, lightweight ollama webui)
 | 
			
		||||
- [AI Toolkit for Visual Studio Code](https://aka.ms/ai-tooklit/ollama-docs) (Microsoft-official VSCode extension to chat, test, evaluate models with Ollama support, and use them in your AI applications.)
 | 
			
		||||
- [MinimalNextOllamaChat](https://github.com/anilkay/MinimalNextOllamaChat) (Minimal Web UI for Chat and Model Control)
 | 
			
		||||
- [Chipper](https://github.com/TilmanGriesel/chipper) AI interface for tinkerers (Ollama, Haystack RAG, Python)
 | 
			
		||||
- [ChibiChat](https://github.com/CosmicEventHorizon/ChibiChat) (Kotlin-based Android app to chat with Ollama and Koboldcpp API endpoints)
 | 
			
		||||
- [LocalLLM](https://github.com/qusaismael/localllm) (Minimal Web-App to run ollama models on it with a GUI)
 | 
			
		||||
- [Ollamazing](https://github.com/buiducnhat/ollamazing) (Web extension to run Ollama models)
 | 
			
		||||
- [OpenDeepResearcher-via-searxng](https://github.com/benhaotang/OpenDeepResearcher-via-searxng) (A Deep Research equivalent endpoint with Ollama support for running locally)
 | 
			
		||||
- [AntSK](https://github.com/AIDotNet/AntSK) (Out-of-the-box & Adaptable RAG Chatbot)
 | 
			
		||||
- [MaxKB](https://github.com/1Panel-dev/MaxKB/) (Ready-to-use & flexible RAG Chatbot)
 | 
			
		||||
- [yla](https://github.com/danielekp/yla) (Web interface to freely interact with your customized models)
 | 
			
		||||
- [LangBot](https://github.com/RockChinQ/LangBot) (LLM-based instant messaging bots platform, with Agents, RAG features, supports multiple platforms)
 | 
			
		||||
- [1Panel](https://github.com/1Panel-dev/1Panel/) (Web-based Linux Server Management Tool)
 | 
			
		||||
- [AstrBot](https://github.com/Soulter/AstrBot/) (User-friendly LLM-based multi-platform chatbot with a WebUI, supporting RAG, LLM agents, and plugins integration)
 | 
			
		||||
- [Reins](https://github.com/ibrahimcetin/reins) (Easily tweak parameters, customize system prompts per chat, and enhance your AI experiments with reasoning model support.)
 | 
			
		||||
- [Flufy](https://github.com/Aharon-Bensadoun/Flufy) (A beautiful chat interface for interacting with Ollama's API. Built with React, TypeScript, and Material-UI.)
 | 
			
		||||
- [Ellama](https://github.com/zeozeozeo/ellama) (Friendly native app to chat with an Ollama instance)
 | 
			
		||||
- [screenpipe](https://github.com/mediar-ai/screenpipe) Build agents powered by your screen history
 | 
			
		||||
- [Ollamb](https://github.com/hengkysteen/ollamb) (Simple yet rich in features, cross-platform built with Flutter and designed for Ollama. Try the [web demo](https://hengkysteen.github.io/demo/ollamb/).)
 | 
			
		||||
- [Writeopia](https://github.com/Writeopia/Writeopia) (Text editor with integration with Ollama)
 | 
			
		||||
- [AppFlowy](https://github.com/AppFlowy-IO/AppFlowy) (AI collaborative workspace with Ollama, cross-platform and self-hostable)
 | 
			
		||||
- [Lumina](https://github.com/cushydigit/lumina.git) (A lightweight, minimal React.js frontend for interacting with Ollama servers)
 | 
			
		||||
 | 
			
		||||
### Cloud
 | 
			
		||||
 | 
			
		||||
- [Google Cloud](https://cloud.google.com/run/docs/tutorials/gpu-gemma2-with-ollama)
 | 
			
		||||
- [Fly.io](https://fly.io/docs/python/do-more/add-ollama/)
 | 
			
		||||
- [Koyeb](https://www.koyeb.com/deploy/ollama)
 | 
			
		||||
 | 
			
		||||
### Terminal
 | 
			
		||||
 | 
			
		||||
- [oterm](https://github.com/ggozad/oterm)
 | 
			
		||||
- [Ellama Emacs client](https://github.com/s-kostyaev/ellama)
 | 
			
		||||
- [Emacs client](https://github.com/zweifisch/ollama)
 | 
			
		||||
- [neollama](https://github.com/paradoxical-dev/neollama) UI client for interacting with models from within Neovim
 | 
			
		||||
- [gen.nvim](https://github.com/David-Kunz/gen.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)
 | 
			
		||||
- [gptel Emacs client](https://github.com/karthink/gptel)
 | 
			
		||||
- [Oatmeal](https://github.com/dustinblackman/oatmeal)
 | 
			
		||||
- [cmdh](https://github.com/pgibler/cmdh)
 | 
			
		||||
- [ooo](https://github.com/npahlfer/ooo)
 | 
			
		||||
- [shell-pilot](https://github.com/reid41/shell-pilot)(Interact with models via pure shell scripts on Linux or macOS)
 | 
			
		||||
- [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)
 | 
			
		||||
- [gollama](https://github.com/sammcj/gollama)
 | 
			
		||||
- [ParLlama](https://github.com/paulrobello/parllama)
 | 
			
		||||
- [Ollama eBook Summary](https://github.com/cognitivetech/ollama-ebook-summary/)
 | 
			
		||||
- [Ollama Mixture of Experts (MOE) in 50 lines of code](https://github.com/rapidarchitect/ollama_moe)
 | 
			
		||||
- [vim-intelligence-bridge](https://github.com/pepo-ec/vim-intelligence-bridge) Simple interaction of "Ollama" with the Vim editor
 | 
			
		||||
- [x-cmd ollama](https://x-cmd.com/mod/ollama)
 | 
			
		||||
- [bb7](https://github.com/drunkwcodes/bb7)
 | 
			
		||||
- [SwollamaCLI](https://github.com/marcusziade/Swollama) bundled with the Swollama Swift package. [Demo](https://github.com/marcusziade/Swollama?tab=readme-ov-file#cli-usage)
 | 
			
		||||
- [aichat](https://github.com/sigoden/aichat) All-in-one LLM CLI tool featuring Shell Assistant, Chat-REPL, RAG, AI tools & agents, with access to OpenAI, Claude, Gemini, Ollama, Groq, and more.
 | 
			
		||||
- [PowershAI](https://github.com/rrg92/powershai) PowerShell module that brings AI to terminal on Windows, including support for Ollama
 | 
			
		||||
- [DeepShell](https://github.com/Abyss-c0re/deepshell) Your self-hosted AI assistant. Interactive Shell, Files and Folders analysis.
 | 
			
		||||
- [orbiton](https://github.com/xyproto/orbiton) Configuration-free text editor and IDE with support for tab completion with Ollama.
 | 
			
		||||
- [orca-cli](https://github.com/molbal/orca-cli) Ollama Registry CLI Application - Browse, pull, and download models from Ollama Registry in your terminal.
 | 
			
		||||
- [GGUF-to-Ollama](https://github.com/jonathanhecl/gguf-to-ollama) - Importing GGUF to Ollama made easy (multiplatform)
 | 
			
		||||
 | 
			
		||||
### Apple Vision Pro
 | 
			
		||||
 | 
			
		||||
- [SwiftChat](https://github.com/aws-samples/swift-chat) (Cross-platform AI chat app supporting Apple Vision Pro via "Designed for iPad")
 | 
			
		||||
- [Enchanted](https://github.com/AugustDev/enchanted)
 | 
			
		||||
 | 
			
		||||
### Database
 | 
			
		||||
 | 
			
		||||
- [MindsDB](https://github.com/mindsdb/mindsdb/blob/staging/mindsdb/integrations/handlers/ollama_handler/README.md)
 | 
			
		||||
- [pgai](https://github.com/timescale/pgai) - PostgreSQL as a vector database (Create and search embeddings from Ollama models using pgvector)
 | 
			
		||||
   - [Get started guide](https://github.com/timescale/pgai/blob/main/docs/vectorizer-quick-start.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)
 | 
			
		||||
- [Kangaroo](https://github.com/dbkangaroo/kangaroo) (AI-powered SQL client and admin tool for popular databases)
 | 
			
		||||
 | 
			
		||||
### Package managers
 | 
			
		||||
 | 
			
		||||
- [Pacman](https://archlinux.org/packages/extra/x86_64/ollama/)
 | 
			
		||||
- [Gentoo](https://github.com/gentoo/guru/tree/master/app-misc/ollama)
 | 
			
		||||
- [Homebrew](https://formulae.brew.sh/formula/ollama)
 | 
			
		||||
- [Helm Chart](https://artifacthub.io/packages/helm/ollama-helm/ollama)
 | 
			
		||||
- [Guix channel](https://codeberg.org/tusharhero/ollama-guix)
 | 
			
		||||
- [Nix package](https://search.nixos.org/packages?show=ollama&from=0&size=50&sort=relevance&type=packages&query=ollama)
 | 
			
		||||
- [Flox](https://flox.dev/blog/ollama-part-one)
 | 
			
		||||
 | 
			
		||||
### 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/chat/ollama/) and [LangChain.js](https://js.langchain.com/docs/integrations/chat/ollama/) with [example](https://js.langchain.com/docs/tutorials/local_rag/)
 | 
			
		||||
- [Firebase Genkit](https://firebase.google.com/docs/genkit/plugins/ollama)
 | 
			
		||||
- [crewAI](https://github.com/crewAIInc/crewAI)
 | 
			
		||||
- [Yacana](https://remembersoftwares.github.io/yacana/) (User-friendly multi-agent framework for brainstorming and executing predetermined flows with built-in tool integration)
 | 
			
		||||
- [Spring AI](https://github.com/spring-projects/spring-ai) with [reference](https://docs.spring.io/spring-ai/reference/api/chat/ollama-chat.html) and [example](https://github.com/tzolov/ollama-tools)
 | 
			
		||||
- [LangChainGo](https://github.com/tmc/langchaingo/) with [example](https://github.com/tmc/langchaingo/tree/main/examples/ollama-completion-example)
 | 
			
		||||
- [LlamaIndex](https://gpt-index.readthedocs.io/en/stable/examples/llm/ollama.html)
 | 
			
		||||
- [LangChain4j](https://github.com/langchain4j/langchain4j) with [example](https://github.com/langchain4j/langchain4j-examples/tree/main/ollama-examples/src/main/java)
 | 
			
		||||
- [LangChainRust](https://github.com/Abraxas-365/langchain-rust) with [example](https://github.com/Abraxas-365/langchain-rust/blob/main/examples/llm_ollama.rs)
 | 
			
		||||
- [LangChain for .NET](https://github.com/tryAGI/LangChain) with [example](https://github.com/tryAGI/LangChain/blob/main/examples/LangChain.Samples.OpenAI/Program.cs)
 | 
			
		||||
- [LLPhant](https://github.com/theodo-group/LLPhant?tab=readme-ov-file#ollama)
 | 
			
		||||
- [LlamaIndex](https://docs.llamaindex.ai/en/stable/examples/llm/ollama/) and [LlamaIndexTS](https://ts.llamaindex.ai/modules/llms/available_llms/ollama)
 | 
			
		||||
- [LiteLLM](https://github.com/BerriAI/litellm)
 | 
			
		||||
- [OllamaFarm for Go](https://github.com/presbrey/ollamafarm)
 | 
			
		||||
- [OllamaSharp for .NET](https://github.com/awaescher/OllamaSharp)
 | 
			
		||||
- [Ollama for Ruby](https://github.com/gbaptista/ollama-ai)
 | 
			
		||||
- [Ollama-rs for Rust](https://github.com/pepperoni21/ollama-rs)
 | 
			
		||||
- [Ollama4j for Java](https://github.com/amithkoujalgi/ollama4j)
 | 
			
		||||
- [Ollama-hpp for C++](https://github.com/jmont-dev/ollama-hpp)
 | 
			
		||||
- [Ollama4j for Java](https://github.com/ollama4j/ollama4j)
 | 
			
		||||
- [ModelFusion Typescript Library](https://modelfusion.dev/integration/model-provider/ollama)
 | 
			
		||||
- [OllamaKit for Swift](https://github.com/kevinhermawan/OllamaKit)
 | 
			
		||||
- [Ollama for Dart](https://github.com/breitburg/dart-ollama)
 | 
			
		||||
@@ -306,25 +499,101 @@ See the [API documentation](./docs/api.md) for all endpoints.
 | 
			
		||||
- [LangChainDart](https://github.com/davidmigloz/langchain_dart)
 | 
			
		||||
- [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)
 | 
			
		||||
 | 
			
		||||
- [Elixir LangChain](https://github.com/brainlid/langchain)
 | 
			
		||||
- [Ollama for R - rollama](https://github.com/JBGruber/rollama)
 | 
			
		||||
- [Ollama for R - ollama-r](https://github.com/hauselin/ollama-r)
 | 
			
		||||
- [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/)
 | 
			
		||||
- [Portkey](https://portkey.ai/docs/welcome/integration-guides/ollama)
 | 
			
		||||
- [PromptingTools.jl](https://github.com/svilupp/PromptingTools.jl) with an [example](https://svilupp.github.io/PromptingTools.jl/dev/examples/working_with_ollama)
 | 
			
		||||
- [LlamaScript](https://github.com/Project-Llama/llamascript)
 | 
			
		||||
- [llm-axe](https://github.com/emirsahin1/llm-axe) (Python Toolkit for Building LLM Powered Apps)
 | 
			
		||||
- [Gollm](https://docs.gollm.co/examples/ollama-example)
 | 
			
		||||
- [Gollama for Golang](https://github.com/jonathanhecl/gollama)
 | 
			
		||||
- [Ollamaclient for Golang](https://github.com/xyproto/ollamaclient)
 | 
			
		||||
- [High-level function abstraction in Go](https://gitlab.com/tozd/go/fun)
 | 
			
		||||
- [Ollama PHP](https://github.com/ArdaGnsrn/ollama-php)
 | 
			
		||||
- [Agents-Flex for Java](https://github.com/agents-flex/agents-flex) with [example](https://github.com/agents-flex/agents-flex/tree/main/agents-flex-llm/agents-flex-llm-ollama/src/test/java/com/agentsflex/llm/ollama)
 | 
			
		||||
- [Parakeet](https://github.com/parakeet-nest/parakeet) is a GoLang library, made to simplify the development of small generative AI applications with Ollama.
 | 
			
		||||
- [Haverscript](https://github.com/andygill/haverscript) with [examples](https://github.com/andygill/haverscript/tree/main/examples)
 | 
			
		||||
- [Ollama for Swift](https://github.com/mattt/ollama-swift)
 | 
			
		||||
- [Swollama for Swift](https://github.com/marcusziade/Swollama) with [DocC](https://marcusziade.github.io/Swollama/documentation/swollama/)
 | 
			
		||||
- [GoLamify](https://github.com/prasad89/golamify)
 | 
			
		||||
- [Ollama for Haskell](https://github.com/tusharad/ollama-haskell)
 | 
			
		||||
- [multi-llm-ts](https://github.com/nbonamy/multi-llm-ts) (A Typescript/JavaScript library allowing access to different LLM in a unified API)
 | 
			
		||||
- [LlmTornado](https://github.com/lofcz/llmtornado) (C# library providing a unified interface for major FOSS & Commercial inference APIs)
 | 
			
		||||
- [Ollama for Zig](https://github.com/dravenk/ollama-zig)
 | 
			
		||||
- [Abso](https://github.com/lunary-ai/abso) (OpenAI-compatible TypeScript SDK for any LLM provider)
 | 
			
		||||
- [Nichey](https://github.com/goodreasonai/nichey) is a Python package for generating custom wikis for your research topic
 | 
			
		||||
- [Ollama for D](https://github.com/kassane/ollama-d)
 | 
			
		||||
- [OllamaPlusPlus](https://github.com/HardCodeDev777/OllamaPlusPlus) (Very simple C++ library for Ollama)
 | 
			
		||||
 | 
			
		||||
### Mobile
 | 
			
		||||
 | 
			
		||||
- [SwiftChat](https://github.com/aws-samples/swift-chat) (Lightning-fast Cross-platform AI chat app with native UI for Android, iOS, and iPad)
 | 
			
		||||
- [Enchanted](https://github.com/AugustDev/enchanted)
 | 
			
		||||
- [Maid](https://github.com/Mobile-Artificial-Intelligence/maid)
 | 
			
		||||
- [Ollama App](https://github.com/JHubi1/ollama-app) (Modern and easy-to-use multi-platform client for Ollama)
 | 
			
		||||
- [ConfiChat](https://github.com/1runeberg/confichat) (Lightweight, standalone, multi-platform, and privacy-focused LLM chat interface with optional encryption)
 | 
			
		||||
- [Ollama Android Chat](https://github.com/sunshine0523/OllamaServer) (No need for Termux, start the Ollama service with one click on an Android device)
 | 
			
		||||
- [Reins](https://github.com/ibrahimcetin/reins) (Easily tweak parameters, customize system prompts per chat, and enhance your AI experiments with reasoning model support.)
 | 
			
		||||
 | 
			
		||||
### Extensions & Plugins
 | 
			
		||||
 | 
			
		||||
- [Raycast extension](https://github.com/MassimilianoPasquini97/raycast_ollama)
 | 
			
		||||
- [Discollama](https://github.com/mxyng/discollama) (Discord bot inside the Ollama discord channel)
 | 
			
		||||
- [Continue](https://github.com/continuedev/continue)
 | 
			
		||||
- [Vibe](https://github.com/thewh1teagle/vibe) (Transcribe and analyze meetings with Ollama)
 | 
			
		||||
- [Obsidian Ollama plugin](https://github.com/hinterdupfinger/obsidian-ollama)
 | 
			
		||||
- [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)
 | 
			
		||||
- [Discord AI Bot](https://github.com/mekb-turtle/discord-ai-bot)
 | 
			
		||||
- [Ollama Telegram Bot](https://github.com/ruecat/ollama-telegram)
 | 
			
		||||
- [Hass Ollama Conversation](https://github.com/ej52/hass-ollama-conversation)
 | 
			
		||||
- [Rivet plugin](https://github.com/abrenneke/rivet-plugin-ollama)
 | 
			
		||||
- [Llama Coder](https://github.com/ex3ndr/llama-coder) (Copilot alternative using Ollama)
 | 
			
		||||
- [Obsidian BMO Chatbot plugin](https://github.com/longy2k/obsidian-bmo-chatbot)
 | 
			
		||||
- [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)
 | 
			
		||||
- [Llama Coder](https://github.com/ex3ndr/llama-coder) (Copilot alternative using Ollama)
 | 
			
		||||
- [Ollama Copilot](https://github.com/bernardo-bruning/ollama-copilot) (Proxy that allows you to use Ollama as a copilot like GitHub Copilot)
 | 
			
		||||
- [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 Hugging Face)
 | 
			
		||||
- [Page Assist](https://github.com/n4ze3m/page-assist) (Chrome Extension)
 | 
			
		||||
- [Plasmoid Ollama Control](https://github.com/imoize/plasmoid-ollamacontrol) (KDE Plasma extension that allows you to quickly manage/control Ollama model)
 | 
			
		||||
- [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)
 | 
			
		||||
- [ChatGPTBox: All in one browser extension](https://github.com/josStorer/chatGPTBox) with [Integrating Tutorial](https://github.com/josStorer/chatGPTBox/issues/616#issuecomment-1975186467)
 | 
			
		||||
- [Discord AI chat/moderation bot](https://github.com/rapmd73/Companion) Chat/moderation bot written in python. Uses Ollama to create personalities.
 | 
			
		||||
- [Headless Ollama](https://github.com/nischalj10/headless-ollama) (Scripts to automatically install ollama client & models on any OS for apps that depend on ollama server)
 | 
			
		||||
- [Terraform AWS Ollama & Open WebUI](https://github.com/xuyangbocn/terraform-aws-self-host-llm) (A Terraform module to deploy on AWS a ready-to-use Ollama service, together with its front-end Open WebUI service.)
 | 
			
		||||
- [node-red-contrib-ollama](https://github.com/jakubburkiewicz/node-red-contrib-ollama)
 | 
			
		||||
- [Local AI Helper](https://github.com/ivostoykov/localAI) (Chrome and Firefox extensions that enable interactions with the active tab and customisable API endpoints. Includes secure storage for user prompts.)
 | 
			
		||||
- [vnc-lm](https://github.com/jake83741/vnc-lm) (Discord bot for messaging with LLMs through Ollama and LiteLLM. Seamlessly move between local and flagship models.)
 | 
			
		||||
- [LSP-AI](https://github.com/SilasMarvin/lsp-ai) (Open-source language server for AI-powered functionality)
 | 
			
		||||
- [QodeAssist](https://github.com/Palm1r/QodeAssist) (AI-powered coding assistant plugin for Qt Creator)
 | 
			
		||||
- [Obsidian Quiz Generator plugin](https://github.com/ECuiDev/obsidian-quiz-generator)
 | 
			
		||||
- [AI Summmary Helper plugin](https://github.com/philffm/ai-summary-helper)
 | 
			
		||||
- [TextCraft](https://github.com/suncloudsmoon/TextCraft) (Copilot in Word alternative using Ollama)
 | 
			
		||||
- [Alfred Ollama](https://github.com/zeitlings/alfred-ollama) (Alfred Workflow)
 | 
			
		||||
- [TextLLaMA](https://github.com/adarshM84/TextLLaMA) A Chrome Extension that helps you write emails, correct grammar, and translate into any language
 | 
			
		||||
- [Simple-Discord-AI](https://github.com/zyphixor/simple-discord-ai)
 | 
			
		||||
- [LLM Telegram Bot](https://github.com/innightwolfsleep/llm_telegram_bot) (telegram bot, primary for RP. Oobabooga-like buttons, [A1111](https://github.com/AUTOMATIC1111/stable-diffusion-webui) API integration e.t.c)
 | 
			
		||||
- [mcp-llm](https://github.com/sammcj/mcp-llm) (MCP Server to allow LLMs to call other LLMs)
 | 
			
		||||
- [UnityCodeLama](https://github.com/HardCodeDev777/UnityCodeLama) (Unity Edtior tool to analyze scripts via Ollama)
 | 
			
		||||
 | 
			
		||||
### Supported backends
 | 
			
		||||
 | 
			
		||||
- [llama.cpp](https://github.com/ggerganov/llama.cpp) project founded by Georgi Gerganov.
 | 
			
		||||
 | 
			
		||||
### Observability
 | 
			
		||||
- [Opik](https://www.comet.com/docs/opik/cookbook/ollama) is an open-source platform to debug, evaluate, and monitor your LLM applications, RAG systems, and agentic workflows with comprehensive tracing, automated evaluations, and production-ready dashboards. Opik supports native intergration to Ollama.
 | 
			
		||||
- [Lunary](https://lunary.ai/docs/integrations/ollama) is the leading open-source LLM observability platform. It provides a variety of enterprise-grade features such as real-time analytics, prompt templates management, PII masking, and comprehensive agent tracing.
 | 
			
		||||
- [OpenLIT](https://github.com/openlit/openlit) is an OpenTelemetry-native tool for monitoring Ollama Applications & GPUs using traces and metrics.
 | 
			
		||||
- [HoneyHive](https://docs.honeyhive.ai/integrations/ollama) is an AI observability and evaluation platform for AI agents. Use HoneyHive to evaluate agent performance, interrogate failures, and monitor quality in production.
 | 
			
		||||
- [Langfuse](https://langfuse.com/docs/integrations/ollama) is an open source LLM observability platform that enables teams to collaboratively monitor, evaluate and debug AI applications.
 | 
			
		||||
- [MLflow Tracing](https://mlflow.org/docs/latest/llms/tracing/index.html#automatic-tracing) is an open source LLM observability tool with a convenient API to log and visualize traces, making it easy to debug and evaluate GenAI applications.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								SECURITY.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								SECURITY.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
# Security
 | 
			
		||||
 | 
			
		||||
The Ollama maintainer team takes security seriously and will actively work to resolve security issues.
 | 
			
		||||
 | 
			
		||||
## Reporting a vulnerability
 | 
			
		||||
 | 
			
		||||
If you discover a security vulnerability, please do not open a public issue. Instead, please report it by emailing hello@ollama.com. We ask that you give us sufficient time to investigate and address the vulnerability before disclosing it publicly.
 | 
			
		||||
 | 
			
		||||
Please include the following details in your report:
 | 
			
		||||
- A description of the vulnerability
 | 
			
		||||
- Steps to reproduce the issue
 | 
			
		||||
- Your assessment of the potential impact
 | 
			
		||||
- Any possible mitigations
 | 
			
		||||
 | 
			
		||||
## Security best practices
 | 
			
		||||
 | 
			
		||||
While the maintainer team does their best to secure Ollama, users are encouraged to implement their own security best practices, such as:
 | 
			
		||||
 | 
			
		||||
- Regularly updating to the latest version of Ollama
 | 
			
		||||
- Securing access to hosted instances of Ollama
 | 
			
		||||
- Monitoring systems for unusual activity
 | 
			
		||||
 | 
			
		||||
## Contact
 | 
			
		||||
 | 
			
		||||
For any other questions or concerns related to security, please contact us at hello@ollama.com
 | 
			
		||||
							
								
								
									
										169
									
								
								api/client.go
									
									
									
									
									
								
							
							
						
						
									
										169
									
								
								api/client.go
									
									
									
									
									
								
							@@ -1,3 +1,16 @@
 | 
			
		||||
// 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 [the API documentation].
 | 
			
		||||
// The ollama command-line client itself uses this package to interact with
 | 
			
		||||
// the backend service.
 | 
			
		||||
//
 | 
			
		||||
// # Examples
 | 
			
		||||
//
 | 
			
		||||
// Several examples of using this package are available [in the GitHub
 | 
			
		||||
// repository].
 | 
			
		||||
//
 | 
			
		||||
// [the API documentation]: https://github.com/ollama/ollama/blob/main/docs/api.md
 | 
			
		||||
// [in the GitHub repository]: https://github.com/ollama/ollama/tree/main/api/examples
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
@@ -8,20 +21,20 @@ import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/jmorganca/ollama/format"
 | 
			
		||||
	"github.com/jmorganca/ollama/version"
 | 
			
		||||
	"github.com/ollama/ollama/envconfig"
 | 
			
		||||
	"github.com/ollama/ollama/format"
 | 
			
		||||
	"github.com/ollama/ollama/version"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Client encapsulates client state for interacting with the ollama
 | 
			
		||||
// service. Use [ClientFromEnvironment] to create new Clients.
 | 
			
		||||
type Client struct {
 | 
			
		||||
	base *url.URL
 | 
			
		||||
	http http.Client
 | 
			
		||||
	http *http.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func checkError(resp *http.Response, body []byte) error {
 | 
			
		||||
@@ -40,56 +53,27 @@ func checkError(resp *http.Response, body []byte) error {
 | 
			
		||||
	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 listening. 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) {
 | 
			
		||||
	defaultPort := "11434"
 | 
			
		||||
	return &Client{
 | 
			
		||||
		base: envconfig.Host(),
 | 
			
		||||
		http: http.DefaultClient,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	scheme, hostport, ok := strings.Cut(os.Getenv("OLLAMA_HOST"), "://")
 | 
			
		||||
	switch {
 | 
			
		||||
	case !ok:
 | 
			
		||||
		scheme, hostport = "http", os.Getenv("OLLAMA_HOST")
 | 
			
		||||
	case scheme == "http":
 | 
			
		||||
		defaultPort = "80"
 | 
			
		||||
	case scheme == "https":
 | 
			
		||||
		defaultPort = "443"
 | 
			
		||||
func NewClient(base *url.URL, http *http.Client) *Client {
 | 
			
		||||
	return &Client{
 | 
			
		||||
		base: base,
 | 
			
		||||
		http: http,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// trim trailing slashes
 | 
			
		||||
	hostport = strings.TrimRight(hostport, "/")
 | 
			
		||||
 | 
			
		||||
	host, port, err := net.SplitHostPort(hostport)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		host, port = "127.0.0.1", defaultPort
 | 
			
		||||
		if ip := net.ParseIP(strings.Trim(hostport, "[]")); ip != nil {
 | 
			
		||||
			host = ip.String()
 | 
			
		||||
		} else if hostport != "" {
 | 
			
		||||
			host = hostport
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client := Client{
 | 
			
		||||
		base: &url.URL{
 | 
			
		||||
			Scheme: scheme,
 | 
			
		||||
			Host:   net.JoinHostPort(host, port),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mockRequest, err := http.NewRequest(http.MethodHead, client.base.String(), nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 {
 | 
			
		||||
@@ -148,7 +132,7 @@ func (c *Client) do(ctx context.Context, method, path string, reqData, respData
 | 
			
		||||
const maxBufferSize = 512 * format.KiloByte
 | 
			
		||||
 | 
			
		||||
func (c *Client) stream(ctx context.Context, method, path string, data any, fn func([]byte) error) error {
 | 
			
		||||
	var buf *bytes.Buffer
 | 
			
		||||
	var buf io.Reader
 | 
			
		||||
	if data != nil {
 | 
			
		||||
		bts, err := json.Marshal(data)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -189,7 +173,7 @@ func (c *Client) stream(ctx context.Context, method, path string, data any, fn f
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if errorResponse.Error != "" {
 | 
			
		||||
			return fmt.Errorf(errorResponse.Error)
 | 
			
		||||
			return errors.New(errorResponse.Error)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if response.StatusCode >= http.StatusBadRequest {
 | 
			
		||||
@@ -208,8 +192,14 @@ func (c *Client) stream(ctx context.Context, method, path string, data any, fn f
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
// 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 {
 | 
			
		||||
	return c.stream(ctx, http.MethodPost, "/api/generate", req, func(bts []byte) error {
 | 
			
		||||
		var resp GenerateResponse
 | 
			
		||||
@@ -221,8 +211,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
 | 
			
		||||
 | 
			
		||||
// 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 {
 | 
			
		||||
	return c.stream(ctx, http.MethodPost, "/api/chat", req, func(bts []byte) error {
 | 
			
		||||
		var resp ChatResponse
 | 
			
		||||
@@ -234,8 +231,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
 | 
			
		||||
 | 
			
		||||
// 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 {
 | 
			
		||||
	return c.stream(ctx, http.MethodPost, "/api/pull", req, func(bts []byte) error {
 | 
			
		||||
		var resp ProgressResponse
 | 
			
		||||
@@ -247,8 +250,14 @@ func (c *Client) Pull(ctx context.Context, req *PullRequest, fn PullProgressFunc
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PushProgressFunc is a function that [Client.Push] invokes when progress is
 | 
			
		||||
// made.
 | 
			
		||||
// It's similar to other progress function types like [PullProgressFunc].
 | 
			
		||||
type PushProgressFunc func(ProgressResponse) error
 | 
			
		||||
 | 
			
		||||
// Push uploads a model to the model library; requires registering for ollama.ai
 | 
			
		||||
// and adding a public key first. fn is called each time progress is made on
 | 
			
		||||
// the request and can be used to display a progress bar, etc.
 | 
			
		||||
func (c *Client) Push(ctx context.Context, req *PushRequest, fn PushProgressFunc) error {
 | 
			
		||||
	return c.stream(ctx, http.MethodPost, "/api/push", req, func(bts []byte) error {
 | 
			
		||||
		var resp ProgressResponse
 | 
			
		||||
@@ -260,8 +269,15 @@ func (c *Client) Push(ctx context.Context, req *PushRequest, fn PushProgressFunc
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateProgressFunc is a function that [Client.Create] invokes when progress
 | 
			
		||||
// is made.
 | 
			
		||||
// It's similar to other progress function types like [PullProgressFunc].
 | 
			
		||||
type CreateProgressFunc func(ProgressResponse) error
 | 
			
		||||
 | 
			
		||||
// Create creates a model from a [Modelfile]. fn is a progress function that
 | 
			
		||||
// behaves similarly to other methods (see [Client.Pull]).
 | 
			
		||||
//
 | 
			
		||||
// [Modelfile]: https://github.com/ollama/ollama/blob/main/docs/modelfile.md
 | 
			
		||||
func (c *Client) Create(ctx context.Context, req *CreateRequest, fn CreateProgressFunc) error {
 | 
			
		||||
	return c.stream(ctx, http.MethodPost, "/api/create", req, func(bts []byte) error {
 | 
			
		||||
		var resp ProgressResponse
 | 
			
		||||
@@ -273,6 +289,7 @@ func (c *Client) Create(ctx context.Context, req *CreateRequest, fn CreateProgre
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// List lists models that are available locally.
 | 
			
		||||
func (c *Client) List(ctx context.Context) (*ListResponse, error) {
 | 
			
		||||
	var lr ListResponse
 | 
			
		||||
	if err := c.do(ctx, http.MethodGet, "/api/tags", nil, &lr); err != nil {
 | 
			
		||||
@@ -281,6 +298,17 @@ func (c *Client) List(ctx context.Context) (*ListResponse, error) {
 | 
			
		||||
	return &lr, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListRunning lists running models.
 | 
			
		||||
func (c *Client) ListRunning(ctx context.Context) (*ProcessResponse, error) {
 | 
			
		||||
	var lr ProcessResponse
 | 
			
		||||
	if err := c.do(ctx, http.MethodGet, "/api/ps", nil, &lr); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &lr, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Copy copies a model - creating a model with another name from an existing
 | 
			
		||||
// model.
 | 
			
		||||
func (c *Client) Copy(ctx context.Context, req *CopyRequest) error {
 | 
			
		||||
	if err := c.do(ctx, http.MethodPost, "/api/copy", req, nil); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@@ -288,6 +316,7 @@ func (c *Client) Copy(ctx context.Context, req *CopyRequest) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Delete deletes a model and its data.
 | 
			
		||||
func (c *Client) Delete(ctx context.Context, req *DeleteRequest) error {
 | 
			
		||||
	if err := c.do(ctx, http.MethodDelete, "/api/delete", req, nil); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@@ -295,6 +324,7 @@ func (c *Client) Delete(ctx context.Context, req *DeleteRequest) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Show obtains model information, including details, modelfile, license etc.
 | 
			
		||||
func (c *Client) Show(ctx context.Context, req *ShowRequest) (*ShowResponse, error) {
 | 
			
		||||
	var resp ShowResponse
 | 
			
		||||
	if err := c.do(ctx, http.MethodPost, "/api/show", req, &resp); err != nil {
 | 
			
		||||
@@ -303,12 +333,25 @@ func (c *Client) Show(ctx context.Context, req *ShowRequest) (*ShowResponse, err
 | 
			
		||||
	return &resp, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Heartbeat checks if the server has started and is responsive; if yes, it
 | 
			
		||||
// returns nil, otherwise an error.
 | 
			
		||||
func (c *Client) Heartbeat(ctx context.Context) error {
 | 
			
		||||
	if err := c.do(ctx, http.MethodHead, "/", nil, nil); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Embed generates embeddings from a model.
 | 
			
		||||
func (c *Client) Embed(ctx context.Context, req *EmbedRequest) (*EmbedResponse, error) {
 | 
			
		||||
	var resp EmbedResponse
 | 
			
		||||
	if err := c.do(ctx, http.MethodPost, "/api/embed", req, &resp); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &resp, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Embeddings generates an embedding from a model.
 | 
			
		||||
func (c *Client) Embeddings(ctx context.Context, req *EmbeddingRequest) (*EmbeddingResponse, error) {
 | 
			
		||||
	var resp EmbeddingResponse
 | 
			
		||||
	if err := c.do(ctx, http.MethodPost, "/api/embeddings", req, &resp); err != nil {
 | 
			
		||||
@@ -317,21 +360,13 @@ func (c *Client) Embeddings(ctx context.Context, req *EmbeddingRequest) (*Embedd
 | 
			
		||||
	return &resp, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateBlob creates a blob from a file on the server. digest is the
 | 
			
		||||
// expected SHA256 digest of the file, and r represents the file.
 | 
			
		||||
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 {
 | 
			
		||||
		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
 | 
			
		||||
	return c.do(ctx, http.MethodPost, fmt.Sprintf("/api/blobs/%s", digest), r, nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Version returns the Ollama server version as a string.
 | 
			
		||||
func (c *Client) Version(ctx context.Context) (string, error) {
 | 
			
		||||
	var version struct {
 | 
			
		||||
		Version string `json:"version"`
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,14 @@
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
import "testing"
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/http/httptest"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestClientFromEnvironment(t *testing.T) {
 | 
			
		||||
	type testCase struct {
 | 
			
		||||
@@ -41,3 +49,206 @@ func TestClientFromEnvironment(t *testing.T) {
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// testError represents an internal error type with status code and message
 | 
			
		||||
// this is used since the error response from the server is not a standard error struct
 | 
			
		||||
type testError struct {
 | 
			
		||||
	message    string
 | 
			
		||||
	statusCode int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e testError) Error() string {
 | 
			
		||||
	return e.message
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestClientStream(t *testing.T) {
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		name      string
 | 
			
		||||
		responses []any
 | 
			
		||||
		wantErr   string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "immediate error response",
 | 
			
		||||
			responses: []any{
 | 
			
		||||
				testError{
 | 
			
		||||
					message:    "test error message",
 | 
			
		||||
					statusCode: http.StatusBadRequest,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			wantErr: "test error message",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "error after successful chunks, ok response",
 | 
			
		||||
			responses: []any{
 | 
			
		||||
				ChatResponse{Message: Message{Content: "partial response 1"}},
 | 
			
		||||
				ChatResponse{Message: Message{Content: "partial response 2"}},
 | 
			
		||||
				testError{
 | 
			
		||||
					message:    "mid-stream error",
 | 
			
		||||
					statusCode: http.StatusOK,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			wantErr: "mid-stream error",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "successful stream completion",
 | 
			
		||||
			responses: []any{
 | 
			
		||||
				ChatResponse{Message: Message{Content: "chunk 1"}},
 | 
			
		||||
				ChatResponse{Message: Message{Content: "chunk 2"}},
 | 
			
		||||
				ChatResponse{
 | 
			
		||||
					Message:    Message{Content: "final chunk"},
 | 
			
		||||
					Done:       true,
 | 
			
		||||
					DoneReason: "stop",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
				flusher, ok := w.(http.Flusher)
 | 
			
		||||
				if !ok {
 | 
			
		||||
					t.Fatal("expected http.Flusher")
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				w.Header().Set("Content-Type", "application/x-ndjson")
 | 
			
		||||
 | 
			
		||||
				for _, resp := range tc.responses {
 | 
			
		||||
					if errResp, ok := resp.(testError); ok {
 | 
			
		||||
						w.WriteHeader(errResp.statusCode)
 | 
			
		||||
						err := json.NewEncoder(w).Encode(map[string]string{
 | 
			
		||||
							"error": errResp.message,
 | 
			
		||||
						})
 | 
			
		||||
						if err != nil {
 | 
			
		||||
							t.Fatal("failed to encode error response:", err)
 | 
			
		||||
						}
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if err := json.NewEncoder(w).Encode(resp); err != nil {
 | 
			
		||||
						t.Fatalf("failed to encode response: %v", err)
 | 
			
		||||
					}
 | 
			
		||||
					flusher.Flush()
 | 
			
		||||
				}
 | 
			
		||||
			}))
 | 
			
		||||
			defer ts.Close()
 | 
			
		||||
 | 
			
		||||
			client := NewClient(&url.URL{Scheme: "http", Host: ts.Listener.Addr().String()}, http.DefaultClient)
 | 
			
		||||
 | 
			
		||||
			var receivedChunks []ChatResponse
 | 
			
		||||
			err := client.stream(t.Context(), http.MethodPost, "/v1/chat", nil, func(chunk []byte) error {
 | 
			
		||||
				var resp ChatResponse
 | 
			
		||||
				if err := json.Unmarshal(chunk, &resp); err != nil {
 | 
			
		||||
					return fmt.Errorf("failed to unmarshal chunk: %w", err)
 | 
			
		||||
				}
 | 
			
		||||
				receivedChunks = append(receivedChunks, resp)
 | 
			
		||||
				return nil
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			if tc.wantErr != "" {
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					t.Fatal("expected error but got nil")
 | 
			
		||||
				}
 | 
			
		||||
				if !strings.Contains(err.Error(), tc.wantErr) {
 | 
			
		||||
					t.Errorf("expected error containing %q, got %v", tc.wantErr, err)
 | 
			
		||||
				}
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Errorf("unexpected error: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestClientDo(t *testing.T) {
 | 
			
		||||
	testCases := []struct {
 | 
			
		||||
		name     string
 | 
			
		||||
		response any
 | 
			
		||||
		wantErr  string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "immediate error response",
 | 
			
		||||
			response: testError{
 | 
			
		||||
				message:    "test error message",
 | 
			
		||||
				statusCode: http.StatusBadRequest,
 | 
			
		||||
			},
 | 
			
		||||
			wantErr: "test error message",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "server error response",
 | 
			
		||||
			response: testError{
 | 
			
		||||
				message:    "internal error",
 | 
			
		||||
				statusCode: http.StatusInternalServerError,
 | 
			
		||||
			},
 | 
			
		||||
			wantErr: "internal error",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "successful response",
 | 
			
		||||
			response: struct {
 | 
			
		||||
				ID      string `json:"id"`
 | 
			
		||||
				Success bool   `json:"success"`
 | 
			
		||||
			}{
 | 
			
		||||
				ID:      "msg_123",
 | 
			
		||||
				Success: true,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range testCases {
 | 
			
		||||
		t.Run(tc.name, func(t *testing.T) {
 | 
			
		||||
			ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
				if errResp, ok := tc.response.(testError); ok {
 | 
			
		||||
					w.WriteHeader(errResp.statusCode)
 | 
			
		||||
					err := json.NewEncoder(w).Encode(map[string]string{
 | 
			
		||||
						"error": errResp.message,
 | 
			
		||||
					})
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						t.Fatal("failed to encode error response:", err)
 | 
			
		||||
					}
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
				if err := json.NewEncoder(w).Encode(tc.response); err != nil {
 | 
			
		||||
					t.Fatalf("failed to encode response: %v", err)
 | 
			
		||||
				}
 | 
			
		||||
			}))
 | 
			
		||||
			defer ts.Close()
 | 
			
		||||
 | 
			
		||||
			client := NewClient(&url.URL{Scheme: "http", Host: ts.Listener.Addr().String()}, http.DefaultClient)
 | 
			
		||||
 | 
			
		||||
			var resp struct {
 | 
			
		||||
				ID      string `json:"id"`
 | 
			
		||||
				Success bool   `json:"success"`
 | 
			
		||||
			}
 | 
			
		||||
			err := client.do(t.Context(), http.MethodPost, "/v1/messages", nil, &resp)
 | 
			
		||||
 | 
			
		||||
			if tc.wantErr != "" {
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					t.Fatalf("got nil, want error %q", tc.wantErr)
 | 
			
		||||
				}
 | 
			
		||||
				if err.Error() != tc.wantErr {
 | 
			
		||||
					t.Errorf("error message mismatch: got %q, want %q", err.Error(), tc.wantErr)
 | 
			
		||||
				}
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatalf("got error %q, want nil", err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if expectedResp, ok := tc.response.(struct {
 | 
			
		||||
				ID      string `json:"id"`
 | 
			
		||||
				Success bool   `json:"success"`
 | 
			
		||||
			}); ok {
 | 
			
		||||
				if resp.ID != expectedResp.ID {
 | 
			
		||||
					t.Errorf("response ID mismatch: got %q, want %q", resp.ID, expectedResp.ID)
 | 
			
		||||
				}
 | 
			
		||||
				if resp.Success != expectedResp.Success {
 | 
			
		||||
					t.Errorf("response Success mismatch: got %v, want %v", resp.Success, expectedResp.Success)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								api/examples/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								api/examples/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
# Ollama API Examples
 | 
			
		||||
 | 
			
		||||
Run the examples in this directory with:
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
go run example_name/main.go
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Chat - Chat with a model
 | 
			
		||||
- [chat/main.go](chat/main.go)
 | 
			
		||||
 | 
			
		||||
## Generate - Generate text from a model
 | 
			
		||||
- [generate/main.go](generate/main.go)
 | 
			
		||||
- [generate-streaming/main.go](generate-streaming/main.go)
 | 
			
		||||
 | 
			
		||||
## Pull - Pull a model
 | 
			
		||||
- [pull-progress/main.go](pull-progress/main.go)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										51
									
								
								api/examples/chat/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								api/examples/chat/main.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	client, err := api.ClientFromEnvironment()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	messages := []api.Message{
 | 
			
		||||
		api.Message{
 | 
			
		||||
			Role:    "system",
 | 
			
		||||
			Content: "Provide very brief, concise responses",
 | 
			
		||||
		},
 | 
			
		||||
		api.Message{
 | 
			
		||||
			Role:    "user",
 | 
			
		||||
			Content: "Name some unusual animals",
 | 
			
		||||
		},
 | 
			
		||||
		api.Message{
 | 
			
		||||
			Role:    "assistant",
 | 
			
		||||
			Content: "Monotreme, platypus, echidna",
 | 
			
		||||
		},
 | 
			
		||||
		api.Message{
 | 
			
		||||
			Role:    "user",
 | 
			
		||||
			Content: "which of these is the most dangerous?",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	req := &api.ChatRequest{
 | 
			
		||||
		Model:    "llama3.2",
 | 
			
		||||
		Messages: messages,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	respFunc := func(resp api.ChatResponse) error {
 | 
			
		||||
		fmt.Print(resp.Message.Content)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = client.Chat(ctx, req, respFunc)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								api/examples/generate-streaming/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								api/examples/generate-streaming/main.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	client, err := api.ClientFromEnvironment()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// By default, GenerateRequest is streaming.
 | 
			
		||||
	req := &api.GenerateRequest{
 | 
			
		||||
		Model:  "gemma2",
 | 
			
		||||
		Prompt: "how many planets are there?",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	respFunc := func(resp api.GenerateResponse) error {
 | 
			
		||||
		// Only print the response here; GenerateResponse has a number of other
 | 
			
		||||
		// interesting fields you want to examine.
 | 
			
		||||
 | 
			
		||||
		// In streaming mode, responses are partial so we call fmt.Print (and not
 | 
			
		||||
		// Println) in order to avoid spurious newlines being introduced. The
 | 
			
		||||
		// model will insert its own newlines if it wants.
 | 
			
		||||
		fmt.Print(resp.Response)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = client.Generate(ctx, req, respFunc)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	fmt.Println()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								api/examples/generate/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								api/examples/generate/main.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	client, err := api.ClientFromEnvironment()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req := &api.GenerateRequest{
 | 
			
		||||
		Model:  "gemma2",
 | 
			
		||||
		Prompt: "how many planets are there?",
 | 
			
		||||
 | 
			
		||||
		// set streaming to false
 | 
			
		||||
		Stream: new(bool),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	respFunc := func(resp api.GenerateResponse) error {
 | 
			
		||||
		// Only print the response here; GenerateResponse has a number of other
 | 
			
		||||
		// interesting fields you want to examine.
 | 
			
		||||
		fmt.Println(resp.Response)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = client.Generate(ctx, req, respFunc)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								api/examples/multimodal/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								api/examples/multimodal/main.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	if len(os.Args) <= 1 {
 | 
			
		||||
		log.Fatal("usage: <image name>")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	imgData, err := os.ReadFile(os.Args[1])
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client, err := api.ClientFromEnvironment()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req := &api.GenerateRequest{
 | 
			
		||||
		Model:  "llava",
 | 
			
		||||
		Prompt: "describe this image",
 | 
			
		||||
		Images: []api.ImageData{imgData},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
	respFunc := func(resp api.GenerateResponse) error {
 | 
			
		||||
		// In streaming mode, responses are partial so we call fmt.Print (and not
 | 
			
		||||
		// Println) in order to avoid spurious newlines being introduced. The
 | 
			
		||||
		// model will insert its own newlines if it wants.
 | 
			
		||||
		fmt.Print(resp.Response)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = client.Generate(ctx, req, respFunc)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	fmt.Println()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										31
									
								
								api/examples/pull-progress/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								api/examples/pull-progress/main.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	client, err := api.ClientFromEnvironment()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	req := &api.PullRequest{
 | 
			
		||||
		Model: "mistral",
 | 
			
		||||
	}
 | 
			
		||||
	progressFunc := func(resp api.ProgressResponse) error {
 | 
			
		||||
		fmt.Printf("Progress: status=%v, total=%v, completed=%v\n", resp.Status, resp.Total, resp.Completed)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = client.Pull(ctx, req, progressFunc)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										594
									
								
								api/types.go
									
									
									
									
									
								
							
							
						
						
									
										594
									
								
								api/types.go
									
									
									
									
									
								
							@@ -3,14 +3,19 @@ package api
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"math"
 | 
			
		||||
	"os"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/envconfig"
 | 
			
		||||
	"github.com/ollama/ollama/types/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// StatusError is an error with an HTTP status code and message.
 | 
			
		||||
type StatusError struct {
 | 
			
		||||
	StatusCode   int
 | 
			
		||||
	Status       string
 | 
			
		||||
@@ -31,41 +36,208 @@ func (e StatusError) Error() string {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ImageData represents the raw binary data of an image file.
 | 
			
		||||
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 {
 | 
			
		||||
	Model    string      `json:"model"`
 | 
			
		||||
	Prompt   string      `json:"prompt"`
 | 
			
		||||
	System   string      `json:"system"`
 | 
			
		||||
	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"`
 | 
			
		||||
	// Model is the model name; it should be a name familiar to Ollama from
 | 
			
		||||
	// the library at https://ollama.com/library
 | 
			
		||||
	Model string `json:"model"`
 | 
			
		||||
 | 
			
		||||
	Options map[string]interface{} `json:"options"`
 | 
			
		||||
	// Prompt is the textual prompt to send to the model.
 | 
			
		||||
	Prompt string `json:"prompt"`
 | 
			
		||||
 | 
			
		||||
	// Suffix is the text that comes after the inserted text.
 | 
			
		||||
	Suffix string `json:"suffix"`
 | 
			
		||||
 | 
			
		||||
	// 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
 | 
			
		||||
	// [Client.Generate]. 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 json.RawMessage `json:"format,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// 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 raw image bytes 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]any `json:"options"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ChatRequest describes a request sent by [Client.Chat].
 | 
			
		||||
type ChatRequest struct {
 | 
			
		||||
	Model    string    `json:"model"`
 | 
			
		||||
	// Model is the model name, as in [GenerateRequest].
 | 
			
		||||
	Model string `json:"model"`
 | 
			
		||||
 | 
			
		||||
	// Messages is the messages of the chat - can be used to keep a chat memory.
 | 
			
		||||
	Messages []Message `json:"messages"`
 | 
			
		||||
	Stream   *bool     `json:"stream,omitempty"`
 | 
			
		||||
	Format   string    `json:"format"`
 | 
			
		||||
 | 
			
		||||
	Options map[string]interface{} `json:"options"`
 | 
			
		||||
	// Stream enables streaming of returned responses; true by default.
 | 
			
		||||
	Stream *bool `json:"stream,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// Format is the format to return the response in (e.g. "json").
 | 
			
		||||
	Format json.RawMessage `json:"format,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// KeepAlive controls how long the model will stay loaded into memory
 | 
			
		||||
	// following the request.
 | 
			
		||||
	KeepAlive *Duration `json:"keep_alive,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// Tools is an optional list of tools the model has access to.
 | 
			
		||||
	Tools `json:"tools,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// Options lists model-specific options.
 | 
			
		||||
	Options map[string]any `json:"options"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Tools []Tool
 | 
			
		||||
 | 
			
		||||
func (t Tools) String() string {
 | 
			
		||||
	bts, _ := json.Marshal(t)
 | 
			
		||||
	return string(bts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t Tool) String() string {
 | 
			
		||||
	bts, _ := json.Marshal(t)
 | 
			
		||||
	return string(bts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Message is a single message in a chat sequence. The message contains the
 | 
			
		||||
// role ("system", "user", or "assistant"), the content and an optional list
 | 
			
		||||
// of images.
 | 
			
		||||
type Message struct {
 | 
			
		||||
	Role    string      `json:"role"` // one of ["system", "user", "assistant"]
 | 
			
		||||
	Content string      `json:"content"`
 | 
			
		||||
	Images  []ImageData `json:"images,omitempty"`
 | 
			
		||||
	Role      string      `json:"role"`
 | 
			
		||||
	Content   string      `json:"content"`
 | 
			
		||||
	Images    []ImageData `json:"images,omitempty"`
 | 
			
		||||
	ToolCalls []ToolCall  `json:"tool_calls,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Message) UnmarshalJSON(b []byte) error {
 | 
			
		||||
	type Alias Message
 | 
			
		||||
	var a Alias
 | 
			
		||||
	if err := json.Unmarshal(b, &a); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	*m = Message(a)
 | 
			
		||||
	m.Role = strings.ToLower(m.Role)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ToolCall struct {
 | 
			
		||||
	Function ToolCallFunction `json:"function"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ToolCallFunction struct {
 | 
			
		||||
	Index     int                       `json:"index,omitempty"`
 | 
			
		||||
	Name      string                    `json:"name"`
 | 
			
		||||
	Arguments ToolCallFunctionArguments `json:"arguments"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ToolCallFunctionArguments map[string]any
 | 
			
		||||
 | 
			
		||||
func (t *ToolCallFunctionArguments) String() string {
 | 
			
		||||
	bts, _ := json.Marshal(t)
 | 
			
		||||
	return string(bts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Tool struct {
 | 
			
		||||
	Type     string       `json:"type"`
 | 
			
		||||
	Items    any          `json:"items,omitempty"`
 | 
			
		||||
	Function ToolFunction `json:"function"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PropertyType can be either a string or an array of strings
 | 
			
		||||
type PropertyType []string
 | 
			
		||||
 | 
			
		||||
// UnmarshalJSON implements the json.Unmarshaler interface
 | 
			
		||||
func (pt *PropertyType) UnmarshalJSON(data []byte) error {
 | 
			
		||||
	// Try to unmarshal as a string first
 | 
			
		||||
	var s string
 | 
			
		||||
	if err := json.Unmarshal(data, &s); err == nil {
 | 
			
		||||
		*pt = []string{s}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If that fails, try to unmarshal as an array of strings
 | 
			
		||||
	var a []string
 | 
			
		||||
	if err := json.Unmarshal(data, &a); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	*pt = a
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MarshalJSON implements the json.Marshaler interface
 | 
			
		||||
func (pt PropertyType) MarshalJSON() ([]byte, error) {
 | 
			
		||||
	if len(pt) == 1 {
 | 
			
		||||
		// If there's only one type, marshal as a string
 | 
			
		||||
		return json.Marshal(pt[0])
 | 
			
		||||
	}
 | 
			
		||||
	// Otherwise marshal as an array
 | 
			
		||||
	return json.Marshal([]string(pt))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// String returns a string representation of the PropertyType
 | 
			
		||||
func (pt PropertyType) String() string {
 | 
			
		||||
	if len(pt) == 0 {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	if len(pt) == 1 {
 | 
			
		||||
		return pt[0]
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("%v", []string(pt))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ToolFunction struct {
 | 
			
		||||
	Name        string `json:"name"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
	Parameters  struct {
 | 
			
		||||
		Type       string   `json:"type"`
 | 
			
		||||
		Defs       any      `json:"$defs,omitempty"`
 | 
			
		||||
		Items      any      `json:"items,omitempty"`
 | 
			
		||||
		Required   []string `json:"required"`
 | 
			
		||||
		Properties map[string]struct {
 | 
			
		||||
			Type        PropertyType `json:"type"`
 | 
			
		||||
			Items       any          `json:"items,omitempty"`
 | 
			
		||||
			Description string       `json:"description"`
 | 
			
		||||
			Enum        []any        `json:"enum,omitempty"`
 | 
			
		||||
		} `json:"properties"`
 | 
			
		||||
	} `json:"parameters"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *ToolFunction) String() string {
 | 
			
		||||
	bts, _ := json.Marshal(t)
 | 
			
		||||
	return string(bts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ChatResponse is the response returned by [Client.Chat]. Its fields are
 | 
			
		||||
// similar to [GenerateResponse].
 | 
			
		||||
type ChatResponse struct {
 | 
			
		||||
	Model     string    `json:"model"`
 | 
			
		||||
	CreatedAt time.Time `json:"created_at"`
 | 
			
		||||
	Message   Message   `json:"message"`
 | 
			
		||||
	Model      string    `json:"model"`
 | 
			
		||||
	CreatedAt  time.Time `json:"created_at"`
 | 
			
		||||
	Message    Message   `json:"message"`
 | 
			
		||||
	DoneReason string    `json:"done_reason,omitempty"`
 | 
			
		||||
 | 
			
		||||
	Done bool `json:"done"`
 | 
			
		||||
 | 
			
		||||
@@ -81,7 +253,8 @@ type Metrics struct {
 | 
			
		||||
	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, also
 | 
			
		||||
// add it to the API docs.
 | 
			
		||||
type Options struct {
 | 
			
		||||
	Runner
 | 
			
		||||
 | 
			
		||||
@@ -91,104 +264,155 @@ type Options struct {
 | 
			
		||||
	NumPredict       int      `json:"num_predict,omitempty"`
 | 
			
		||||
	TopK             int      `json:"top_k,omitempty"`
 | 
			
		||||
	TopP             float32  `json:"top_p,omitempty"`
 | 
			
		||||
	TFSZ             float32  `json:"tfs_z,omitempty"`
 | 
			
		||||
	MinP             float32  `json:"min_p,omitempty"`
 | 
			
		||||
	TypicalP         float32  `json:"typical_p,omitempty"`
 | 
			
		||||
	RepeatLastN      int      `json:"repeat_last_n,omitempty"`
 | 
			
		||||
	Temperature      float32  `json:"temperature,omitempty"`
 | 
			
		||||
	RepeatPenalty    float32  `json:"repeat_penalty,omitempty"`
 | 
			
		||||
	PresencePenalty  float32  `json:"presence_penalty,omitempty"`
 | 
			
		||||
	FrequencyPenalty float32  `json:"frequency_penalty,omitempty"`
 | 
			
		||||
	Mirostat         int      `json:"mirostat,omitempty"`
 | 
			
		||||
	MirostatTau      float32  `json:"mirostat_tau,omitempty"`
 | 
			
		||||
	MirostatEta      float32  `json:"mirostat_eta,omitempty"`
 | 
			
		||||
	PenalizeNewline  bool     `json:"penalize_newline,omitempty"`
 | 
			
		||||
	Stop             []string `json:"stop,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Runner options which must be set when the model is loaded into memory
 | 
			
		||||
type Runner struct {
 | 
			
		||||
	UseNUMA            bool    `json:"numa,omitempty"`
 | 
			
		||||
	NumCtx             int     `json:"num_ctx,omitempty"`
 | 
			
		||||
	NumBatch           int     `json:"num_batch,omitempty"`
 | 
			
		||||
	NumGQA             int     `json:"num_gqa,omitempty"`
 | 
			
		||||
	NumGPU             int     `json:"num_gpu,omitempty"`
 | 
			
		||||
	MainGPU            int     `json:"main_gpu,omitempty"`
 | 
			
		||||
	LowVRAM            bool    `json:"low_vram,omitempty"`
 | 
			
		||||
	F16KV              bool    `json:"f16_kv,omitempty"`
 | 
			
		||||
	LogitsAll          bool    `json:"logits_all,omitempty"`
 | 
			
		||||
	VocabOnly          bool    `json:"vocab_only,omitempty"`
 | 
			
		||||
	UseMMap            bool    `json:"use_mmap,omitempty"`
 | 
			
		||||
	UseMLock           bool    `json:"use_mlock,omitempty"`
 | 
			
		||||
	EmbeddingOnly      bool    `json:"embedding_only,omitempty"`
 | 
			
		||||
	RopeFrequencyBase  float32 `json:"rope_frequency_base,omitempty"`
 | 
			
		||||
	RopeFrequencyScale float32 `json:"rope_frequency_scale,omitempty"`
 | 
			
		||||
	NumThread          int     `json:"num_thread,omitempty"`
 | 
			
		||||
	NumCtx    int   `json:"num_ctx,omitempty"`
 | 
			
		||||
	NumBatch  int   `json:"num_batch,omitempty"`
 | 
			
		||||
	NumGPU    int   `json:"num_gpu,omitempty"`
 | 
			
		||||
	MainGPU   int   `json:"main_gpu,omitempty"`
 | 
			
		||||
	UseMMap   *bool `json:"use_mmap,omitempty"`
 | 
			
		||||
	NumThread int   `json:"num_thread,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EmbedRequest is the request passed to [Client.Embed].
 | 
			
		||||
type EmbedRequest struct {
 | 
			
		||||
	// Model is the model name.
 | 
			
		||||
	Model string `json:"model"`
 | 
			
		||||
 | 
			
		||||
	// Input is the input to embed.
 | 
			
		||||
	Input any `json:"input"`
 | 
			
		||||
 | 
			
		||||
	// KeepAlive controls how long the model will stay loaded in memory following
 | 
			
		||||
	// this request.
 | 
			
		||||
	KeepAlive *Duration `json:"keep_alive,omitempty"`
 | 
			
		||||
 | 
			
		||||
	Truncate *bool `json:"truncate,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// Options lists model-specific options.
 | 
			
		||||
	Options map[string]any `json:"options"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EmbedResponse is the response from [Client.Embed].
 | 
			
		||||
type EmbedResponse struct {
 | 
			
		||||
	Model      string      `json:"model"`
 | 
			
		||||
	Embeddings [][]float32 `json:"embeddings"`
 | 
			
		||||
 | 
			
		||||
	TotalDuration   time.Duration `json:"total_duration,omitempty"`
 | 
			
		||||
	LoadDuration    time.Duration `json:"load_duration,omitempty"`
 | 
			
		||||
	PromptEvalCount int           `json:"prompt_eval_count,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EmbeddingRequest is the request passed to [Client.Embeddings].
 | 
			
		||||
type EmbeddingRequest struct {
 | 
			
		||||
	Model  string `json:"model"`
 | 
			
		||||
	// Model is the model name.
 | 
			
		||||
	Model string `json:"model"`
 | 
			
		||||
 | 
			
		||||
	// Prompt is the textual prompt to embed.
 | 
			
		||||
	Prompt string `json:"prompt"`
 | 
			
		||||
 | 
			
		||||
	Options map[string]interface{} `json:"options"`
 | 
			
		||||
	// KeepAlive controls how long the model will stay loaded in memory following
 | 
			
		||||
	// this request.
 | 
			
		||||
	KeepAlive *Duration `json:"keep_alive,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// Options lists model-specific options.
 | 
			
		||||
	Options map[string]any `json:"options"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EmbeddingResponse is the response from [Client.Embeddings].
 | 
			
		||||
type EmbeddingResponse struct {
 | 
			
		||||
	Embedding []float64 `json:"embedding"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateRequest is the request passed to [Client.Create].
 | 
			
		||||
type CreateRequest struct {
 | 
			
		||||
	Model     string `json:"model"`
 | 
			
		||||
	Path      string `json:"path"`
 | 
			
		||||
	Modelfile string `json:"modelfile"`
 | 
			
		||||
	Stream    *bool  `json:"stream,omitempty"`
 | 
			
		||||
	Model    string `json:"model"`
 | 
			
		||||
	Stream   *bool  `json:"stream,omitempty"`
 | 
			
		||||
	Quantize string `json:"quantize,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// Name is deprecated, see Model
 | 
			
		||||
	From       string            `json:"from,omitempty"`
 | 
			
		||||
	Files      map[string]string `json:"files,omitempty"`
 | 
			
		||||
	Adapters   map[string]string `json:"adapters,omitempty"`
 | 
			
		||||
	Template   string            `json:"template,omitempty"`
 | 
			
		||||
	License    any               `json:"license,omitempty"`
 | 
			
		||||
	System     string            `json:"system,omitempty"`
 | 
			
		||||
	Parameters map[string]any    `json:"parameters,omitempty"`
 | 
			
		||||
	Messages   []Message         `json:"messages,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// Deprecated: set the model name with Model instead
 | 
			
		||||
	Name string `json:"name"`
 | 
			
		||||
	// Deprecated: use Quantize instead
 | 
			
		||||
	Quantization string `json:"quantization,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteRequest is the request passed to [Client.Delete].
 | 
			
		||||
type DeleteRequest struct {
 | 
			
		||||
	Model string `json:"model"`
 | 
			
		||||
 | 
			
		||||
	// Name is deprecated, see Model
 | 
			
		||||
	// Deprecated: set the model name with Model instead
 | 
			
		||||
	Name string `json:"name"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ShowRequest is the request passed to [Client.Show].
 | 
			
		||||
type ShowRequest struct {
 | 
			
		||||
	Model    string `json:"model"`
 | 
			
		||||
	System   string `json:"system"`
 | 
			
		||||
	Model  string `json:"model"`
 | 
			
		||||
	System string `json:"system"`
 | 
			
		||||
 | 
			
		||||
	// Template is deprecated
 | 
			
		||||
	Template string `json:"template"`
 | 
			
		||||
	Verbose  bool   `json:"verbose"`
 | 
			
		||||
 | 
			
		||||
	Options map[string]interface{} `json:"options"`
 | 
			
		||||
	Options map[string]any `json:"options"`
 | 
			
		||||
 | 
			
		||||
	// Name is deprecated, see Model
 | 
			
		||||
	// Deprecated: set the model name with Model instead
 | 
			
		||||
	Name string `json:"name"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ShowResponse is the response returned from [Client.Show].
 | 
			
		||||
type ShowResponse struct {
 | 
			
		||||
	License    string       `json:"license,omitempty"`
 | 
			
		||||
	Modelfile  string       `json:"modelfile,omitempty"`
 | 
			
		||||
	Parameters string       `json:"parameters,omitempty"`
 | 
			
		||||
	Template   string       `json:"template,omitempty"`
 | 
			
		||||
	System     string       `json:"system,omitempty"`
 | 
			
		||||
	Details    ModelDetails `json:"details,omitempty"`
 | 
			
		||||
	License       string             `json:"license,omitempty"`
 | 
			
		||||
	Modelfile     string             `json:"modelfile,omitempty"`
 | 
			
		||||
	Parameters    string             `json:"parameters,omitempty"`
 | 
			
		||||
	Template      string             `json:"template,omitempty"`
 | 
			
		||||
	System        string             `json:"system,omitempty"`
 | 
			
		||||
	Details       ModelDetails       `json:"details,omitempty"`
 | 
			
		||||
	Messages      []Message          `json:"messages,omitempty"`
 | 
			
		||||
	ModelInfo     map[string]any     `json:"model_info,omitempty"`
 | 
			
		||||
	ProjectorInfo map[string]any     `json:"projector_info,omitempty"`
 | 
			
		||||
	Tensors       []Tensor           `json:"tensors,omitempty"`
 | 
			
		||||
	Capabilities  []model.Capability `json:"capabilities,omitempty"`
 | 
			
		||||
	ModifiedAt    time.Time          `json:"modified_at,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CopyRequest is the request passed to [Client.Copy].
 | 
			
		||||
type CopyRequest struct {
 | 
			
		||||
	Source      string `json:"source"`
 | 
			
		||||
	Destination string `json:"destination"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PullRequest is the request passed to [Client.Pull].
 | 
			
		||||
type PullRequest struct {
 | 
			
		||||
	Model    string `json:"model"`
 | 
			
		||||
	Insecure bool   `json:"insecure,omitempty"`
 | 
			
		||||
	Username string `json:"username"`
 | 
			
		||||
	Password string `json:"password"`
 | 
			
		||||
	Insecure bool   `json:"insecure,omitempty"` // Deprecated: ignored
 | 
			
		||||
	Username string `json:"username"`           // Deprecated: ignored
 | 
			
		||||
	Password string `json:"password"`           // Deprecated: ignored
 | 
			
		||||
	Stream   *bool  `json:"stream,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// Name is deprecated, see Model
 | 
			
		||||
	// Deprecated: set the model name with Model instead
 | 
			
		||||
	Name string `json:"name"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ProgressResponse is the response passed to progress functions like
 | 
			
		||||
// [PullProgressFunc] and [PushProgressFunc].
 | 
			
		||||
type ProgressResponse struct {
 | 
			
		||||
	Status    string `json:"status"`
 | 
			
		||||
	Digest    string `json:"digest,omitempty"`
 | 
			
		||||
@@ -196,6 +420,7 @@ type ProgressResponse struct {
 | 
			
		||||
	Completed int64  `json:"completed,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PushRequest is the request passed to [Client.Push].
 | 
			
		||||
type PushRequest struct {
 | 
			
		||||
	Model    string `json:"model"`
 | 
			
		||||
	Insecure bool   `json:"insecure,omitempty"`
 | 
			
		||||
@@ -203,15 +428,22 @@ type PushRequest struct {
 | 
			
		||||
	Password string `json:"password"`
 | 
			
		||||
	Stream   *bool  `json:"stream,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// Name is deprecated, see Model
 | 
			
		||||
	// Deprecated: set the model name with Model instead
 | 
			
		||||
	Name string `json:"name"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListResponse is the response from [Client.List].
 | 
			
		||||
type ListResponse struct {
 | 
			
		||||
	Models []ModelResponse `json:"models"`
 | 
			
		||||
	Models []ListModelResponse `json:"models"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ModelResponse struct {
 | 
			
		||||
// ProcessResponse is the response from [Client.Process].
 | 
			
		||||
type ProcessResponse struct {
 | 
			
		||||
	Models []ProcessModelResponse `json:"models"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListModelResponse is a single model description in [ListResponse].
 | 
			
		||||
type ListModelResponse struct {
 | 
			
		||||
	Name       string       `json:"name"`
 | 
			
		||||
	Model      string       `json:"model"`
 | 
			
		||||
	ModifiedAt time.Time    `json:"modified_at"`
 | 
			
		||||
@@ -220,22 +452,48 @@ type ModelResponse struct {
 | 
			
		||||
	Details    ModelDetails `json:"details,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ProcessModelResponse is a single model description in [ProcessResponse].
 | 
			
		||||
type ProcessModelResponse struct {
 | 
			
		||||
	Name      string       `json:"name"`
 | 
			
		||||
	Model     string       `json:"model"`
 | 
			
		||||
	Size      int64        `json:"size"`
 | 
			
		||||
	Digest    string       `json:"digest"`
 | 
			
		||||
	Details   ModelDetails `json:"details,omitempty"`
 | 
			
		||||
	ExpiresAt time.Time    `json:"expires_at"`
 | 
			
		||||
	SizeVRAM  int64        `json:"size_vram"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TokenResponse struct {
 | 
			
		||||
	Token string `json:"token"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GenerateResponse is the response passed into [GenerateResponseFunc].
 | 
			
		||||
type GenerateResponse struct {
 | 
			
		||||
	Model     string    `json:"model"`
 | 
			
		||||
	CreatedAt time.Time `json:"created_at"`
 | 
			
		||||
	Response  string    `json:"response"`
 | 
			
		||||
	// Model is the model name that generated the response.
 | 
			
		||||
	Model string `json:"model"`
 | 
			
		||||
 | 
			
		||||
	Done    bool  `json:"done"`
 | 
			
		||||
	// CreatedAt is the timestamp of the response.
 | 
			
		||||
	CreatedAt time.Time `json:"created_at"`
 | 
			
		||||
 | 
			
		||||
	// Response is the textual response itself.
 | 
			
		||||
	Response string `json:"response"`
 | 
			
		||||
 | 
			
		||||
	// Done specifies if the response is complete.
 | 
			
		||||
	Done bool `json:"done"`
 | 
			
		||||
 | 
			
		||||
	// DoneReason is the reason the model stopped generating text.
 | 
			
		||||
	DoneReason string `json:"done_reason,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// Context is an encoding of the conversation used in this response; this
 | 
			
		||||
	// can be sent in the next request to keep a conversational memory.
 | 
			
		||||
	Context []int `json:"context,omitempty"`
 | 
			
		||||
 | 
			
		||||
	Metrics
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ModelDetails provides details about a model.
 | 
			
		||||
type ModelDetails struct {
 | 
			
		||||
	ParentModel       string   `json:"parent_model"`
 | 
			
		||||
	Format            string   `json:"format"`
 | 
			
		||||
	Family            string   `json:"family"`
 | 
			
		||||
	Families          []string `json:"families"`
 | 
			
		||||
@@ -243,6 +501,13 @@ type ModelDetails struct {
 | 
			
		||||
	QuantizationLevel string   `json:"quantization_level"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Tensor describes the metadata for a given tensor.
 | 
			
		||||
type Tensor struct {
 | 
			
		||||
	Name  string   `json:"name"`
 | 
			
		||||
	Type  string   `json:"type"`
 | 
			
		||||
	Shape []uint64 `json:"shape"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Metrics) Summary() {
 | 
			
		||||
	if m.TotalDuration > 0 {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "total duration:       %v\n", m.TotalDuration)
 | 
			
		||||
@@ -271,9 +536,7 @@ func (m *Metrics) Summary() {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var ErrInvalidOpts = fmt.Errorf("invalid options")
 | 
			
		||||
 | 
			
		||||
func (opts *Options) FromMap(m map[string]interface{}) error {
 | 
			
		||||
func (opts *Options) FromMap(m map[string]any) error {
 | 
			
		||||
	valueOpts := reflect.ValueOf(opts).Elem() // names of the fields in the options struct
 | 
			
		||||
	typeOpts := reflect.TypeOf(opts).Elem()   // types of the fields in the options struct
 | 
			
		||||
 | 
			
		||||
@@ -286,111 +549,111 @@ func (opts *Options) FromMap(m map[string]interface{}) error {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	invalidOpts := []string{}
 | 
			
		||||
	for key, val := range m {
 | 
			
		||||
		if opt, ok := jsonOpts[key]; ok {
 | 
			
		||||
			field := valueOpts.FieldByName(opt.Name)
 | 
			
		||||
			if field.IsValid() && field.CanSet() {
 | 
			
		||||
				if val == nil {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
		opt, ok := jsonOpts[key]
 | 
			
		||||
		if !ok {
 | 
			
		||||
			slog.Warn("invalid option provided", "option", key)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
				switch field.Kind() {
 | 
			
		||||
				case reflect.Int:
 | 
			
		||||
					switch t := val.(type) {
 | 
			
		||||
					case int64:
 | 
			
		||||
						field.SetInt(t)
 | 
			
		||||
					case float64:
 | 
			
		||||
						// when JSON unmarshals numbers, it uses float64, not int
 | 
			
		||||
						field.SetInt(int64(t))
 | 
			
		||||
					default:
 | 
			
		||||
						return fmt.Errorf("option %q must be of type integer", key)
 | 
			
		||||
		field := valueOpts.FieldByName(opt.Name)
 | 
			
		||||
		if field.IsValid() && field.CanSet() {
 | 
			
		||||
			if val == nil {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			switch field.Kind() {
 | 
			
		||||
			case reflect.Int:
 | 
			
		||||
				switch t := val.(type) {
 | 
			
		||||
				case int64:
 | 
			
		||||
					field.SetInt(t)
 | 
			
		||||
				case float64:
 | 
			
		||||
					// when JSON unmarshals numbers, it uses float64, not int
 | 
			
		||||
					field.SetInt(int64(t))
 | 
			
		||||
				default:
 | 
			
		||||
					return fmt.Errorf("option %q must be of type integer", key)
 | 
			
		||||
				}
 | 
			
		||||
			case reflect.Bool:
 | 
			
		||||
				val, ok := val.(bool)
 | 
			
		||||
				if !ok {
 | 
			
		||||
					return fmt.Errorf("option %q must be of type boolean", key)
 | 
			
		||||
				}
 | 
			
		||||
				field.SetBool(val)
 | 
			
		||||
			case reflect.Float32:
 | 
			
		||||
				// JSON unmarshals to float64
 | 
			
		||||
				val, ok := val.(float64)
 | 
			
		||||
				if !ok {
 | 
			
		||||
					return fmt.Errorf("option %q must be of type float32", key)
 | 
			
		||||
				}
 | 
			
		||||
				field.SetFloat(val)
 | 
			
		||||
			case reflect.String:
 | 
			
		||||
				val, ok := val.(string)
 | 
			
		||||
				if !ok {
 | 
			
		||||
					return fmt.Errorf("option %q must be of type string", key)
 | 
			
		||||
				}
 | 
			
		||||
				field.SetString(val)
 | 
			
		||||
			case reflect.Slice:
 | 
			
		||||
				// JSON unmarshals to []any, not []string
 | 
			
		||||
				val, ok := val.([]any)
 | 
			
		||||
				if !ok {
 | 
			
		||||
					return fmt.Errorf("option %q must be of type array", key)
 | 
			
		||||
				}
 | 
			
		||||
				// convert []any to []string
 | 
			
		||||
				slice := make([]string, len(val))
 | 
			
		||||
				for i, item := range val {
 | 
			
		||||
					str, ok := item.(string)
 | 
			
		||||
					if !ok {
 | 
			
		||||
						return fmt.Errorf("option %q must be of an array of strings", key)
 | 
			
		||||
					}
 | 
			
		||||
				case reflect.Bool:
 | 
			
		||||
					slice[i] = str
 | 
			
		||||
				}
 | 
			
		||||
				field.Set(reflect.ValueOf(slice))
 | 
			
		||||
			case reflect.Pointer:
 | 
			
		||||
				var b bool
 | 
			
		||||
				if field.Type() == reflect.TypeOf(&b) {
 | 
			
		||||
					val, ok := val.(bool)
 | 
			
		||||
					if !ok {
 | 
			
		||||
						return fmt.Errorf("option %q must be of type boolean", key)
 | 
			
		||||
					}
 | 
			
		||||
					field.SetBool(val)
 | 
			
		||||
				case reflect.Float32:
 | 
			
		||||
					// JSON unmarshals to float64
 | 
			
		||||
					val, ok := val.(float64)
 | 
			
		||||
					if !ok {
 | 
			
		||||
						return fmt.Errorf("option %q must be of type float32", key)
 | 
			
		||||
					}
 | 
			
		||||
					field.SetFloat(val)
 | 
			
		||||
				case reflect.String:
 | 
			
		||||
					val, ok := val.(string)
 | 
			
		||||
					if !ok {
 | 
			
		||||
						return fmt.Errorf("option %q must be of type string", key)
 | 
			
		||||
					}
 | 
			
		||||
					field.SetString(val)
 | 
			
		||||
				case reflect.Slice:
 | 
			
		||||
					// JSON unmarshals to []interface{}, not []string
 | 
			
		||||
					val, ok := val.([]interface{})
 | 
			
		||||
					if !ok {
 | 
			
		||||
						return fmt.Errorf("option %q must be of type array", key)
 | 
			
		||||
					}
 | 
			
		||||
					// convert []interface{} to []string
 | 
			
		||||
					slice := make([]string, len(val))
 | 
			
		||||
					for i, item := range val {
 | 
			
		||||
						str, ok := item.(string)
 | 
			
		||||
						if !ok {
 | 
			
		||||
							return fmt.Errorf("option %q must be of an array of strings", key)
 | 
			
		||||
						}
 | 
			
		||||
						slice[i] = str
 | 
			
		||||
					}
 | 
			
		||||
					field.Set(reflect.ValueOf(slice))
 | 
			
		||||
				default:
 | 
			
		||||
					return fmt.Errorf("unknown type loading config params: %v", field.Kind())
 | 
			
		||||
					field.Set(reflect.ValueOf(&val))
 | 
			
		||||
				} else {
 | 
			
		||||
					return fmt.Errorf("unknown type loading config params: %v %v", field.Kind(), field.Type())
 | 
			
		||||
				}
 | 
			
		||||
			default:
 | 
			
		||||
				return fmt.Errorf("unknown type loading config params: %v", field.Kind())
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			invalidOpts = append(invalidOpts, key)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(invalidOpts) > 0 {
 | 
			
		||||
		return fmt.Errorf("%w: %v", ErrInvalidOpts, strings.Join(invalidOpts, ", "))
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DefaultOptions is the default set of options for [GenerateRequest]; these
 | 
			
		||||
// values are used unless the user specifies other values explicitly.
 | 
			
		||||
func DefaultOptions() Options {
 | 
			
		||||
	return Options{
 | 
			
		||||
		// options set on request to runner
 | 
			
		||||
		NumPredict:       -1,
 | 
			
		||||
		NumKeep:          0,
 | 
			
		||||
		NumPredict: -1,
 | 
			
		||||
 | 
			
		||||
		// set a minimal num_keep to avoid issues on context shifts
 | 
			
		||||
		NumKeep:          4,
 | 
			
		||||
		Temperature:      0.8,
 | 
			
		||||
		TopK:             40,
 | 
			
		||||
		TopP:             0.9,
 | 
			
		||||
		TFSZ:             1.0,
 | 
			
		||||
		TypicalP:         1.0,
 | 
			
		||||
		RepeatLastN:      64,
 | 
			
		||||
		RepeatPenalty:    1.1,
 | 
			
		||||
		PresencePenalty:  0.0,
 | 
			
		||||
		FrequencyPenalty: 0.0,
 | 
			
		||||
		Mirostat:         0,
 | 
			
		||||
		MirostatTau:      5.0,
 | 
			
		||||
		MirostatEta:      0.1,
 | 
			
		||||
		PenalizeNewline:  true,
 | 
			
		||||
		Seed:             -1,
 | 
			
		||||
 | 
			
		||||
		Runner: Runner{
 | 
			
		||||
			// options set when the model is loaded
 | 
			
		||||
			NumCtx:             2048,
 | 
			
		||||
			RopeFrequencyBase:  10000.0,
 | 
			
		||||
			RopeFrequencyScale: 1.0,
 | 
			
		||||
			NumBatch:           512,
 | 
			
		||||
			NumGPU:             -1, // -1 here indicates that NumGPU should be set dynamically
 | 
			
		||||
			NumGQA:             1,
 | 
			
		||||
			NumThread:          0, // let the runtime decide
 | 
			
		||||
			LowVRAM:            false,
 | 
			
		||||
			F16KV:              true,
 | 
			
		||||
			UseMLock:           false,
 | 
			
		||||
			UseMMap:            true,
 | 
			
		||||
			UseNUMA:            false,
 | 
			
		||||
			EmbeddingOnly:      true,
 | 
			
		||||
			NumCtx:    int(envconfig.ContextLength()),
 | 
			
		||||
			NumBatch:  512,
 | 
			
		||||
			NumGPU:    -1, // -1 here indicates that NumGPU should be set dynamically
 | 
			
		||||
			NumThread: 0,  // let the runtime decide
 | 
			
		||||
			UseMMap:   nil,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -399,6 +662,13 @@ type Duration struct {
 | 
			
		||||
	time.Duration
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d Duration) MarshalJSON() ([]byte, error) {
 | 
			
		||||
	if d.Duration < 0 {
 | 
			
		||||
		return []byte("-1"), nil
 | 
			
		||||
	}
 | 
			
		||||
	return []byte("\"" + d.Duration.String() + "\""), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Duration) UnmarshalJSON(b []byte) (err error) {
 | 
			
		||||
	var v any
 | 
			
		||||
	if err := json.Unmarshal(b, &v); err != nil {
 | 
			
		||||
@@ -410,22 +680,27 @@ func (d *Duration) UnmarshalJSON(b []byte) (err error) {
 | 
			
		||||
	switch t := v.(type) {
 | 
			
		||||
	case float64:
 | 
			
		||||
		if t < 0 {
 | 
			
		||||
			t = math.MaxFloat64
 | 
			
		||||
			d.Duration = time.Duration(math.MaxInt64)
 | 
			
		||||
		} else {
 | 
			
		||||
			d.Duration = time.Duration(int(t) * int(time.Second))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		d.Duration = time.Duration(t)
 | 
			
		||||
	case string:
 | 
			
		||||
		d.Duration, err = time.ParseDuration(t)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if d.Duration < 0 {
 | 
			
		||||
			d.Duration = time.Duration(math.MaxInt64)
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("Unsupported type: '%s'", reflect.TypeOf(v))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormatParams converts specified parameter options to their correct types
 | 
			
		||||
func FormatParams(params map[string][]string) (map[string]interface{}, error) {
 | 
			
		||||
func FormatParams(params map[string][]string) (map[string]any, error) {
 | 
			
		||||
	opts := Options{}
 | 
			
		||||
	valueOpts := reflect.ValueOf(&opts).Elem() // names of the fields in the options struct
 | 
			
		||||
	typeOpts := reflect.TypeOf(opts)           // types of the fields in the options struct
 | 
			
		||||
@@ -439,7 +714,7 @@ func FormatParams(params map[string][]string) (map[string]interface{}, error) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	out := make(map[string]interface{})
 | 
			
		||||
	out := make(map[string]any)
 | 
			
		||||
	// iterate params and set values based on json struct tags
 | 
			
		||||
	for key, vals := range params {
 | 
			
		||||
		if opt, ok := jsonOpts[key]; !ok {
 | 
			
		||||
@@ -474,6 +749,17 @@ func FormatParams(params map[string][]string) (map[string]interface{}, error) {
 | 
			
		||||
				case reflect.Slice:
 | 
			
		||||
					// TODO: only string slices are supported right now
 | 
			
		||||
					out[key] = vals
 | 
			
		||||
				case reflect.Pointer:
 | 
			
		||||
					var b bool
 | 
			
		||||
					if field.Type() == reflect.TypeOf(&b) {
 | 
			
		||||
						boolVal, err := strconv.ParseBool(vals[0])
 | 
			
		||||
						if err != nil {
 | 
			
		||||
							return nil, fmt.Errorf("invalid bool value %s", vals)
 | 
			
		||||
						}
 | 
			
		||||
						out[key] = &boolVal
 | 
			
		||||
					} else {
 | 
			
		||||
						return nil, fmt.Errorf("unknown type %s for %s", field.Kind(), key)
 | 
			
		||||
					}
 | 
			
		||||
				default:
 | 
			
		||||
					return nil, fmt.Errorf("unknown type %s for %s", field.Kind(), key)
 | 
			
		||||
				}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										374
									
								
								api/types_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										374
									
								
								api/types_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,374 @@
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"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 Float",
 | 
			
		||||
			req:  `{ "keep_alive": 42.5 }`,
 | 
			
		||||
			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 Float",
 | 
			
		||||
			req:  `{ "keep_alive": -3.14 }`,
 | 
			
		||||
			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)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDurationMarshalUnmarshal(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name     string
 | 
			
		||||
		input    time.Duration
 | 
			
		||||
		expected time.Duration
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			"negative duration",
 | 
			
		||||
			time.Duration(-1),
 | 
			
		||||
			time.Duration(math.MaxInt64),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"positive duration",
 | 
			
		||||
			42 * time.Second,
 | 
			
		||||
			42 * time.Second,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"another positive duration",
 | 
			
		||||
			42 * time.Minute,
 | 
			
		||||
			42 * time.Minute,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"zero duration",
 | 
			
		||||
			time.Duration(0),
 | 
			
		||||
			time.Duration(0),
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"max duration",
 | 
			
		||||
			time.Duration(math.MaxInt64),
 | 
			
		||||
			time.Duration(math.MaxInt64),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		t.Run(test.name, func(t *testing.T) {
 | 
			
		||||
			b, err := json.Marshal(Duration{test.input})
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
			var d Duration
 | 
			
		||||
			err = json.Unmarshal(b, &d)
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
			assert.Equal(t, test.expected, d.Duration, "input %v, marshalled %v, got %v", test.input, string(b), d.Duration)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestUseMmapParsingFromJSON(t *testing.T) {
 | 
			
		||||
	tr := true
 | 
			
		||||
	fa := false
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name string
 | 
			
		||||
		req  string
 | 
			
		||||
		exp  *bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "Undefined",
 | 
			
		||||
			req:  `{ }`,
 | 
			
		||||
			exp:  nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "True",
 | 
			
		||||
			req:  `{ "use_mmap": true }`,
 | 
			
		||||
			exp:  &tr,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "False",
 | 
			
		||||
			req:  `{ "use_mmap": false }`,
 | 
			
		||||
			exp:  &fa,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		t.Run(test.name, func(t *testing.T) {
 | 
			
		||||
			var oMap map[string]any
 | 
			
		||||
			err := json.Unmarshal([]byte(test.req), &oMap)
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
			opts := DefaultOptions()
 | 
			
		||||
			err = opts.FromMap(oMap)
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
			assert.Equal(t, test.exp, opts.UseMMap)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestUseMmapFormatParams(t *testing.T) {
 | 
			
		||||
	tr := true
 | 
			
		||||
	fa := false
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name string
 | 
			
		||||
		req  map[string][]string
 | 
			
		||||
		exp  *bool
 | 
			
		||||
		err  error
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "True",
 | 
			
		||||
			req: map[string][]string{
 | 
			
		||||
				"use_mmap": {"true"},
 | 
			
		||||
			},
 | 
			
		||||
			exp: &tr,
 | 
			
		||||
			err: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "False",
 | 
			
		||||
			req: map[string][]string{
 | 
			
		||||
				"use_mmap": {"false"},
 | 
			
		||||
			},
 | 
			
		||||
			exp: &fa,
 | 
			
		||||
			err: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Numeric True",
 | 
			
		||||
			req: map[string][]string{
 | 
			
		||||
				"use_mmap": {"1"},
 | 
			
		||||
			},
 | 
			
		||||
			exp: &tr,
 | 
			
		||||
			err: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Numeric False",
 | 
			
		||||
			req: map[string][]string{
 | 
			
		||||
				"use_mmap": {"0"},
 | 
			
		||||
			},
 | 
			
		||||
			exp: &fa,
 | 
			
		||||
			err: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "invalid string",
 | 
			
		||||
			req: map[string][]string{
 | 
			
		||||
				"use_mmap": {"foo"},
 | 
			
		||||
			},
 | 
			
		||||
			exp: nil,
 | 
			
		||||
			err: errors.New("invalid bool value [foo]"),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		t.Run(test.name, func(t *testing.T) {
 | 
			
		||||
			resp, err := FormatParams(test.req)
 | 
			
		||||
			require.Equal(t, test.err, err)
 | 
			
		||||
			respVal, ok := resp["use_mmap"]
 | 
			
		||||
			if test.exp != nil {
 | 
			
		||||
				assert.True(t, ok, "resp: %v", resp)
 | 
			
		||||
				assert.Equal(t, *test.exp, *respVal.(*bool))
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMessage_UnmarshalJSON(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		input    string
 | 
			
		||||
		expected string
 | 
			
		||||
	}{
 | 
			
		||||
		{`{"role": "USER", "content": "Hello!"}`, "user"},
 | 
			
		||||
		{`{"role": "System", "content": "Initialization complete."}`, "system"},
 | 
			
		||||
		{`{"role": "assistant", "content": "How can I help you?"}`, "assistant"},
 | 
			
		||||
		{`{"role": "TOOl", "content": "Access granted."}`, "tool"},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		var msg Message
 | 
			
		||||
		if err := json.Unmarshal([]byte(test.input), &msg); err != nil {
 | 
			
		||||
			t.Errorf("Unexpected error: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if msg.Role != test.expected {
 | 
			
		||||
			t.Errorf("role not lowercased: got %v, expected %v", msg.Role, test.expected)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestToolFunction_UnmarshalJSON(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		input   string
 | 
			
		||||
		wantErr string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "valid enum with same types",
 | 
			
		||||
			input: `{
 | 
			
		||||
				"name": "test",
 | 
			
		||||
				"description": "test function",
 | 
			
		||||
				"parameters": {
 | 
			
		||||
					"type": "object",
 | 
			
		||||
					"required": ["test"],
 | 
			
		||||
					"properties": {
 | 
			
		||||
						"test": {
 | 
			
		||||
							"type": "string",
 | 
			
		||||
							"description": "test prop",
 | 
			
		||||
							"enum": ["a", "b", "c"]
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}`,
 | 
			
		||||
			wantErr: "",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "empty enum array",
 | 
			
		||||
			input: `{
 | 
			
		||||
				"name": "test",
 | 
			
		||||
				"description": "test function",
 | 
			
		||||
				"parameters": {
 | 
			
		||||
					"type": "object",
 | 
			
		||||
					"required": ["test"],
 | 
			
		||||
					"properties": {
 | 
			
		||||
						"test": {
 | 
			
		||||
							"type": "string",
 | 
			
		||||
							"description": "test prop",
 | 
			
		||||
							"enum": []
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}`,
 | 
			
		||||
			wantErr: "",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			var tf ToolFunction
 | 
			
		||||
			err := json.Unmarshal([]byte(tt.input), &tf)
 | 
			
		||||
 | 
			
		||||
			if tt.wantErr != "" {
 | 
			
		||||
				require.Error(t, err)
 | 
			
		||||
				assert.Contains(t, err.Error(), tt.wantErr)
 | 
			
		||||
			} else {
 | 
			
		||||
				require.NoError(t, err)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPropertyType_UnmarshalJSON(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name     string
 | 
			
		||||
		input    string
 | 
			
		||||
		expected PropertyType
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:     "string type",
 | 
			
		||||
			input:    `"string"`,
 | 
			
		||||
			expected: PropertyType{"string"},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "array of types",
 | 
			
		||||
			input:    `["string", "number"]`,
 | 
			
		||||
			expected: PropertyType{"string", "number"},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "array with single type",
 | 
			
		||||
			input:    `["string"]`,
 | 
			
		||||
			expected: PropertyType{"string"},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		t.Run(test.name, func(t *testing.T) {
 | 
			
		||||
			var pt PropertyType
 | 
			
		||||
			if err := json.Unmarshal([]byte(test.input), &pt); err != nil {
 | 
			
		||||
				t.Errorf("Unexpected error: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if len(pt) != len(test.expected) {
 | 
			
		||||
				t.Errorf("Length mismatch: got %v, expected %v", len(pt), len(test.expected))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for i, v := range pt {
 | 
			
		||||
				if v != test.expected[i] {
 | 
			
		||||
					t.Errorf("Value mismatch at index %d: got %v, expected %v", i, v, test.expected[i])
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPropertyType_MarshalJSON(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name     string
 | 
			
		||||
		input    PropertyType
 | 
			
		||||
		expected string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:     "single type",
 | 
			
		||||
			input:    PropertyType{"string"},
 | 
			
		||||
			expected: `"string"`,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "multiple types",
 | 
			
		||||
			input:    PropertyType{"string", "number"},
 | 
			
		||||
			expected: `["string","number"]`,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:     "empty type",
 | 
			
		||||
			input:    PropertyType{},
 | 
			
		||||
			expected: `[]`,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		t.Run(test.name, func(t *testing.T) {
 | 
			
		||||
			data, err := json.Marshal(test.input)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Errorf("Unexpected error: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if string(data) != test.expected {
 | 
			
		||||
				t.Errorf("Marshaled data mismatch: got %v, expected %v", string(data), test.expected)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										93
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										93
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,92 +1 @@
 | 
			
		||||
# Logs
 | 
			
		||||
logs
 | 
			
		||||
*.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/
 | 
			
		||||
ollama.syso
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,22 @@
 | 
			
		||||
# Desktop
 | 
			
		||||
# Ollama App
 | 
			
		||||
 | 
			
		||||
This app builds upon Ollama to provide a desktop experience for running models.
 | 
			
		||||
## Linux
 | 
			
		||||
 | 
			
		||||
## Developing
 | 
			
		||||
TODO
 | 
			
		||||
 | 
			
		||||
First, build the `ollama` binary:
 | 
			
		||||
## MacOS
 | 
			
		||||
 | 
			
		||||
TODO
 | 
			
		||||
 | 
			
		||||
## 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.
 | 
			
		||||
 | 
			
		||||
```powershell
 | 
			
		||||
powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1
 | 
			
		||||
```
 | 
			
		||||
cd ..
 | 
			
		||||
go build .
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Then run the desktop app with `npm start`:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
cd app
 | 
			
		||||
npm install
 | 
			
		||||
npm start
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								app/assets/app.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/assets/app.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 7.3 KiB  | 
							
								
								
									
										17
									
								
								app/assets/assets.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								app/assets/setup.bmp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/assets/setup.bmp
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 76 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/assets/tray.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/assets/tray.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 89 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								app/assets/tray_upgrade.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/assets/tray_upgrade.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 91 KiB  | 
							
								
								
									
										9
									
								
								app/lifecycle/getstarted_nonwindows.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/lifecycle/getstarted_nonwindows.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
//go:build !windows
 | 
			
		||||
 | 
			
		||||
package lifecycle
 | 
			
		||||
 | 
			
		||||
import "errors"
 | 
			
		||||
 | 
			
		||||
func GetStarted() error {
 | 
			
		||||
	return errors.New("not implemented")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										43
									
								
								app/lifecycle/getstarted_windows.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/lifecycle/getstarted_windows.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
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()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										94
									
								
								app/lifecycle/lifecycle.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								app/lifecycle/lifecycle.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
package lifecycle
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"syscall"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/app/store"
 | 
			
		||||
	"github.com/ollama/ollama/app/tray"
 | 
			
		||||
	"github.com/ollama/ollama/envconfig"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Run() {
 | 
			
		||||
	InitLogging()
 | 
			
		||||
	slog.Info("app config", "env", envconfig.Values())
 | 
			
		||||
 | 
			
		||||
	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 := DoUpgrade(cancel, done)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					slog.Warn(fmt.Sprintf("upgrade attempt failed: %s", err))
 | 
			
		||||
				}
 | 
			
		||||
			case <-callbacks.ShowLogs:
 | 
			
		||||
				ShowLogs()
 | 
			
		||||
			case <-callbacks.DoFirstUse:
 | 
			
		||||
				err := GetStarted()
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					slog.Warn(fmt.Sprintf("Failed to launch getting started shell: %s", err))
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	// Are we first use?
 | 
			
		||||
	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)
 | 
			
		||||
	} else {
 | 
			
		||||
		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
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	StartBackgroundUpdaterChecker(ctx, t.UpdateAvailable)
 | 
			
		||||
 | 
			
		||||
	t.Run()
 | 
			
		||||
	cancel()
 | 
			
		||||
	slog.Info("Waiting for ollama server to shutdown...")
 | 
			
		||||
	if done != nil {
 | 
			
		||||
		<-done
 | 
			
		||||
	}
 | 
			
		||||
	slog.Info("Ollama app exiting")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										62
									
								
								app/lifecycle/logging.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								app/lifecycle/logging.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
package lifecycle
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/envconfig"
 | 
			
		||||
	"github.com/ollama/ollama/logutil"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func InitLogging() {
 | 
			
		||||
	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 {
 | 
			
		||||
		rotateLogs(AppLogFile)
 | 
			
		||||
		logFile, err = os.OpenFile(AppLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o755)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			slog.Error(fmt.Sprintf("failed to create server log %v", err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	slog.SetDefault(logutil.NewLogger(logFile, envconfig.LogLevel()))
 | 
			
		||||
	slog.Info("ollama app started")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func rotateLogs(logFile string) {
 | 
			
		||||
	if _, err := os.Stat(logFile); os.IsNotExist(err) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	index := strings.LastIndex(logFile, ".")
 | 
			
		||||
	pre := logFile[:index]
 | 
			
		||||
	post := "." + logFile[index+1:]
 | 
			
		||||
	for i := LogRotationCount; i > 0; i-- {
 | 
			
		||||
		older := pre + "-" + strconv.Itoa(i) + post
 | 
			
		||||
		newer := pre + "-" + strconv.Itoa(i-1) + post
 | 
			
		||||
		if i == 1 {
 | 
			
		||||
			newer = pre + post
 | 
			
		||||
		}
 | 
			
		||||
		if _, err := os.Stat(newer); err == nil {
 | 
			
		||||
			if _, err := os.Stat(older); err == nil {
 | 
			
		||||
				err := os.Remove(older)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					slog.Warn("Failed to remove older log", "older", older, "error", err)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			err := os.Rename(newer, older)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				slog.Warn("Failed to rotate log", "older", older, "newer", newer, "error", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								app/lifecycle/logging_nonwindows.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/lifecycle/logging_nonwindows.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
//go:build !windows
 | 
			
		||||
 | 
			
		||||
package lifecycle
 | 
			
		||||
 | 
			
		||||
import "log/slog"
 | 
			
		||||
 | 
			
		||||
func ShowLogs() {
 | 
			
		||||
	slog.Warn("not implemented")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										44
									
								
								app/lifecycle/logging_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								app/lifecycle/logging_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
package lifecycle
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestRotateLogs(t *testing.T) {
 | 
			
		||||
	logDir := t.TempDir()
 | 
			
		||||
	logFile := filepath.Join(logDir, "testlog.log")
 | 
			
		||||
 | 
			
		||||
	// No log exists
 | 
			
		||||
	rotateLogs(logFile)
 | 
			
		||||
 | 
			
		||||
	require.NoError(t, os.WriteFile(logFile, []byte("1"), 0o644))
 | 
			
		||||
	assert.FileExists(t, logFile)
 | 
			
		||||
	// First rotation
 | 
			
		||||
	rotateLogs(logFile)
 | 
			
		||||
	assert.FileExists(t, filepath.Join(logDir, "testlog-1.log"))
 | 
			
		||||
	assert.NoFileExists(t, filepath.Join(logDir, "testlog-2.log"))
 | 
			
		||||
	assert.NoFileExists(t, logFile)
 | 
			
		||||
 | 
			
		||||
	// Should be a no-op without a new log
 | 
			
		||||
	rotateLogs(logFile)
 | 
			
		||||
	assert.FileExists(t, filepath.Join(logDir, "testlog-1.log"))
 | 
			
		||||
	assert.NoFileExists(t, filepath.Join(logDir, "testlog-2.log"))
 | 
			
		||||
	assert.NoFileExists(t, logFile)
 | 
			
		||||
 | 
			
		||||
	for i := 2; i <= LogRotationCount+1; i++ {
 | 
			
		||||
		require.NoError(t, os.WriteFile(logFile, []byte(strconv.Itoa(i)), 0o644))
 | 
			
		||||
		assert.FileExists(t, logFile)
 | 
			
		||||
		rotateLogs(logFile)
 | 
			
		||||
		assert.NoFileExists(t, logFile)
 | 
			
		||||
		for j := 1; j < i; j++ {
 | 
			
		||||
			assert.FileExists(t, filepath.Join(logDir, "testlog-"+strconv.Itoa(j)+".log"))
 | 
			
		||||
		}
 | 
			
		||||
		assert.NoFileExists(t, filepath.Join(logDir, "testlog-"+strconv.Itoa(i+1)+".log"))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								app/lifecycle/logging_windows.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/lifecycle/logging_windows.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
package lifecycle
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"syscall"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										84
									
								
								app/lifecycle/paths.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								app/lifecycle/paths.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,84 @@
 | 
			
		||||
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"
 | 
			
		||||
	LogRotationCount = 5
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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")
 | 
			
		||||
 | 
			
		||||
		exe, err := os.Executable()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			slog.Warn("error discovering executable directory", "error", err)
 | 
			
		||||
			AppDir = filepath.Join(localAppData, "Programs", "Ollama")
 | 
			
		||||
		} else {
 | 
			
		||||
			AppDir = filepath.Dir(exe)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 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))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else if runtime.GOOS == "darwin" {
 | 
			
		||||
		// TODO
 | 
			
		||||
		AppName += ".app"
 | 
			
		||||
		// } else if runtime.GOOS == "linux" {
 | 
			
		||||
		// TODO
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										186
									
								
								app/lifecycle/server.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								app/lifecycle/server.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,186 @@
 | 
			
		||||
package lifecycle
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func getCLIFullPath(command string) string {
 | 
			
		||||
	var cmdPath string
 | 
			
		||||
	appExe, err := os.Executable()
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		// Check both the same location as the tray app, as well as ./bin
 | 
			
		||||
		cmdPath = filepath.Join(filepath.Dir(appExe), command)
 | 
			
		||||
		_, err := os.Stat(cmdPath)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return cmdPath
 | 
			
		||||
		}
 | 
			
		||||
		cmdPath = filepath.Join(filepath.Dir(appExe), "bin", command)
 | 
			
		||||
		_, err = os.Stat(cmdPath)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return cmdPath
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	cmdPath, err = exec.LookPath(command)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		_, err := os.Stat(cmdPath)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return cmdPath
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	pwd, err := os.Getwd()
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		cmdPath = filepath.Join(pwd, command)
 | 
			
		||||
		_, err = os.Stat(cmdPath)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return cmdPath
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return command
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func start(ctx context.Context, command string) (*exec.Cmd, error) {
 | 
			
		||||
	cmd := getCmd(ctx, getCLIFullPath(command))
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rotateLogs(ServerLogFile)
 | 
			
		||||
	logFile, err := os.OpenFile(ServerLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to create server log: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logDir := filepath.Dir(ServerLogFile)
 | 
			
		||||
	_, err = os.Stat(logDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if !errors.Is(err, os.ErrNotExist) {
 | 
			
		||||
			return nil, fmt.Errorf("stat ollama server log dir %s: %v", logDir, err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := os.MkdirAll(logDir, 0o755); err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("create ollama server log dir %s: %v", logDir, 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) (chan int, error) {
 | 
			
		||||
	done := make(chan int)
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		// Keep the server running unless we're shuttind down the app
 | 
			
		||||
		crashCount := 0
 | 
			
		||||
		for {
 | 
			
		||||
			slog.Info("starting server...")
 | 
			
		||||
			cmd, err := start(ctx, command)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				crashCount++
 | 
			
		||||
				slog.Error(fmt.Sprintf("failed to start server %s", err))
 | 
			
		||||
				time.Sleep(500 * time.Millisecond * time.Duration(crashCount))
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										38
									
								
								app/lifecycle/server_unix.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								app/lifecycle/server_unix.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
//go:build !windows
 | 
			
		||||
 | 
			
		||||
package lifecycle
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										91
									
								
								app/lifecycle/server_windows.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								app/lifecycle/server_windows.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
			
		||||
package lifecycle
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	}
 | 
			
		||||
	//nolint:errcheck
 | 
			
		||||
	defer dll.Release()
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
	//nolint:errcheck
 | 
			
		||||
	defer windows.CloseHandle(hProcess)
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										229
									
								
								app/lifecycle/updater.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								app/lifecycle/updater.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,229 @@
 | 
			
		||||
package lifecycle
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/rand"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"mime"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/auth"
 | 
			
		||||
	"github.com/ollama/ollama/version"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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", strconv.FormatInt(time.Now().Unix(), 10))
 | 
			
		||||
 | 
			
		||||
	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 == http.StatusNoContent {
 | 
			
		||||
		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 != http.StatusOK {
 | 
			
		||||
		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 != http.StatusOK {
 | 
			
		||||
		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 := Installer
 | 
			
		||||
	_, 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)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								app/lifecycle/updater_nonwindows.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/lifecycle/updater_nonwindows.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
//go:build !windows
 | 
			
		||||
 | 
			
		||||
package lifecycle
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func DoUpgrade(cancel context.CancelFunc, done chan int) error {
 | 
			
		||||
	return errors.New("not implemented")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										74
									
								
								app/lifecycle/updater_windows.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								app/lifecycle/updater_windows.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
package lifecycle
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func DoUpgrade(cancel context.CancelFunc, done chan int) error {
 | 
			
		||||
	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 errors.New("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 " + UpgradeLogFile)
 | 
			
		||||
 | 
			
		||||
	// make the upgrade show progress, but non interactive
 | 
			
		||||
	installArgs := []string{
 | 
			
		||||
		"/CLOSEAPPLICATIONS",                    // Quit the tray app if it's still running
 | 
			
		||||
		"/LOG=" + filepath.Base(UpgradeLogFile), // Only relative seems reliable, so set pwd
 | 
			
		||||
		"/FORCECLOSEAPPLICATIONS",               // Force close the tray app - might be needed
 | 
			
		||||
		"/SP",                                   // Skip the "This will install... Do you wish to continue" prompt
 | 
			
		||||
		"/NOCANCEL",                             // Disable the ability to cancel upgrade mid-flight to avoid partially installed upgrades
 | 
			
		||||
		"/SILENT",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 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(UpgradeLogFile)) //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 errors.New("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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								app/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/main.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
// Compile with the following to get rid of the cmd pop up on windows
 | 
			
		||||
// go build -ldflags="-H windowsgui" .
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/ollama/ollama/app/lifecycle"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	lifecycle.Run()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										204
									
								
								app/ollama.iss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								app/ollama.iss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,204 @@
 | 
			
		||||
; 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=x64compatible arm64
 | 
			
		||||
ArchitecturesInstallIn64BitMode=x64compatible 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
 | 
			
		||||
RestartIfNeededByRun=no
 | 
			
		||||
 | 
			
		||||
; https://jrsoftware.org/ishelp/index.php?topic=setup_wizardimagefile
 | 
			
		||||
WizardSmallImageFile=.\assets\setup.bmp
 | 
			
		||||
 | 
			
		||||
; Ollama requires Windows 10 22H2 or newer for proper unicode rendering
 | 
			
		||||
; TODO: consider setting this to 10.0.19045
 | 
			
		||||
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]
 | 
			
		||||
#if DirExists("..\dist\windows-amd64")
 | 
			
		||||
Source: "..\dist\windows-amd64-app.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}" ;Check: not IsArm64();  Flags: ignoreversion 64bit
 | 
			
		||||
Source: "..\dist\windows-amd64\ollama.exe"; DestDir: "{app}"; Check: not IsArm64(); Flags: ignoreversion 64bit
 | 
			
		||||
Source: "..\dist\windows-amd64\lib\ollama\*"; DestDir: "{app}\lib\ollama\"; Check: not IsArm64(); Flags: ignoreversion 64bit recursesubdirs
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#if DirExists("..\dist\windows-arm64")
 | 
			
		||||
Source: "..\dist\windows-arm64\vc_redist.arm64.exe"; DestDir: "{tmp}"; Check: IsArm64() and vc_redist_needed(); Flags: deleteafterinstall
 | 
			
		||||
Source: "..\dist\windows-arm64-app.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}" ;Check: IsArm64();  Flags: ignoreversion 64bit
 | 
			
		||||
Source: "..\dist\windows-arm64\ollama.exe"; DestDir: "{app}"; Check: IsArm64(); Flags: ignoreversion 64bit
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
Source: "..\dist\ollama_welcome.ps1"; DestDir: "{app}"; Flags: ignoreversion
 | 
			
		||||
Source: ".\assets\app.ico"; DestDir: "{app}"; Flags: ignoreversion
 | 
			
		||||
 | 
			
		||||
[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]
 | 
			
		||||
#if DirExists("..\dist\windows-arm64")
 | 
			
		||||
Filename: "{tmp}\vc_redist.arm64.exe"; Parameters: "/install /passive /norestart"; Check: IsArm64() and vc_redist_needed(); StatusMsg: "Installing VC++ Redistributables..."; Flags: waituntilterminated
 | 
			
		||||
#endif
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
[InstallDelete]
 | 
			
		||||
Type: filesandordirs; Name: "{%TEMP}\ollama*"
 | 
			
		||||
Type: filesandordirs; Name: "{%LOCALAPPDATA}\Programs\Ollama"
 | 
			
		||||
 | 
			
		||||
[Messages]
 | 
			
		||||
WizardReady=Ollama
 | 
			
		||||
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.2
 | 
			
		||||
;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;
 | 
			
		||||
 | 
			
		||||
{ --- VC Runtime libraries discovery code - Only install vc_redist if it isn't already installed ----- }
 | 
			
		||||
const VCRTL_MIN_V1 = 14;
 | 
			
		||||
const VCRTL_MIN_V2 = 40;
 | 
			
		||||
const VCRTL_MIN_V3 = 33807;
 | 
			
		||||
const VCRTL_MIN_V4 = 0;
 | 
			
		||||
 | 
			
		||||
 // check if the minimum required vc redist is installed (by looking the registry)
 | 
			
		||||
function vc_redist_needed (): Boolean;
 | 
			
		||||
var
 | 
			
		||||
  sRegKey: string;
 | 
			
		||||
  v1: Cardinal;
 | 
			
		||||
  v2: Cardinal;
 | 
			
		||||
  v3: Cardinal;
 | 
			
		||||
  v4: Cardinal;
 | 
			
		||||
begin
 | 
			
		||||
  sRegKey := 'SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\arm64';
 | 
			
		||||
  if (RegQueryDWordValue (HKEY_LOCAL_MACHINE, sRegKey, 'Major', v1)  and
 | 
			
		||||
      RegQueryDWordValue (HKEY_LOCAL_MACHINE, sRegKey, 'Minor', v2) and
 | 
			
		||||
      RegQueryDWordValue (HKEY_LOCAL_MACHINE, sRegKey, 'Bld', v3) and
 | 
			
		||||
      RegQueryDWordValue (HKEY_LOCAL_MACHINE, sRegKey, 'RBld', v4)) then
 | 
			
		||||
  begin
 | 
			
		||||
    Log ('VC Redist version: ' + IntToStr (v1) +
 | 
			
		||||
        '.' + IntToStr (v2) + '.' + IntToStr (v3) +
 | 
			
		||||
        '.' + IntToStr (v4));
 | 
			
		||||
    { Version info was found. Return true if later or equal to our
 | 
			
		||||
       minimal required version RTL_MIN_Vx }
 | 
			
		||||
    Result := not (
 | 
			
		||||
        (v1 > VCRTL_MIN_V1) or ((v1 = VCRTL_MIN_V1) and
 | 
			
		||||
         ((v2 > VCRTL_MIN_V2) or ((v2 = VCRTL_MIN_V2) and
 | 
			
		||||
          ((v3 > VCRTL_MIN_V3) or ((v3 = VCRTL_MIN_V3) and
 | 
			
		||||
           (v4 >= VCRTL_MIN_V4)))))));
 | 
			
		||||
  end
 | 
			
		||||
  else
 | 
			
		||||
    Result := TRUE;
 | 
			
		||||
end;
 | 
			
		||||
							
								
								
									
										29
									
								
								app/ollama.rc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								app/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/ollama_welcome.ps1
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/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 llama3.2"
 | 
			
		||||
write-host ""
 | 
			
		||||
							
								
								
									
										97
									
								
								app/store/store.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								app/store/store.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
			
		||||
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.NewString()
 | 
			
		||||
	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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								app/tray/commontray/types.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								app/tray/tray_nonwindows.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/tray/tray_nonwindows.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
//go:build !windows
 | 
			
		||||
 | 
			
		||||
package tray
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/app/tray/commontray"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func InitPlatformTray(icon, updateIcon []byte) (commontray.OllamaTray, error) {
 | 
			
		||||
	return nil, errors.New("not implemented")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								app/tray/tray_windows.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										181
									
								
								app/tray/wintray/eventloop.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								app/tray/wintray/eventloop.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,181 @@
 | 
			
		||||
//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 unregister window %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),
 | 
			
		||||
			wParam,
 | 
			
		||||
			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))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										72
									
								
								app/tray/wintray/menus.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								app/tray/wintray/menus.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
			
		||||
//go:build windows
 | 
			
		||||
 | 
			
		||||
package wintray
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"unsafe"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/sys/windows"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	_ = iota
 | 
			
		||||
	updateAvailableMenuID
 | 
			
		||||
	updateMenuID
 | 
			
		||||
	separatorMenuID
 | 
			
		||||
	diagLogsMenuID
 | 
			
		||||
	diagSeparatorMenuID
 | 
			
		||||
	quitMenuID
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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(updateAvailableMenuID, 0, updateAvailableMenuTitle, true); err != nil {
 | 
			
		||||
			return fmt.Errorf("unable to create menu entries %w", err)
 | 
			
		||||
		}
 | 
			
		||||
		if err := t.addOrUpdateMenuItem(updateMenuID, 0, updateMenuTitle, 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
									
								
							
							
						
						
									
										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"
 | 
			
		||||
	updateMenuTitle          = "Restart to update"
 | 
			
		||||
	diagLogsMenuTitle        = "View logs"
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										66
									
								
								app/tray/wintray/notifyicon.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										488
									
								
								app/tray/wintray/tray.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										488
									
								
								app/tray/wintray/tray.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,488 @@
 | 
			
		||||
//go:build windows
 | 
			
		||||
 | 
			
		||||
package wintray
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/md5"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"unsafe"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/sys/windows"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/app/tray/commontray"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 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:             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:       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:   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|TPM_RIGHTBUTTON,
 | 
			
		||||
		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, 0o644); 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 | NIF_TIP
 | 
			
		||||
	if toolTipUTF16, err := syscall.UTF16FromString(commontray.ToolTip); err == nil {
 | 
			
		||||
		copy(t.nid.Tip[:], toolTipUTF16)
 | 
			
		||||
	} else {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	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()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										91
									
								
								app/tray/wintray/w32api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								app/tray/wintray/w32api.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
			
		||||
//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_TIP             = 0x00000004
 | 
			
		||||
	NIF_INFO            = 0x00000010
 | 
			
		||||
	NIF_MESSAGE         = 0x00000001
 | 
			
		||||
	SW_HIDE             = 0
 | 
			
		||||
	TPM_BOTTOMALIGN     = 0x0020
 | 
			
		||||
	TPM_LEFTALIGN       = 0x0000
 | 
			
		||||
	TPM_RIGHTBUTTON     = 0x0002
 | 
			
		||||
	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
									
								
							
							
						
						
									
										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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										92
									
								
								auth/auth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								auth/auth.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
package auth
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/rand"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/crypto/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const defaultPrivateKey = "id_ed25519"
 | 
			
		||||
 | 
			
		||||
func keyPath() (string, error) {
 | 
			
		||||
	home, err := os.UserHomeDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return filepath.Join(home, ".ollama", defaultPrivateKey), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetPublicKey() (string, error) {
 | 
			
		||||
	keyPath, err := keyPath()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	publicKey := ssh.MarshalAuthorizedKey(privateKey.PublicKey())
 | 
			
		||||
 | 
			
		||||
	return strings.TrimSpace(string(publicKey)), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
	keyPath, err := keyPath()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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 "", errors.New("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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										178
									
								
								benchmark/server_benchmark_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								benchmark/server_benchmark_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,178 @@
 | 
			
		||||
package benchmark
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Command line flags
 | 
			
		||||
var modelFlag string
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	flag.StringVar(&modelFlag, "m", "", "Name of the model to benchmark")
 | 
			
		||||
	flag.Lookup("m").DefValue = "model"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// modelName returns the model name from flags, failing the test if not set
 | 
			
		||||
func modelName(b *testing.B) string {
 | 
			
		||||
	if modelFlag == "" {
 | 
			
		||||
		b.Fatal("Error: -m flag is required for benchmark tests")
 | 
			
		||||
	}
 | 
			
		||||
	return modelFlag
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TestCase struct {
 | 
			
		||||
	name      string
 | 
			
		||||
	prompt    string
 | 
			
		||||
	maxTokens int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// runGenerateBenchmark contains the common generate and metrics logic
 | 
			
		||||
func runGenerateBenchmark(b *testing.B, ctx context.Context, client *api.Client, req *api.GenerateRequest) {
 | 
			
		||||
	start := time.Now()
 | 
			
		||||
	var ttft time.Duration
 | 
			
		||||
	var metrics api.Metrics
 | 
			
		||||
 | 
			
		||||
	err := client.Generate(ctx, req, func(resp api.GenerateResponse) error {
 | 
			
		||||
		if ttft == 0 && resp.Response != "" {
 | 
			
		||||
			ttft = time.Since(start)
 | 
			
		||||
		}
 | 
			
		||||
		if resp.Done {
 | 
			
		||||
			metrics = resp.Metrics
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// Report custom metrics as part of the benchmark results
 | 
			
		||||
	b.ReportMetric(float64(ttft.Milliseconds()), "ttft_ms")
 | 
			
		||||
	b.ReportMetric(float64(metrics.LoadDuration.Milliseconds()), "load_ms")
 | 
			
		||||
 | 
			
		||||
	// Token throughput metrics
 | 
			
		||||
	promptThroughput := float64(metrics.PromptEvalCount) / metrics.PromptEvalDuration.Seconds()
 | 
			
		||||
	genThroughput := float64(metrics.EvalCount) / metrics.EvalDuration.Seconds()
 | 
			
		||||
	b.ReportMetric(promptThroughput, "prompt_tok/s")
 | 
			
		||||
	b.ReportMetric(genThroughput, "gen_tok/s")
 | 
			
		||||
 | 
			
		||||
	// Token counts
 | 
			
		||||
	b.ReportMetric(float64(metrics.PromptEvalCount), "prompt_tokens")
 | 
			
		||||
	b.ReportMetric(float64(metrics.EvalCount), "gen_tokens")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BenchmarkColdStart runs benchmarks with model loading from cold state
 | 
			
		||||
func BenchmarkColdStart(b *testing.B) {
 | 
			
		||||
	client := setup(b)
 | 
			
		||||
	tests := []TestCase{
 | 
			
		||||
		{"short_prompt", "Write a long story", 100},
 | 
			
		||||
		{"medium_prompt", "Write a detailed economic analysis", 500},
 | 
			
		||||
		{"long_prompt", "Write a comprehensive AI research paper", 1000},
 | 
			
		||||
	}
 | 
			
		||||
	m := modelName(b)
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		b.Run(fmt.Sprintf("%s/cold/%s", m, tt.name), func(b *testing.B) {
 | 
			
		||||
			ctx := b.Context()
 | 
			
		||||
 | 
			
		||||
			// Set number of tokens as our throughput metric
 | 
			
		||||
			b.SetBytes(int64(tt.maxTokens))
 | 
			
		||||
 | 
			
		||||
			for b.Loop() {
 | 
			
		||||
				b.StopTimer()
 | 
			
		||||
				// Ensure model is unloaded before each iteration
 | 
			
		||||
				unload(client, m, b)
 | 
			
		||||
				b.StartTimer()
 | 
			
		||||
 | 
			
		||||
				req := &api.GenerateRequest{
 | 
			
		||||
					Model:   m,
 | 
			
		||||
					Prompt:  tt.prompt,
 | 
			
		||||
					Options: map[string]any{"num_predict": tt.maxTokens, "temperature": 0.1},
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				runGenerateBenchmark(b, ctx, client, req)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BenchmarkWarmStart runs benchmarks with pre-loaded model
 | 
			
		||||
func BenchmarkWarmStart(b *testing.B) {
 | 
			
		||||
	client := setup(b)
 | 
			
		||||
	tests := []TestCase{
 | 
			
		||||
		{"short_prompt", "Write a long story", 100},
 | 
			
		||||
		{"medium_prompt", "Write a detailed economic analysis", 500},
 | 
			
		||||
		{"long_prompt", "Write a comprehensive AI research paper", 1000},
 | 
			
		||||
	}
 | 
			
		||||
	m := modelName(b)
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		b.Run(fmt.Sprintf("%s/warm/%s", m, tt.name), func(b *testing.B) {
 | 
			
		||||
			ctx := b.Context()
 | 
			
		||||
 | 
			
		||||
			// Pre-warm the model
 | 
			
		||||
			warmup(client, m, tt.prompt, b)
 | 
			
		||||
 | 
			
		||||
			// Set number of tokens as our throughput metric
 | 
			
		||||
			b.SetBytes(int64(tt.maxTokens))
 | 
			
		||||
 | 
			
		||||
			for b.Loop() {
 | 
			
		||||
				req := &api.GenerateRequest{
 | 
			
		||||
					Model:   m,
 | 
			
		||||
					Prompt:  tt.prompt,
 | 
			
		||||
					Options: map[string]any{"num_predict": tt.maxTokens, "temperature": 0.1},
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				runGenerateBenchmark(b, ctx, client, req)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// setup verifies server and model availability
 | 
			
		||||
func setup(b *testing.B) *api.Client {
 | 
			
		||||
	client, err := api.ClientFromEnvironment()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		b.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	if _, err := client.Show(b.Context(), &api.ShowRequest{Model: modelName(b)}); err != nil {
 | 
			
		||||
		b.Fatalf("Model unavailable: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// warmup ensures the model is loaded and warmed up
 | 
			
		||||
func warmup(client *api.Client, model string, prompt string, b *testing.B) {
 | 
			
		||||
	for range 3 {
 | 
			
		||||
		err := client.Generate(
 | 
			
		||||
			context.Background(),
 | 
			
		||||
			&api.GenerateRequest{
 | 
			
		||||
				Model:   model,
 | 
			
		||||
				Prompt:  prompt,
 | 
			
		||||
				Options: map[string]any{"num_predict": 50, "temperature": 0.1},
 | 
			
		||||
			},
 | 
			
		||||
			func(api.GenerateResponse) error { return nil },
 | 
			
		||||
		)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			b.Logf("Error during model warm-up: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// unload forces model unloading using KeepAlive: 0 parameter
 | 
			
		||||
func unload(client *api.Client, model string, b *testing.B) {
 | 
			
		||||
	req := &api.GenerateRequest{
 | 
			
		||||
		Model:     model,
 | 
			
		||||
		KeepAlive: &api.Duration{Duration: 0},
 | 
			
		||||
	}
 | 
			
		||||
	if err := client.Generate(context.Background(), req, func(api.GenerateResponse) error { return nil }); err != nil {
 | 
			
		||||
		b.Logf("Unload error: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	time.Sleep(1 * time.Second)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1001
									
								
								cmd/cmd.go
									
									
									
									
									
								
							
							
						
						
									
										1001
									
								
								cmd/cmd.go
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										916
									
								
								cmd/cmd_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										916
									
								
								cmd/cmd_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,916 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/http/httptest"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/go-cmp/cmp"
 | 
			
		||||
	"github.com/spf13/cobra"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/api"
 | 
			
		||||
	"github.com/ollama/ollama/types/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestShowInfo(t *testing.T) {
 | 
			
		||||
	t.Run("bare details", func(t *testing.T) {
 | 
			
		||||
		var b bytes.Buffer
 | 
			
		||||
		if err := showInfo(&api.ShowResponse{
 | 
			
		||||
			Details: api.ModelDetails{
 | 
			
		||||
				Family:            "test",
 | 
			
		||||
				ParameterSize:     "7B",
 | 
			
		||||
				QuantizationLevel: "FP16",
 | 
			
		||||
			},
 | 
			
		||||
		}, false, &b); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		expect := `  Model
 | 
			
		||||
    architecture    test    
 | 
			
		||||
    parameters      7B      
 | 
			
		||||
    quantization    FP16    
 | 
			
		||||
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
		if diff := cmp.Diff(expect, b.String()); diff != "" {
 | 
			
		||||
			t.Errorf("unexpected output (-want +got):\n%s", diff)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("bare model info", func(t *testing.T) {
 | 
			
		||||
		var b bytes.Buffer
 | 
			
		||||
		if err := showInfo(&api.ShowResponse{
 | 
			
		||||
			ModelInfo: map[string]any{
 | 
			
		||||
				"general.architecture":    "test",
 | 
			
		||||
				"general.parameter_count": float64(7_000_000_000),
 | 
			
		||||
				"test.context_length":     float64(0),
 | 
			
		||||
				"test.embedding_length":   float64(0),
 | 
			
		||||
			},
 | 
			
		||||
			Details: api.ModelDetails{
 | 
			
		||||
				Family:            "test",
 | 
			
		||||
				ParameterSize:     "7B",
 | 
			
		||||
				QuantizationLevel: "FP16",
 | 
			
		||||
			},
 | 
			
		||||
		}, false, &b); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		expect := `  Model
 | 
			
		||||
    architecture        test    
 | 
			
		||||
    parameters          7B      
 | 
			
		||||
    context length      0       
 | 
			
		||||
    embedding length    0       
 | 
			
		||||
    quantization        FP16    
 | 
			
		||||
 | 
			
		||||
`
 | 
			
		||||
		if diff := cmp.Diff(expect, b.String()); diff != "" {
 | 
			
		||||
			t.Errorf("unexpected output (-want +got):\n%s", diff)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("verbose model", func(t *testing.T) {
 | 
			
		||||
		var b bytes.Buffer
 | 
			
		||||
		if err := showInfo(&api.ShowResponse{
 | 
			
		||||
			Details: api.ModelDetails{
 | 
			
		||||
				Family:            "test",
 | 
			
		||||
				ParameterSize:     "8B",
 | 
			
		||||
				QuantizationLevel: "FP16",
 | 
			
		||||
			},
 | 
			
		||||
			Parameters: `
 | 
			
		||||
			stop up`,
 | 
			
		||||
			ModelInfo: map[string]any{
 | 
			
		||||
				"general.architecture":    "test",
 | 
			
		||||
				"general.parameter_count": float64(8_000_000_000),
 | 
			
		||||
				"some.true_bool":          true,
 | 
			
		||||
				"some.false_bool":         false,
 | 
			
		||||
				"test.context_length":     float64(1000),
 | 
			
		||||
				"test.embedding_length":   float64(11434),
 | 
			
		||||
			},
 | 
			
		||||
			Tensors: []api.Tensor{
 | 
			
		||||
				{Name: "blk.0.attn_k.weight", Type: "BF16", Shape: []uint64{42, 3117}},
 | 
			
		||||
				{Name: "blk.0.attn_q.weight", Type: "FP16", Shape: []uint64{3117, 42}},
 | 
			
		||||
			},
 | 
			
		||||
		}, true, &b); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		expect := `  Model
 | 
			
		||||
    architecture        test     
 | 
			
		||||
    parameters          8B       
 | 
			
		||||
    context length      1000     
 | 
			
		||||
    embedding length    11434    
 | 
			
		||||
    quantization        FP16     
 | 
			
		||||
 | 
			
		||||
  Parameters
 | 
			
		||||
    stop    up    
 | 
			
		||||
 | 
			
		||||
  Metadata
 | 
			
		||||
    general.architecture       test     
 | 
			
		||||
    general.parameter_count    8e+09    
 | 
			
		||||
    some.false_bool            false    
 | 
			
		||||
    some.true_bool             true     
 | 
			
		||||
    test.context_length        1000     
 | 
			
		||||
    test.embedding_length      11434    
 | 
			
		||||
 | 
			
		||||
  Tensors
 | 
			
		||||
    blk.0.attn_k.weight    BF16    [42 3117]    
 | 
			
		||||
    blk.0.attn_q.weight    FP16    [3117 42]    
 | 
			
		||||
 | 
			
		||||
`
 | 
			
		||||
		if diff := cmp.Diff(expect, b.String()); diff != "" {
 | 
			
		||||
			t.Errorf("unexpected output (-want +got):\n%s", diff)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("parameters", func(t *testing.T) {
 | 
			
		||||
		var b bytes.Buffer
 | 
			
		||||
		if err := showInfo(&api.ShowResponse{
 | 
			
		||||
			Details: api.ModelDetails{
 | 
			
		||||
				Family:            "test",
 | 
			
		||||
				ParameterSize:     "7B",
 | 
			
		||||
				QuantizationLevel: "FP16",
 | 
			
		||||
			},
 | 
			
		||||
			Parameters: `
 | 
			
		||||
			stop never
 | 
			
		||||
			stop gonna
 | 
			
		||||
			stop give
 | 
			
		||||
			stop you
 | 
			
		||||
			stop up
 | 
			
		||||
			temperature 99`,
 | 
			
		||||
		}, false, &b); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		expect := `  Model
 | 
			
		||||
    architecture    test    
 | 
			
		||||
    parameters      7B      
 | 
			
		||||
    quantization    FP16    
 | 
			
		||||
 | 
			
		||||
  Parameters
 | 
			
		||||
    stop           never    
 | 
			
		||||
    stop           gonna    
 | 
			
		||||
    stop           give     
 | 
			
		||||
    stop           you      
 | 
			
		||||
    stop           up       
 | 
			
		||||
    temperature    99       
 | 
			
		||||
 | 
			
		||||
`
 | 
			
		||||
		if diff := cmp.Diff(expect, b.String()); diff != "" {
 | 
			
		||||
			t.Errorf("unexpected output (-want +got):\n%s", diff)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("project info", func(t *testing.T) {
 | 
			
		||||
		var b bytes.Buffer
 | 
			
		||||
		if err := showInfo(&api.ShowResponse{
 | 
			
		||||
			Details: api.ModelDetails{
 | 
			
		||||
				Family:            "test",
 | 
			
		||||
				ParameterSize:     "7B",
 | 
			
		||||
				QuantizationLevel: "FP16",
 | 
			
		||||
			},
 | 
			
		||||
			ProjectorInfo: map[string]any{
 | 
			
		||||
				"general.architecture":         "clip",
 | 
			
		||||
				"general.parameter_count":      float64(133_700_000),
 | 
			
		||||
				"clip.vision.embedding_length": float64(0),
 | 
			
		||||
				"clip.vision.projection_dim":   float64(0),
 | 
			
		||||
			},
 | 
			
		||||
		}, false, &b); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		expect := `  Model
 | 
			
		||||
    architecture    test    
 | 
			
		||||
    parameters      7B      
 | 
			
		||||
    quantization    FP16    
 | 
			
		||||
 | 
			
		||||
  Projector
 | 
			
		||||
    architecture        clip       
 | 
			
		||||
    parameters          133.70M    
 | 
			
		||||
    embedding length    0          
 | 
			
		||||
    dimensions          0          
 | 
			
		||||
 | 
			
		||||
`
 | 
			
		||||
		if diff := cmp.Diff(expect, b.String()); diff != "" {
 | 
			
		||||
			t.Errorf("unexpected output (-want +got):\n%s", diff)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("system", func(t *testing.T) {
 | 
			
		||||
		var b bytes.Buffer
 | 
			
		||||
		if err := showInfo(&api.ShowResponse{
 | 
			
		||||
			Details: api.ModelDetails{
 | 
			
		||||
				Family:            "test",
 | 
			
		||||
				ParameterSize:     "7B",
 | 
			
		||||
				QuantizationLevel: "FP16",
 | 
			
		||||
			},
 | 
			
		||||
			System: `You are a pirate!
 | 
			
		||||
Ahoy, matey!
 | 
			
		||||
Weigh anchor!
 | 
			
		||||
			`,
 | 
			
		||||
		}, false, &b); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		expect := `  Model
 | 
			
		||||
    architecture    test    
 | 
			
		||||
    parameters      7B      
 | 
			
		||||
    quantization    FP16    
 | 
			
		||||
 | 
			
		||||
  System
 | 
			
		||||
    You are a pirate!    
 | 
			
		||||
    Ahoy, matey!         
 | 
			
		||||
 | 
			
		||||
`
 | 
			
		||||
		if diff := cmp.Diff(expect, b.String()); diff != "" {
 | 
			
		||||
			t.Errorf("unexpected output (-want +got):\n%s", diff)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("license", func(t *testing.T) {
 | 
			
		||||
		var b bytes.Buffer
 | 
			
		||||
		license := "MIT License\nCopyright (c) Ollama\n"
 | 
			
		||||
		if err := showInfo(&api.ShowResponse{
 | 
			
		||||
			Details: api.ModelDetails{
 | 
			
		||||
				Family:            "test",
 | 
			
		||||
				ParameterSize:     "7B",
 | 
			
		||||
				QuantizationLevel: "FP16",
 | 
			
		||||
			},
 | 
			
		||||
			License: license,
 | 
			
		||||
		}, false, &b); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		expect := `  Model
 | 
			
		||||
    architecture    test    
 | 
			
		||||
    parameters      7B      
 | 
			
		||||
    quantization    FP16    
 | 
			
		||||
 | 
			
		||||
  License
 | 
			
		||||
    MIT License             
 | 
			
		||||
    Copyright (c) Ollama    
 | 
			
		||||
 | 
			
		||||
`
 | 
			
		||||
		if diff := cmp.Diff(expect, b.String()); diff != "" {
 | 
			
		||||
			t.Errorf("unexpected output (-want +got):\n%s", diff)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("capabilities", func(t *testing.T) {
 | 
			
		||||
		var b bytes.Buffer
 | 
			
		||||
		if err := showInfo(&api.ShowResponse{
 | 
			
		||||
			Details: api.ModelDetails{
 | 
			
		||||
				Family:            "test",
 | 
			
		||||
				ParameterSize:     "7B",
 | 
			
		||||
				QuantizationLevel: "FP16",
 | 
			
		||||
			},
 | 
			
		||||
			Capabilities: []model.Capability{model.CapabilityVision, model.CapabilityTools},
 | 
			
		||||
		}, false, &b); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		expect := "  Model\n" +
 | 
			
		||||
			"    architecture    test    \n" +
 | 
			
		||||
			"    parameters      7B      \n" +
 | 
			
		||||
			"    quantization    FP16    \n" +
 | 
			
		||||
			"\n" +
 | 
			
		||||
			"  Capabilities\n" +
 | 
			
		||||
			"    vision    \n" +
 | 
			
		||||
			"    tools     \n" +
 | 
			
		||||
			"\n"
 | 
			
		||||
 | 
			
		||||
		if diff := cmp.Diff(expect, b.String()); diff != "" {
 | 
			
		||||
			t.Errorf("unexpected output (-want +got):\n%s", diff)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDeleteHandler(t *testing.T) {
 | 
			
		||||
	stopped := false
 | 
			
		||||
	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		if r.URL.Path == "/api/delete" && r.Method == http.MethodDelete {
 | 
			
		||||
			var req api.DeleteRequest
 | 
			
		||||
			if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 | 
			
		||||
				http.Error(w, err.Error(), http.StatusBadRequest)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if req.Name == "test-model" {
 | 
			
		||||
				w.WriteHeader(http.StatusOK)
 | 
			
		||||
			} else {
 | 
			
		||||
				w.WriteHeader(http.StatusNotFound)
 | 
			
		||||
			}
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if r.URL.Path == "/api/generate" && r.Method == http.MethodPost {
 | 
			
		||||
			var req api.GenerateRequest
 | 
			
		||||
			if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 | 
			
		||||
				http.Error(w, err.Error(), http.StatusBadRequest)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if req.Model == "test-model" {
 | 
			
		||||
				w.WriteHeader(http.StatusOK)
 | 
			
		||||
				if err := json.NewEncoder(w).Encode(api.GenerateResponse{
 | 
			
		||||
					Done: true,
 | 
			
		||||
				}); err != nil {
 | 
			
		||||
					http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
				}
 | 
			
		||||
				stopped = true
 | 
			
		||||
				return
 | 
			
		||||
			} else {
 | 
			
		||||
				w.WriteHeader(http.StatusNotFound)
 | 
			
		||||
				if err := json.NewEncoder(w).Encode(api.GenerateResponse{
 | 
			
		||||
					Done: false,
 | 
			
		||||
				}); err != nil {
 | 
			
		||||
					http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	t.Setenv("OLLAMA_HOST", mockServer.URL)
 | 
			
		||||
	t.Cleanup(mockServer.Close)
 | 
			
		||||
 | 
			
		||||
	cmd := &cobra.Command{}
 | 
			
		||||
	cmd.SetContext(t.Context())
 | 
			
		||||
	if err := DeleteHandler(cmd, []string{"test-model"}); err != nil {
 | 
			
		||||
		t.Fatalf("DeleteHandler failed: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	if !stopped {
 | 
			
		||||
		t.Fatal("Model was not stopped before deletion")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := DeleteHandler(cmd, []string{"test-model-not-found"})
 | 
			
		||||
	if err == nil || !strings.Contains(err.Error(), "unable to stop existing running model \"test-model-not-found\"") {
 | 
			
		||||
		t.Fatalf("DeleteHandler failed: expected error about stopping non-existent model, got %v", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetModelfileName(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name          string
 | 
			
		||||
		modelfileName string
 | 
			
		||||
		fileExists    bool
 | 
			
		||||
		expectedName  string
 | 
			
		||||
		expectedErr   error
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:          "no modelfile specified, no modelfile exists",
 | 
			
		||||
			modelfileName: "",
 | 
			
		||||
			fileExists:    false,
 | 
			
		||||
			expectedName:  "",
 | 
			
		||||
			expectedErr:   os.ErrNotExist,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:          "no modelfile specified, modelfile exists",
 | 
			
		||||
			modelfileName: "",
 | 
			
		||||
			fileExists:    true,
 | 
			
		||||
			expectedName:  "Modelfile",
 | 
			
		||||
			expectedErr:   nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:          "modelfile specified, no modelfile exists",
 | 
			
		||||
			modelfileName: "crazyfile",
 | 
			
		||||
			fileExists:    false,
 | 
			
		||||
			expectedName:  "",
 | 
			
		||||
			expectedErr:   os.ErrNotExist,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:          "modelfile specified, modelfile exists",
 | 
			
		||||
			modelfileName: "anotherfile",
 | 
			
		||||
			fileExists:    true,
 | 
			
		||||
			expectedName:  "anotherfile",
 | 
			
		||||
			expectedErr:   nil,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			cmd := &cobra.Command{
 | 
			
		||||
				Use: "fakecmd",
 | 
			
		||||
			}
 | 
			
		||||
			cmd.Flags().String("file", "", "path to modelfile")
 | 
			
		||||
 | 
			
		||||
			var expectedFilename string
 | 
			
		||||
 | 
			
		||||
			if tt.fileExists {
 | 
			
		||||
				var fn string
 | 
			
		||||
				if tt.modelfileName != "" {
 | 
			
		||||
					fn = tt.modelfileName
 | 
			
		||||
				} else {
 | 
			
		||||
					fn = "Modelfile"
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				tempFile, err := os.CreateTemp(t.TempDir(), fn)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("temp modelfile creation failed: %v", err)
 | 
			
		||||
				}
 | 
			
		||||
				defer tempFile.Close()
 | 
			
		||||
 | 
			
		||||
				expectedFilename = tempFile.Name()
 | 
			
		||||
				err = cmd.Flags().Set("file", expectedFilename)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Fatalf("couldn't set file flag: %v", err)
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				expectedFilename = tt.expectedName
 | 
			
		||||
				if tt.modelfileName != "" {
 | 
			
		||||
					err := cmd.Flags().Set("file", tt.modelfileName)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						t.Fatalf("couldn't set file flag: %v", err)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			actualFilename, actualErr := getModelfileName(cmd)
 | 
			
		||||
 | 
			
		||||
			if actualFilename != expectedFilename {
 | 
			
		||||
				t.Errorf("expected filename: '%s' actual filename: '%s'", expectedFilename, actualFilename)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if tt.expectedErr != os.ErrNotExist {
 | 
			
		||||
				if actualErr != tt.expectedErr {
 | 
			
		||||
					t.Errorf("expected err: %v actual err: %v", tt.expectedErr, actualErr)
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				if !os.IsNotExist(actualErr) {
 | 
			
		||||
					t.Errorf("expected err: %v actual err: %v", tt.expectedErr, actualErr)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPushHandler(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name           string
 | 
			
		||||
		modelName      string
 | 
			
		||||
		serverResponse map[string]func(w http.ResponseWriter, r *http.Request)
 | 
			
		||||
		expectedError  string
 | 
			
		||||
		expectedOutput string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:      "successful push",
 | 
			
		||||
			modelName: "test-model",
 | 
			
		||||
			serverResponse: map[string]func(w http.ResponseWriter, r *http.Request){
 | 
			
		||||
				"/api/push": func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
					if r.Method != http.MethodPost {
 | 
			
		||||
						t.Errorf("expected POST request, got %s", r.Method)
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					var req api.PushRequest
 | 
			
		||||
					if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 | 
			
		||||
						http.Error(w, err.Error(), http.StatusBadRequest)
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if req.Name != "test-model" {
 | 
			
		||||
						t.Errorf("expected model name 'test-model', got %s", req.Name)
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					// Simulate progress updates
 | 
			
		||||
					responses := []api.ProgressResponse{
 | 
			
		||||
						{Status: "preparing manifest"},
 | 
			
		||||
						{Digest: "sha256:abc123456789", Total: 100, Completed: 50},
 | 
			
		||||
						{Digest: "sha256:abc123456789", Total: 100, Completed: 100},
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					for _, resp := range responses {
 | 
			
		||||
						if err := json.NewEncoder(w).Encode(resp); err != nil {
 | 
			
		||||
							http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
							return
 | 
			
		||||
						}
 | 
			
		||||
						w.(http.Flusher).Flush()
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			expectedOutput: "\nYou can find your model at:\n\n\thttps://ollama.com/test-model\n",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:      "unauthorized push",
 | 
			
		||||
			modelName: "unauthorized-model",
 | 
			
		||||
			serverResponse: map[string]func(w http.ResponseWriter, r *http.Request){
 | 
			
		||||
				"/api/push": func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
					w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
					w.WriteHeader(http.StatusUnauthorized)
 | 
			
		||||
					err := json.NewEncoder(w).Encode(map[string]string{
 | 
			
		||||
						"error": "access denied",
 | 
			
		||||
					})
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						t.Fatal(err)
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			expectedError: "you are not authorized to push to this namespace, create the model under a namespace you own",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
				if handler, ok := tt.serverResponse[r.URL.Path]; ok {
 | 
			
		||||
					handler(w, r)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				http.Error(w, "not found", http.StatusNotFound)
 | 
			
		||||
			}))
 | 
			
		||||
			defer mockServer.Close()
 | 
			
		||||
 | 
			
		||||
			t.Setenv("OLLAMA_HOST", mockServer.URL)
 | 
			
		||||
 | 
			
		||||
			cmd := &cobra.Command{}
 | 
			
		||||
			cmd.Flags().Bool("insecure", false, "")
 | 
			
		||||
			cmd.SetContext(t.Context())
 | 
			
		||||
 | 
			
		||||
			// Redirect stderr to capture progress output
 | 
			
		||||
			oldStderr := os.Stderr
 | 
			
		||||
			r, w, _ := os.Pipe()
 | 
			
		||||
			os.Stderr = w
 | 
			
		||||
 | 
			
		||||
			// Capture stdout for the "Model pushed" message
 | 
			
		||||
			oldStdout := os.Stdout
 | 
			
		||||
			outR, outW, _ := os.Pipe()
 | 
			
		||||
			os.Stdout = outW
 | 
			
		||||
 | 
			
		||||
			err := PushHandler(cmd, []string{tt.modelName})
 | 
			
		||||
 | 
			
		||||
			// Restore stderr
 | 
			
		||||
			w.Close()
 | 
			
		||||
			os.Stderr = oldStderr
 | 
			
		||||
			// drain the pipe
 | 
			
		||||
			if _, err := io.ReadAll(r); err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Restore stdout and get output
 | 
			
		||||
			outW.Close()
 | 
			
		||||
			os.Stdout = oldStdout
 | 
			
		||||
			stdout, _ := io.ReadAll(outR)
 | 
			
		||||
 | 
			
		||||
			if tt.expectedError == "" {
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Errorf("expected no error, got %v", err)
 | 
			
		||||
				}
 | 
			
		||||
				if tt.expectedOutput != "" {
 | 
			
		||||
					if got := string(stdout); got != tt.expectedOutput {
 | 
			
		||||
						t.Errorf("expected output %q, got %q", tt.expectedOutput, got)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				if err == nil || !strings.Contains(err.Error(), tt.expectedError) {
 | 
			
		||||
					t.Errorf("expected error containing %q, got %v", tt.expectedError, err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestListHandler(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name           string
 | 
			
		||||
		args           []string
 | 
			
		||||
		serverResponse []api.ListModelResponse
 | 
			
		||||
		expectedError  string
 | 
			
		||||
		expectedOutput string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "list all models",
 | 
			
		||||
			args: []string{},
 | 
			
		||||
			serverResponse: []api.ListModelResponse{
 | 
			
		||||
				{Name: "model1", Digest: "sha256:abc123", Size: 1024, ModifiedAt: time.Now().Add(-24 * time.Hour)},
 | 
			
		||||
				{Name: "model2", Digest: "sha256:def456", Size: 2048, ModifiedAt: time.Now().Add(-48 * time.Hour)},
 | 
			
		||||
			},
 | 
			
		||||
			expectedOutput: "NAME      ID              SIZE      MODIFIED     \n" +
 | 
			
		||||
				"model1    sha256:abc12    1.0 KB    24 hours ago    \n" +
 | 
			
		||||
				"model2    sha256:def45    2.0 KB    2 days ago      \n",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "filter models by prefix",
 | 
			
		||||
			args: []string{"model1"},
 | 
			
		||||
			serverResponse: []api.ListModelResponse{
 | 
			
		||||
				{Name: "model1", Digest: "sha256:abc123", Size: 1024, ModifiedAt: time.Now().Add(-24 * time.Hour)},
 | 
			
		||||
				{Name: "model2", Digest: "sha256:def456", Size: 2048, ModifiedAt: time.Now().Add(-24 * time.Hour)},
 | 
			
		||||
			},
 | 
			
		||||
			expectedOutput: "NAME      ID              SIZE      MODIFIED     \n" +
 | 
			
		||||
				"model1    sha256:abc12    1.0 KB    24 hours ago    \n",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name:          "server error",
 | 
			
		||||
			args:          []string{},
 | 
			
		||||
			expectedError: "server error",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
				if r.URL.Path != "/api/tags" || r.Method != http.MethodGet {
 | 
			
		||||
					t.Errorf("unexpected request to %s %s", r.Method, r.URL.Path)
 | 
			
		||||
					http.Error(w, "not found", http.StatusNotFound)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if tt.expectedError != "" {
 | 
			
		||||
					http.Error(w, tt.expectedError, http.StatusInternalServerError)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				response := api.ListResponse{Models: tt.serverResponse}
 | 
			
		||||
				if err := json.NewEncoder(w).Encode(response); err != nil {
 | 
			
		||||
					t.Fatal(err)
 | 
			
		||||
				}
 | 
			
		||||
			}))
 | 
			
		||||
			defer mockServer.Close()
 | 
			
		||||
 | 
			
		||||
			t.Setenv("OLLAMA_HOST", mockServer.URL)
 | 
			
		||||
 | 
			
		||||
			cmd := &cobra.Command{}
 | 
			
		||||
			cmd.SetContext(t.Context())
 | 
			
		||||
 | 
			
		||||
			// Capture stdout
 | 
			
		||||
			oldStdout := os.Stdout
 | 
			
		||||
			r, w, _ := os.Pipe()
 | 
			
		||||
			os.Stdout = w
 | 
			
		||||
 | 
			
		||||
			err := ListHandler(cmd, tt.args)
 | 
			
		||||
 | 
			
		||||
			// Restore stdout and get output
 | 
			
		||||
			w.Close()
 | 
			
		||||
			os.Stdout = oldStdout
 | 
			
		||||
			output, _ := io.ReadAll(r)
 | 
			
		||||
 | 
			
		||||
			if tt.expectedError == "" {
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Errorf("expected no error, got %v", err)
 | 
			
		||||
				}
 | 
			
		||||
				if got := string(output); got != tt.expectedOutput {
 | 
			
		||||
					t.Errorf("expected output:\n%s\ngot:\n%s", tt.expectedOutput, got)
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				if err == nil || !strings.Contains(err.Error(), tt.expectedError) {
 | 
			
		||||
					t.Errorf("expected error containing %q, got %v", tt.expectedError, err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCreateHandler(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name           string
 | 
			
		||||
		modelName      string
 | 
			
		||||
		modelFile      string
 | 
			
		||||
		serverResponse map[string]func(w http.ResponseWriter, r *http.Request)
 | 
			
		||||
		expectedError  string
 | 
			
		||||
		expectedOutput string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name:      "successful create",
 | 
			
		||||
			modelName: "test-model",
 | 
			
		||||
			modelFile: "FROM foo",
 | 
			
		||||
			serverResponse: map[string]func(w http.ResponseWriter, r *http.Request){
 | 
			
		||||
				"/api/create": func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
					if r.Method != http.MethodPost {
 | 
			
		||||
						t.Errorf("expected POST request, got %s", r.Method)
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					req := api.CreateRequest{}
 | 
			
		||||
					if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 | 
			
		||||
						http.Error(w, err.Error(), http.StatusBadRequest)
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if req.Model != "test-model" {
 | 
			
		||||
						t.Errorf("expected model name 'test-model', got %s", req.Name)
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if req.From != "foo" {
 | 
			
		||||
						t.Errorf("expected from 'foo', got %s", req.From)
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					responses := []api.ProgressResponse{
 | 
			
		||||
						{Status: "using existing layer sha256:56bb8bd477a519ffa694fc449c2413c6f0e1d3b1c88fa7e3c9d88d3ae49d4dcb"},
 | 
			
		||||
						{Status: "writing manifest"},
 | 
			
		||||
						{Status: "success"},
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					for _, resp := range responses {
 | 
			
		||||
						if err := json.NewEncoder(w).Encode(resp); err != nil {
 | 
			
		||||
							http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
							return
 | 
			
		||||
						}
 | 
			
		||||
						w.(http.Flusher).Flush()
 | 
			
		||||
					}
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			expectedOutput: "",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
				handler, ok := tt.serverResponse[r.URL.Path]
 | 
			
		||||
				if !ok {
 | 
			
		||||
					t.Errorf("unexpected request to %s", r.URL.Path)
 | 
			
		||||
					http.Error(w, "not found", http.StatusNotFound)
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				handler(w, r)
 | 
			
		||||
			}))
 | 
			
		||||
			t.Setenv("OLLAMA_HOST", mockServer.URL)
 | 
			
		||||
			t.Cleanup(mockServer.Close)
 | 
			
		||||
			tempFile, err := os.CreateTemp(t.TempDir(), "modelfile")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
			defer os.Remove(tempFile.Name())
 | 
			
		||||
 | 
			
		||||
			if _, err := tempFile.WriteString(tt.modelFile); err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
			if err := tempFile.Close(); err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			cmd := &cobra.Command{}
 | 
			
		||||
			cmd.Flags().String("file", "", "")
 | 
			
		||||
			if err := cmd.Flags().Set("file", tempFile.Name()); err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			cmd.Flags().Bool("insecure", false, "")
 | 
			
		||||
			cmd.SetContext(t.Context())
 | 
			
		||||
 | 
			
		||||
			// Redirect stderr to capture progress output
 | 
			
		||||
			oldStderr := os.Stderr
 | 
			
		||||
			r, w, _ := os.Pipe()
 | 
			
		||||
			os.Stderr = w
 | 
			
		||||
 | 
			
		||||
			// Capture stdout for the "Model pushed" message
 | 
			
		||||
			oldStdout := os.Stdout
 | 
			
		||||
			outR, outW, _ := os.Pipe()
 | 
			
		||||
			os.Stdout = outW
 | 
			
		||||
 | 
			
		||||
			err = CreateHandler(cmd, []string{tt.modelName})
 | 
			
		||||
 | 
			
		||||
			// Restore stderr
 | 
			
		||||
			w.Close()
 | 
			
		||||
			os.Stderr = oldStderr
 | 
			
		||||
			// drain the pipe
 | 
			
		||||
			if _, err := io.ReadAll(r); err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Restore stdout and get output
 | 
			
		||||
			outW.Close()
 | 
			
		||||
			os.Stdout = oldStdout
 | 
			
		||||
			stdout, _ := io.ReadAll(outR)
 | 
			
		||||
 | 
			
		||||
			if tt.expectedError == "" {
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					t.Errorf("expected no error, got %v", err)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if tt.expectedOutput != "" {
 | 
			
		||||
					if got := string(stdout); got != tt.expectedOutput {
 | 
			
		||||
						t.Errorf("expected output %q, got %q", tt.expectedOutput, got)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestNewCreateRequest(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name     string
 | 
			
		||||
		from     string
 | 
			
		||||
		opts     runOptions
 | 
			
		||||
		expected *api.CreateRequest
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			"basic test",
 | 
			
		||||
			"newmodel",
 | 
			
		||||
			runOptions{
 | 
			
		||||
				Model:       "mymodel",
 | 
			
		||||
				ParentModel: "",
 | 
			
		||||
				Prompt:      "You are a fun AI agent",
 | 
			
		||||
				Messages:    []api.Message{},
 | 
			
		||||
				WordWrap:    true,
 | 
			
		||||
			},
 | 
			
		||||
			&api.CreateRequest{
 | 
			
		||||
				From:  "mymodel",
 | 
			
		||||
				Model: "newmodel",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"parent model test",
 | 
			
		||||
			"newmodel",
 | 
			
		||||
			runOptions{
 | 
			
		||||
				Model:       "mymodel",
 | 
			
		||||
				ParentModel: "parentmodel",
 | 
			
		||||
				Messages:    []api.Message{},
 | 
			
		||||
				WordWrap:    true,
 | 
			
		||||
			},
 | 
			
		||||
			&api.CreateRequest{
 | 
			
		||||
				From:  "parentmodel",
 | 
			
		||||
				Model: "newmodel",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"parent model as filepath test",
 | 
			
		||||
			"newmodel",
 | 
			
		||||
			runOptions{
 | 
			
		||||
				Model:       "mymodel",
 | 
			
		||||
				ParentModel: "/some/file/like/etc/passwd",
 | 
			
		||||
				Messages:    []api.Message{},
 | 
			
		||||
				WordWrap:    true,
 | 
			
		||||
			},
 | 
			
		||||
			&api.CreateRequest{
 | 
			
		||||
				From:  "mymodel",
 | 
			
		||||
				Model: "newmodel",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"parent model as windows filepath test",
 | 
			
		||||
			"newmodel",
 | 
			
		||||
			runOptions{
 | 
			
		||||
				Model:       "mymodel",
 | 
			
		||||
				ParentModel: "D:\\some\\file\\like\\etc\\passwd",
 | 
			
		||||
				Messages:    []api.Message{},
 | 
			
		||||
				WordWrap:    true,
 | 
			
		||||
			},
 | 
			
		||||
			&api.CreateRequest{
 | 
			
		||||
				From:  "mymodel",
 | 
			
		||||
				Model: "newmodel",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"options test",
 | 
			
		||||
			"newmodel",
 | 
			
		||||
			runOptions{
 | 
			
		||||
				Model:       "mymodel",
 | 
			
		||||
				ParentModel: "parentmodel",
 | 
			
		||||
				Options: map[string]any{
 | 
			
		||||
					"temperature": 1.0,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			&api.CreateRequest{
 | 
			
		||||
				From:  "parentmodel",
 | 
			
		||||
				Model: "newmodel",
 | 
			
		||||
				Parameters: map[string]any{
 | 
			
		||||
					"temperature": 1.0,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"messages test",
 | 
			
		||||
			"newmodel",
 | 
			
		||||
			runOptions{
 | 
			
		||||
				Model:       "mymodel",
 | 
			
		||||
				ParentModel: "parentmodel",
 | 
			
		||||
				System:      "You are a fun AI agent",
 | 
			
		||||
				Messages: []api.Message{
 | 
			
		||||
					{
 | 
			
		||||
						Role:    "user",
 | 
			
		||||
						Content: "hello there!",
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						Role:    "assistant",
 | 
			
		||||
						Content: "hello to you!",
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				WordWrap: true,
 | 
			
		||||
			},
 | 
			
		||||
			&api.CreateRequest{
 | 
			
		||||
				From:   "parentmodel",
 | 
			
		||||
				Model:  "newmodel",
 | 
			
		||||
				System: "You are a fun AI agent",
 | 
			
		||||
				Messages: []api.Message{
 | 
			
		||||
					{
 | 
			
		||||
						Role:    "user",
 | 
			
		||||
						Content: "hello there!",
 | 
			
		||||
					},
 | 
			
		||||
					{
 | 
			
		||||
						Role:    "assistant",
 | 
			
		||||
						Content: "hello to you!",
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			actual := NewCreateRequest(tt.from, tt.opts)
 | 
			
		||||
			if !cmp.Equal(actual, tt.expected) {
 | 
			
		||||
				t.Errorf("expected output %#v, got %#v", tt.expected, actual)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,19 +1,24 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"cmp"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/spf13/cobra"
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
 | 
			
		||||
	"github.com/jmorganca/ollama/api"
 | 
			
		||||
	"github.com/jmorganca/ollama/readline"
 | 
			
		||||
	"github.com/ollama/ollama/api"
 | 
			
		||||
	"github.com/ollama/ollama/envconfig"
 | 
			
		||||
	"github.com/ollama/ollama/readline"
 | 
			
		||||
	"github.com/ollama/ollama/types/errtypes"
 | 
			
		||||
	"github.com/ollama/ollama/types/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type MultilineState int
 | 
			
		||||
@@ -22,48 +27,26 @@ const (
 | 
			
		||||
	MultilineNone MultilineState = iota
 | 
			
		||||
	MultilinePrompt
 | 
			
		||||
	MultilineSystem
 | 
			
		||||
	MultilineTemplate
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func modelIsMultiModal(cmd *cobra.Command, name string) bool {
 | 
			
		||||
	// get model details
 | 
			
		||||
	client, err := api.ClientFromEnvironment()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Println("error: couldn't connect to ollama server")
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	req := api.ShowRequest{Name: name}
 | 
			
		||||
	resp, err := client.Show(cmd.Context(), &req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return slices.Contains(resp.Details.Families, "clip")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
	multiModal := modelIsMultiModal(cmd, opts.Model)
 | 
			
		||||
 | 
			
		||||
	// load the model
 | 
			
		||||
	loadOpts := runOptions{
 | 
			
		||||
		Model:    opts.Model,
 | 
			
		||||
		Prompt:   "",
 | 
			
		||||
		Messages: []api.Message{},
 | 
			
		||||
	}
 | 
			
		||||
	if _, err := chat(cmd, loadOpts); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	usage := func() {
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "Available Commands:")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set          Set session variables")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /show         Show model information")
 | 
			
		||||
		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, "  /set            Set session variables")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /show           Show model information")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /load <model>   Load a session or model")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /save <model>   Save your current session")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /clear          Clear session context")
 | 
			
		||||
		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, "Use \"\"\" to begin a multi-line message.")
 | 
			
		||||
 | 
			
		||||
		if opts.MultiModal {
 | 
			
		||||
			fmt.Fprintf(os.Stderr, "Use %s to include .jpg, .png, or .webp images.\n", filepath.FromSlash("/path/to/file"))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -71,7 +54,6 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "Available Commands:")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set parameter ...     Set a parameter")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set system <string>   Set system message")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set template <string> Set prompt template")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set history           Enable history")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set nohistory         Disable history")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set wordwrap          Enable wordwrap")
 | 
			
		||||
@@ -91,6 +73,7 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "   Alt + f            Move forward (right) one word")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  Ctrl + k            Delete the sentence after the cursor")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  Ctrl + u            Delete the sentence before the cursor")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  Ctrl + w            Delete the word before the cursor")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  Ctrl + l            Clear the screen")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  Ctrl + c            Stop the model from responding")
 | 
			
		||||
@@ -116,12 +99,13 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set parameter num_predict <int>      Max number of tokens to predict")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set parameter top_k <int>            Pick from top k num of tokens")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set parameter top_p <float>          Pick token based on sum of probabilities")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set parameter min_p <float>          Pick token based on top token probability * min_p")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set parameter num_ctx <int>          Set the context size")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set parameter temperature <float>    Set creativity level")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set parameter repeat_penalty <float> How strongly to penalize repetitions")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set parameter repeat_last_n <int>    Set how far back to look for repetitions")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set parameter num_gpu <int>          The number of layers to send to the GPU")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set parameter stop \"<string>\", ...   Set the stop parameters")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "  /set parameter stop <string> <string> ...   Set the stop parameters")
 | 
			
		||||
		fmt.Fprintln(os.Stderr, "")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -135,12 +119,15 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if envconfig.NoHistory() {
 | 
			
		||||
		scanner.HistoryDisable()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmt.Print(readline.StartBracketedPaste)
 | 
			
		||||
	defer fmt.Printf(readline.EndBracketedPaste)
 | 
			
		||||
 | 
			
		||||
	var sb strings.Builder
 | 
			
		||||
	var multiline MultilineState
 | 
			
		||||
	opts.Messages = make([]api.Message, 0)
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		line, err := scanner.Readline()
 | 
			
		||||
@@ -174,12 +161,9 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
			switch multiline {
 | 
			
		||||
			case MultilineSystem:
 | 
			
		||||
				opts.System = sb.String()
 | 
			
		||||
				opts.Messages = append(opts.Messages, api.Message{Role: "system", Content: opts.System})
 | 
			
		||||
				fmt.Println("Set system message.")
 | 
			
		||||
				sb.Reset()
 | 
			
		||||
			case MultilineTemplate:
 | 
			
		||||
				opts.Template = sb.String()
 | 
			
		||||
				fmt.Println("Set prompt template.")
 | 
			
		||||
				sb.Reset()
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			multiline = MultilineNone
 | 
			
		||||
@@ -193,7 +177,6 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
				fmt.Fprintln(&sb)
 | 
			
		||||
				multiline = MultilinePrompt
 | 
			
		||||
				scanner.Prompt.UseAlt = true
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		case scanner.Pasting:
 | 
			
		||||
			fmt.Fprintln(&sb, line)
 | 
			
		||||
@@ -203,6 +186,56 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
			if err := ListHandler(cmd, args[1:]); err != nil {
 | 
			
		||||
				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 := loadOrUnloadModel(cmd, &opts); err != nil {
 | 
			
		||||
				if strings.Contains(err.Error(), "not found") {
 | 
			
		||||
					fmt.Printf("error: %v\n", err)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				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 := NewCreateRequest(args[1], opts)
 | 
			
		||||
			fn := func(resp api.ProgressResponse) error { return nil }
 | 
			
		||||
			err = client.Create(cmd.Context(), req, fn)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				if strings.Contains(err.Error(), errtypes.InvalidModelNameErrMsg) {
 | 
			
		||||
					fmt.Printf("error: The model name '%s' is invalid\n", args[1])
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			fmt.Printf("Created new model '%s'\n", args[1])
 | 
			
		||||
			continue
 | 
			
		||||
		case strings.HasPrefix(line, "/clear"):
 | 
			
		||||
			opts.Messages = []api.Message{}
 | 
			
		||||
			if opts.System != "" {
 | 
			
		||||
				newMessage := api.Message{Role: "system", Content: opts.System}
 | 
			
		||||
				opts.Messages = append(opts.Messages, newMessage)
 | 
			
		||||
			}
 | 
			
		||||
			fmt.Println("Cleared session context")
 | 
			
		||||
			continue
 | 
			
		||||
		case strings.HasPrefix(line, "/set"):
 | 
			
		||||
			args := strings.Fields(line)
 | 
			
		||||
			if len(args) > 1 {
 | 
			
		||||
@@ -218,10 +251,14 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
					opts.WordWrap = false
 | 
			
		||||
					fmt.Println("Set 'nowordwrap' mode.")
 | 
			
		||||
				case "verbose":
 | 
			
		||||
					cmd.Flags().Set("verbose", "true")
 | 
			
		||||
					if err := cmd.Flags().Set("verbose", "true"); err != nil {
 | 
			
		||||
						return err
 | 
			
		||||
					}
 | 
			
		||||
					fmt.Println("Set 'verbose' mode.")
 | 
			
		||||
				case "quiet":
 | 
			
		||||
					cmd.Flags().Set("verbose", "false")
 | 
			
		||||
					if err := cmd.Flags().Set("verbose", "false"); err != nil {
 | 
			
		||||
						return err
 | 
			
		||||
					}
 | 
			
		||||
					fmt.Println("Set 'quiet' mode.")
 | 
			
		||||
				case "format":
 | 
			
		||||
					if len(args) < 3 || args[2] != "json" {
 | 
			
		||||
@@ -246,17 +283,13 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
					}
 | 
			
		||||
					fmt.Printf("Set parameter '%s' to '%s'\n", args[2], strings.Join(params, ", "))
 | 
			
		||||
					opts.Options[args[2]] = fp[args[2]]
 | 
			
		||||
				case "system", "template":
 | 
			
		||||
				case "system":
 | 
			
		||||
					if len(args) < 3 {
 | 
			
		||||
						usageSet()
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if args[1] == "system" {
 | 
			
		||||
						multiline = MultilineSystem
 | 
			
		||||
					} else if args[1] == "template" {
 | 
			
		||||
						multiline = MultilineTemplate
 | 
			
		||||
					}
 | 
			
		||||
					multiline = MultilineSystem
 | 
			
		||||
 | 
			
		||||
					line := strings.Join(args[2:], " ")
 | 
			
		||||
					line, ok := strings.CutPrefix(line, `"""`)
 | 
			
		||||
@@ -276,14 +309,16 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if args[1] == "system" {
 | 
			
		||||
						opts.System = sb.String()
 | 
			
		||||
						fmt.Println("Set system message.")
 | 
			
		||||
					} else if args[1] == "template" {
 | 
			
		||||
						opts.Template = sb.String()
 | 
			
		||||
						fmt.Println("Set prompt template.")
 | 
			
		||||
					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.")
 | 
			
		||||
					sb.Reset()
 | 
			
		||||
					continue
 | 
			
		||||
				default:
 | 
			
		||||
@@ -301,10 +336,9 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
				req := &api.ShowRequest{
 | 
			
		||||
					Name:     opts.Model,
 | 
			
		||||
					System:   opts.System,
 | 
			
		||||
					Template: opts.Template,
 | 
			
		||||
					Options:  opts.Options,
 | 
			
		||||
					Name:    opts.Model,
 | 
			
		||||
					System:  opts.System,
 | 
			
		||||
					Options: opts.Options,
 | 
			
		||||
				}
 | 
			
		||||
				resp, err := client.Show(cmd.Context(), req)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
@@ -314,15 +348,7 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
 | 
			
		||||
				switch args[1] {
 | 
			
		||||
				case "info":
 | 
			
		||||
					fmt.Println("Model details:")
 | 
			
		||||
					if len(resp.Details.Families) > 0 {
 | 
			
		||||
						fmt.Printf("Family              %s\n", strings.Join(resp.Details.Families, ", "))
 | 
			
		||||
					} else if resp.Details.Family != "" {
 | 
			
		||||
						fmt.Printf("Family              %s\n", resp.Details.Family)
 | 
			
		||||
					}
 | 
			
		||||
					fmt.Printf("Parameter Size      %s\n", resp.Details.ParameterSize)
 | 
			
		||||
					fmt.Printf("Quantization Level  %s\n", resp.Details.QuantizationLevel)
 | 
			
		||||
					fmt.Println("")
 | 
			
		||||
					_ = showInfo(resp, false, os.Stderr)
 | 
			
		||||
				case "license":
 | 
			
		||||
					if resp.License == "" {
 | 
			
		||||
						fmt.Println("No license was specified for this model.")
 | 
			
		||||
@@ -355,12 +381,9 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
						fmt.Println("No system message was specified for this model.")
 | 
			
		||||
					}
 | 
			
		||||
				case "template":
 | 
			
		||||
					switch {
 | 
			
		||||
					case opts.Template != "":
 | 
			
		||||
						fmt.Println(opts.Template + "\n")
 | 
			
		||||
					case resp.Template != "":
 | 
			
		||||
					if resp.Template != "" {
 | 
			
		||||
						fmt.Println(resp.Template)
 | 
			
		||||
					default:
 | 
			
		||||
					} else {
 | 
			
		||||
						fmt.Println("No prompt template was specified for this model.")
 | 
			
		||||
					}
 | 
			
		||||
				default:
 | 
			
		||||
@@ -383,13 +406,13 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
			} else {
 | 
			
		||||
				usage()
 | 
			
		||||
			}
 | 
			
		||||
		case line == "/exit", line == "/bye":
 | 
			
		||||
		case strings.HasPrefix(line, "/exit"), strings.HasPrefix(line, "/bye"):
 | 
			
		||||
			return nil
 | 
			
		||||
		case strings.HasPrefix(line, "/"):
 | 
			
		||||
			args := strings.Fields(line)
 | 
			
		||||
			isFile := false
 | 
			
		||||
 | 
			
		||||
			if multiModal {
 | 
			
		||||
			if opts.MultiModal {
 | 
			
		||||
				for _, f := range extractFileNames(line) {
 | 
			
		||||
					if strings.HasPrefix(f, args[0]) {
 | 
			
		||||
						isFile = true
 | 
			
		||||
@@ -411,34 +434,16 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
		if sb.Len() > 0 && multiline == MultilineNone {
 | 
			
		||||
			newMessage := api.Message{Role: "user", Content: sb.String()}
 | 
			
		||||
 | 
			
		||||
			if multiModal {
 | 
			
		||||
			if opts.MultiModal {
 | 
			
		||||
				msg, images, err := extractFileData(sb.String())
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				newMessage.Content = msg
 | 
			
		||||
 | 
			
		||||
				// reset the context if we find another image
 | 
			
		||||
				if len(images) > 0 {
 | 
			
		||||
					newMessage.Images = append(newMessage.Images, images...)
 | 
			
		||||
					// reset the context for the new image
 | 
			
		||||
					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.")
 | 
			
		||||
					fmt.Println()
 | 
			
		||||
					sb.Reset()
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				newMessage.Images = images
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if opts.System != "" {
 | 
			
		||||
				opts.Messages = append(opts.Messages, api.Message{Role: "system", Content: opts.System})
 | 
			
		||||
			}
 | 
			
		||||
			opts.Messages = append(opts.Messages, newMessage)
 | 
			
		||||
 | 
			
		||||
			assistant, err := chat(cmd, opts)
 | 
			
		||||
@@ -454,36 +459,59 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func normalizeFilePath(fp string) string {
 | 
			
		||||
	// Define a map of escaped characters and their replacements
 | 
			
		||||
	replacements := map[string]string{
 | 
			
		||||
		"\\ ":  " ",  // Escaped space
 | 
			
		||||
		"\\(":  "(",  // Escaped left parenthesis
 | 
			
		||||
		"\\)":  ")",  // Escaped right parenthesis
 | 
			
		||||
		"\\[":  "[",  // Escaped left square bracket
 | 
			
		||||
		"\\]":  "]",  // Escaped right square bracket
 | 
			
		||||
		"\\{":  "{",  // Escaped left curly brace
 | 
			
		||||
		"\\}":  "}",  // Escaped right curly brace
 | 
			
		||||
		"\\$":  "$",  // Escaped dollar sign
 | 
			
		||||
		"\\&":  "&",  // Escaped ampersand
 | 
			
		||||
		"\\;":  ";",  // Escaped semicolon
 | 
			
		||||
		"\\'":  "'",  // Escaped single quote
 | 
			
		||||
		"\\\\": "\\", // Escaped backslash
 | 
			
		||||
		"\\*":  "*",  // Escaped asterisk
 | 
			
		||||
		"\\?":  "?",  // Escaped question mark
 | 
			
		||||
func NewCreateRequest(name string, opts runOptions) *api.CreateRequest {
 | 
			
		||||
	parentModel := opts.ParentModel
 | 
			
		||||
 | 
			
		||||
	modelName := model.ParseName(parentModel)
 | 
			
		||||
	if !modelName.IsValid() {
 | 
			
		||||
		parentModel = ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for escaped, actual := range replacements {
 | 
			
		||||
		fp = strings.ReplaceAll(fp, escaped, actual)
 | 
			
		||||
	req := &api.CreateRequest{
 | 
			
		||||
		Model: name,
 | 
			
		||||
		From:  cmp.Or(parentModel, opts.Model),
 | 
			
		||||
	}
 | 
			
		||||
	return fp
 | 
			
		||||
 | 
			
		||||
	if opts.System != "" {
 | 
			
		||||
		req.System = opts.System
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(opts.Options) > 0 {
 | 
			
		||||
		req.Parameters = opts.Options
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(opts.Messages) > 0 {
 | 
			
		||||
		req.Messages = opts.Messages
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return req
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func normalizeFilePath(fp string) string {
 | 
			
		||||
	return strings.NewReplacer(
 | 
			
		||||
		"\\ ", " ", // Escaped space
 | 
			
		||||
		"\\(", "(", // Escaped left parenthesis
 | 
			
		||||
		"\\)", ")", // Escaped right parenthesis
 | 
			
		||||
		"\\[", "[", // Escaped left square bracket
 | 
			
		||||
		"\\]", "]", // Escaped right square bracket
 | 
			
		||||
		"\\{", "{", // Escaped left curly brace
 | 
			
		||||
		"\\}", "}", // Escaped right curly brace
 | 
			
		||||
		"\\$", "$", // Escaped dollar sign
 | 
			
		||||
		"\\&", "&", // Escaped ampersand
 | 
			
		||||
		"\\;", ";", // Escaped semicolon
 | 
			
		||||
		"\\'", "'", // Escaped single quote
 | 
			
		||||
		"\\\\", "\\", // Escaped backslash
 | 
			
		||||
		"\\*", "*", // Escaped asterisk
 | 
			
		||||
		"\\?", "?", // Escaped question mark
 | 
			
		||||
		"\\~", "~", // Escaped tilde
 | 
			
		||||
	).Replace(fp)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func extractFileNames(input string) []string {
 | 
			
		||||
	// Regex to match file paths starting with optional drive letter, / ./ \ or .\ and include escaped or unescaped spaces (\ or %20)
 | 
			
		||||
	// and followed by more characters and a file extension
 | 
			
		||||
	// This will capture non filename strings, but we'll check for file existence to remove mismatches
 | 
			
		||||
	regexPattern := `(?:[a-zA-Z]:)?(?:\./|/|\\)[\S\\ ]+?\.(?i:jpg|jpeg|png|svg)\b`
 | 
			
		||||
	regexPattern := `(?:[a-zA-Z]:)?(?:\./|/|\\)[\S\\ ]+?\.(?i:jpg|jpeg|png|webp)\b`
 | 
			
		||||
	re := regexp.MustCompile(regexPattern)
 | 
			
		||||
 | 
			
		||||
	return re.FindAllString(input, -1)
 | 
			
		||||
@@ -496,18 +524,19 @@ func extractFileData(input string) (string, []api.ImageData, error) {
 | 
			
		||||
	for _, fp := range filePaths {
 | 
			
		||||
		nfp := normalizeFilePath(fp)
 | 
			
		||||
		data, err := getImageData(nfp)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if os.IsNotExist(err) {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			fmt.Printf("Couldn't process image: %q\n", err)
 | 
			
		||||
		if errors.Is(err, os.ErrNotExist) {
 | 
			
		||||
			continue
 | 
			
		||||
		} else if err != nil {
 | 
			
		||||
			fmt.Fprintf(os.Stderr, "Couldn't process image: %q\n", err)
 | 
			
		||||
			return "", imgs, err
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Printf("Added image '%s'\n", nfp)
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "Added image '%s'\n", nfp)
 | 
			
		||||
		input = strings.ReplaceAll(input, "'"+nfp+"'", "")
 | 
			
		||||
		input = strings.ReplaceAll(input, "'"+fp+"'", "")
 | 
			
		||||
		input = strings.ReplaceAll(input, fp, "")
 | 
			
		||||
		imgs = append(imgs, data)
 | 
			
		||||
	}
 | 
			
		||||
	return input, imgs, nil
 | 
			
		||||
	return strings.TrimSpace(input), imgs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getImageData(filePath string) ([]byte, error) {
 | 
			
		||||
@@ -524,7 +553,7 @@ func getImageData(filePath string) ([]byte, error) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	contentType := http.DetectContentType(buf)
 | 
			
		||||
	allowedTypes := []string{"image/jpeg", "image/jpg", "image/svg+xml", "image/png"}
 | 
			
		||||
	allowedTypes := []string{"image/jpeg", "image/jpg", "image/png", "image/webp"}
 | 
			
		||||
	if !slices.Contains(allowedTypes, contentType) {
 | 
			
		||||
		return nil, fmt.Errorf("invalid image type: %s", contentType)
 | 
			
		||||
	}
 | 
			
		||||
@@ -537,7 +566,7 @@ func getImageData(filePath string) ([]byte, error) {
 | 
			
		||||
	// Check if the file size exceeds 100MB
 | 
			
		||||
	var maxSize int64 = 100 * 1024 * 1024 // 100MB in bytes
 | 
			
		||||
	if info.Size() > maxSize {
 | 
			
		||||
		return nil, fmt.Errorf("file size exceeds maximum limit (100MB)")
 | 
			
		||||
		return nil, errors.New("file size exceeds maximum limit (100MB)")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buf = make([]byte, info.Size())
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
@@ -9,43 +11,76 @@ import (
 | 
			
		||||
func TestExtractFilenames(t *testing.T) {
 | 
			
		||||
	// Unix style paths
 | 
			
		||||
	input := ` some preamble 
 | 
			
		||||
 ./relative\ path/one.png inbetween1 ./not a valid two.jpg inbetween2
 | 
			
		||||
/unescaped space /three.jpeg inbetween3 /valid\ path/dir/four.png "./quoted with spaces/five.svg`
 | 
			
		||||
 ./relative\ path/one.png inbetween1 ./not a valid two.jpg inbetween2 ./1.svg
 | 
			
		||||
/unescaped space /three.jpeg inbetween3 /valid\ path/dir/four.png "./quoted with spaces/five.JPG
 | 
			
		||||
/unescaped space /six.webp inbetween6 /valid\ path/dir/seven.WEBP`
 | 
			
		||||
	res := extractFileNames(input)
 | 
			
		||||
	assert.Len(t, res, 5)
 | 
			
		||||
	assert.Len(t, res, 7)
 | 
			
		||||
	assert.Contains(t, res[0], "one.png")
 | 
			
		||||
	assert.Contains(t, res[1], "two.jpg")
 | 
			
		||||
	assert.Contains(t, res[2], "three.jpeg")
 | 
			
		||||
	assert.Contains(t, res[3], "four.png")
 | 
			
		||||
	assert.Contains(t, res[4], "five.svg")
 | 
			
		||||
	assert.Contains(t, res[4], "five.JPG")
 | 
			
		||||
	assert.Contains(t, res[5], "six.webp")
 | 
			
		||||
	assert.Contains(t, res[6], "seven.WEBP")
 | 
			
		||||
	assert.NotContains(t, res[4], '"')
 | 
			
		||||
	assert.NotContains(t, res, "inbtween")
 | 
			
		||||
	assert.NotContains(t, res, "inbetween1")
 | 
			
		||||
	assert.NotContains(t, res, "./1.svg")
 | 
			
		||||
 | 
			
		||||
	// Windows style paths
 | 
			
		||||
	input = ` some preamble
 | 
			
		||||
 c:/users/jdoe/one.png inbetween1 c:/program files/someplace/two.jpg inbetween2 
 | 
			
		||||
 /absolute/nospace/three.jpeg inbetween3 /absolute/with space/four.png inbetween4
 | 
			
		||||
./relative\ path/five.svg inbetween5 "./relative with/spaces/six.png inbetween6
 | 
			
		||||
d:\path with\spaces\seven.svg inbetween7 c:\users\jdoe\eight.png inbetween8 
 | 
			
		||||
 d:\program files\someplace\nine.png inbetween9 "E:\program files\someplace\ten.svg some ending
 | 
			
		||||
./relative\ path/five.JPG inbetween5 "./relative with/spaces/six.png inbetween6
 | 
			
		||||
d:\path with\spaces\seven.JPEG inbetween7 c:\users\jdoe\eight.png inbetween8 
 | 
			
		||||
 d:\program files\someplace\nine.png inbetween9 "E:\program files\someplace\ten.PNG
 | 
			
		||||
c:/users/jdoe/eleven.webp inbetween11 c:/program files/someplace/twelve.WebP inbetween12
 | 
			
		||||
d:\path with\spaces\thirteen.WEBP some ending
 | 
			
		||||
`
 | 
			
		||||
	res = extractFileNames(input)
 | 
			
		||||
	assert.Len(t, res, 10)
 | 
			
		||||
	assert.NotContains(t, res, "inbtween")
 | 
			
		||||
	assert.Len(t, res, 13)
 | 
			
		||||
	assert.NotContains(t, res, "inbetween2")
 | 
			
		||||
	assert.Contains(t, res[0], "one.png")
 | 
			
		||||
	assert.Contains(t, res[0], "c:")
 | 
			
		||||
	assert.Contains(t, res[1], "two.jpg")
 | 
			
		||||
	assert.Contains(t, res[1], "c:")
 | 
			
		||||
	assert.Contains(t, res[2], "three.jpeg")
 | 
			
		||||
	assert.Contains(t, res[3], "four.png")
 | 
			
		||||
	assert.Contains(t, res[4], "five.svg")
 | 
			
		||||
	assert.Contains(t, res[4], "five.JPG")
 | 
			
		||||
	assert.Contains(t, res[5], "six.png")
 | 
			
		||||
	assert.Contains(t, res[6], "seven.svg")
 | 
			
		||||
	assert.Contains(t, res[6], "seven.JPEG")
 | 
			
		||||
	assert.Contains(t, res[6], "d:")
 | 
			
		||||
	assert.Contains(t, res[7], "eight.png")
 | 
			
		||||
	assert.Contains(t, res[7], "c:")
 | 
			
		||||
	assert.Contains(t, res[8], "nine.png")
 | 
			
		||||
	assert.Contains(t, res[8], "d:")
 | 
			
		||||
	assert.Contains(t, res[9], "ten.svg")
 | 
			
		||||
	assert.Contains(t, res[9], "ten.PNG")
 | 
			
		||||
	assert.Contains(t, res[9], "E:")
 | 
			
		||||
	assert.Contains(t, res[10], "eleven.webp")
 | 
			
		||||
	assert.Contains(t, res[10], "c:")
 | 
			
		||||
	assert.Contains(t, res[11], "twelve.WebP")
 | 
			
		||||
	assert.Contains(t, res[11], "c:")
 | 
			
		||||
	assert.Contains(t, res[12], "thirteen.WEBP")
 | 
			
		||||
	assert.Contains(t, res[12], "d:")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Ensure that file paths wrapped in single quotes are removed with the quotes.
 | 
			
		||||
func TestExtractFileDataRemovesQuotedFilepath(t *testing.T) {
 | 
			
		||||
	dir := t.TempDir()
 | 
			
		||||
	fp := filepath.Join(dir, "img.jpg")
 | 
			
		||||
	data := make([]byte, 600)
 | 
			
		||||
	copy(data, []byte{
 | 
			
		||||
		0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 'J', 'F', 'I', 'F',
 | 
			
		||||
		0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 | 
			
		||||
		0xff, 0xd9,
 | 
			
		||||
	})
 | 
			
		||||
	if err := os.WriteFile(fp, data, 0o600); err != nil {
 | 
			
		||||
		t.Fatalf("failed to write test image: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	input := "before '" + fp + "' after"
 | 
			
		||||
	cleaned, imgs, err := extractFileData(input)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Len(t, imgs, 1)
 | 
			
		||||
	assert.Equal(t, cleaned, "before  after")
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								cmd/runner/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								cmd/runner/main.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/runner"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	if err := runner.Execute(os.Args[1:]); err != nil {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "error: %s\n", err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								cmd/start.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								cmd/start.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
//go:build darwin || windows
 | 
			
		||||
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func waitForServer(ctx context.Context, client *api.Client) error {
 | 
			
		||||
	// wait for the server to start
 | 
			
		||||
	timeout := time.After(5 * time.Second)
 | 
			
		||||
	tick := time.Tick(500 * time.Millisecond)
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-timeout:
 | 
			
		||||
			return errors.New("timed out waiting for server to start")
 | 
			
		||||
		case <-tick:
 | 
			
		||||
			if err := client.Heartbeat(ctx); err == nil {
 | 
			
		||||
				return nil // server has started
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								cmd/start_darwin.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								cmd/start_darwin.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"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 errors.New("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
									
								
							
							
						
						
									
										14
									
								
								cmd/start_default.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
//go:build !windows && !darwin
 | 
			
		||||
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func startApp(ctx context.Context, client *api.Client) error {
 | 
			
		||||
	return errors.New("could not connect to ollama server, run 'ollama serve' to start it")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										58
									
								
								cmd/start_windows.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										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 errors.New("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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										251
									
								
								convert/convert.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								convert/convert.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,251 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"cmp"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"os"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/fs/ggml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ModelParameters struct {
 | 
			
		||||
	Architectures []string `json:"architectures"`
 | 
			
		||||
	VocabSize     uint32   `json:"vocab_size"`
 | 
			
		||||
 | 
			
		||||
	TextModel struct {
 | 
			
		||||
		VocabSize uint32 `json:"vocab_size"`
 | 
			
		||||
	} `json:"text_config"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AdapterParameters struct {
 | 
			
		||||
	Alpha          uint32 `json:"lora_alpha"`
 | 
			
		||||
	LoraLayers     uint32 `json:"lora_layers"`
 | 
			
		||||
	LoraParameters struct {
 | 
			
		||||
		Rank  uint32  `json:"rank"`
 | 
			
		||||
		Alpha float32 `json:"alpha"`
 | 
			
		||||
		Scale float32 `json:"scale"`
 | 
			
		||||
	} `json:"lora_parameters"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ModelParameters) KV(t *Tokenizer) ggml.KV {
 | 
			
		||||
	kv := ggml.KV{
 | 
			
		||||
		"general.file_type":            uint32(1),
 | 
			
		||||
		"general.quantization_version": uint32(2),
 | 
			
		||||
		"tokenizer.ggml.pre":           t.Pre,
 | 
			
		||||
		"tokenizer.ggml.model":         t.Vocabulary.Model,
 | 
			
		||||
		"tokenizer.ggml.tokens":        t.Vocabulary.Tokens,
 | 
			
		||||
		"tokenizer.ggml.scores":        t.Vocabulary.Scores,
 | 
			
		||||
		"tokenizer.ggml.token_type":    t.Vocabulary.Types,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(t.Merges) > 0 {
 | 
			
		||||
		kv["tokenizer.ggml.merges"] = t.Merges
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if t.Template != "" {
 | 
			
		||||
		kv["tokenizer.chat_template"] = t.Template
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, sv := range t.SpecialVocabulary {
 | 
			
		||||
		kv[fmt.Sprintf("tokenizer.ggml.%s_token_id", sv.Key())] = uint32(sv.ID)
 | 
			
		||||
		kv[fmt.Sprintf("tokenizer.ggml.add_%s_token", sv.Key())] = sv.AddToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p AdapterParameters) KV() ggml.KV {
 | 
			
		||||
	var alpha float32
 | 
			
		||||
	if p.LoraParameters.Alpha == 0 {
 | 
			
		||||
		alpha = float32(p.Alpha)
 | 
			
		||||
	} else {
 | 
			
		||||
		alpha = p.LoraParameters.Alpha
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	kv := ggml.KV{
 | 
			
		||||
		"adapter.lora.alpha": alpha,
 | 
			
		||||
		"adapter.type":       "lora",
 | 
			
		||||
		"general.file_type":  uint32(1),
 | 
			
		||||
		"general.type":       "adapter",
 | 
			
		||||
		"general.version":    "v0.2",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ModelParameters) specialTokenTypes() []string {
 | 
			
		||||
	return []string{
 | 
			
		||||
		"bos", "eos", "unk", "sep", "pad", "cls", "mask",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ModelConverter interface {
 | 
			
		||||
	// KV maps parameters to LLM key-values
 | 
			
		||||
	KV(*Tokenizer) ggml.KV
 | 
			
		||||
	// Tensors maps input tensors to LLM tensors. Model specific modifications can be done here.
 | 
			
		||||
	Tensors([]Tensor) []*ggml.Tensor
 | 
			
		||||
	// Replacements returns a list of string pairs to replace in tensor names.
 | 
			
		||||
	// See [strings.Replacer](https://pkg.go.dev/strings#Replacer) for details
 | 
			
		||||
	Replacements() []string
 | 
			
		||||
 | 
			
		||||
	// specialTokenTypes returns any special token types the model uses
 | 
			
		||||
	specialTokenTypes() []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type moreParser interface {
 | 
			
		||||
	parseMore(fs.FS) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AdapterConverter interface {
 | 
			
		||||
	// KV maps parameters to LLM key-values
 | 
			
		||||
	KV(ggml.KV) ggml.KV
 | 
			
		||||
	// Tensors maps input tensors to LLM tensors. Adapter specific modifications can be done here.
 | 
			
		||||
	Tensors([]Tensor) []*ggml.Tensor
 | 
			
		||||
	// Replacements returns a list of string pairs to replace in tensor names.
 | 
			
		||||
	// See [strings.Replacer](https://pkg.go.dev/strings#Replacer) for details
 | 
			
		||||
	Replacements() []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ConvertAdapter(fsys fs.FS, f *os.File, baseKV ggml.KV) error {
 | 
			
		||||
	bts, err := fs.ReadFile(fsys, "adapter_config.json")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var p AdapterParameters
 | 
			
		||||
	if err := json.Unmarshal(bts, &p); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	arch, ok := baseKV["general.architecture"]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return errors.New("architecture not set for the base model")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var conv AdapterConverter
 | 
			
		||||
	switch arch {
 | 
			
		||||
	case "llama":
 | 
			
		||||
		conv = &llamaAdapter{}
 | 
			
		||||
	case "gemma2":
 | 
			
		||||
		conv = &gemma2Adapter{}
 | 
			
		||||
	default:
 | 
			
		||||
		return errors.New("unsupported architecture")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ts, err := parseTensors(fsys, strings.NewReplacer(conv.Replacements()...))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := json.Unmarshal(bts, conv); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return writeFile(f, conv.KV(baseKV), conv.Tensors(ts))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Convert writes an Ollama compatible model to the provided io.WriteSeeker based on configurations
 | 
			
		||||
// and files it finds in the input path.
 | 
			
		||||
// Supported input model formats include safetensors.
 | 
			
		||||
// Supported input tokenizers files include tokenizer.json (preferred) and tokenizer.model.
 | 
			
		||||
func ConvertModel(fsys fs.FS, f *os.File) error {
 | 
			
		||||
	bts, err := fs.ReadFile(fsys, "config.json")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var p ModelParameters
 | 
			
		||||
	if err := json.Unmarshal(bts, &p); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(p.Architectures) < 1 {
 | 
			
		||||
		return errors.New("unknown architecture")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var conv ModelConverter
 | 
			
		||||
	switch p.Architectures[0] {
 | 
			
		||||
	case "LlamaForCausalLM":
 | 
			
		||||
		conv = &llamaModel{}
 | 
			
		||||
	case "MllamaForConditionalGeneration":
 | 
			
		||||
		conv = &mllamaModel{}
 | 
			
		||||
	case "Llama4ForConditionalGeneration":
 | 
			
		||||
		conv = &llama4Model{}
 | 
			
		||||
	case "Mistral3ForConditionalGeneration":
 | 
			
		||||
		conv = &mistral3Model{}
 | 
			
		||||
	case "MixtralForCausalLM":
 | 
			
		||||
		conv = &mixtralModel{}
 | 
			
		||||
	case "GemmaForCausalLM":
 | 
			
		||||
		conv = &gemmaModel{}
 | 
			
		||||
	case "Gemma2ForCausalLM":
 | 
			
		||||
		conv = &gemma2Model{}
 | 
			
		||||
	case "Gemma3ForCausalLM", "Gemma3ForConditionalGeneration":
 | 
			
		||||
		conv = &gemma3Model{Architecture: p.Architectures[0]}
 | 
			
		||||
	case "Phi3ForCausalLM":
 | 
			
		||||
		conv = &phi3Model{}
 | 
			
		||||
	case "Qwen2ForCausalLM":
 | 
			
		||||
		conv = &qwen2Model{}
 | 
			
		||||
	case "BertModel":
 | 
			
		||||
		conv = &bertModel{}
 | 
			
		||||
	case "CohereForCausalLM":
 | 
			
		||||
		conv = &commandrModel{}
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("unsupported architecture %q", p.Architectures[0])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := json.Unmarshal(bts, conv); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if t, ok := conv.(moreParser); ok {
 | 
			
		||||
		if err := t.parseMore(fsys); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t, err := parseTokenizer(fsys, conv.specialTokenTypes())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	vocabSize := int(cmp.Or(p.VocabSize, p.TextModel.VocabSize))
 | 
			
		||||
 | 
			
		||||
	switch {
 | 
			
		||||
	case vocabSize == 0:
 | 
			
		||||
		slog.Debug("vocabulary size was not explicitly set by the model", "default size", len(t.Vocabulary.Tokens))
 | 
			
		||||
	case vocabSize > len(t.Vocabulary.Tokens):
 | 
			
		||||
		slog.Debug("vocabulary is smaller than expected, padding with dummy tokens", "expect", vocabSize, "actual", len(t.Vocabulary.Tokens))
 | 
			
		||||
		for i := range vocabSize - len(t.Vocabulary.Tokens) {
 | 
			
		||||
			t.Vocabulary.Tokens = append(t.Vocabulary.Tokens, fmt.Sprintf("[PAD%d]", i))
 | 
			
		||||
			t.Vocabulary.Scores = append(t.Vocabulary.Scores, -1)
 | 
			
		||||
			t.Vocabulary.Types = append(t.Vocabulary.Types, tokenTypeUserDefined)
 | 
			
		||||
		}
 | 
			
		||||
	case vocabSize < len(t.Vocabulary.Tokens):
 | 
			
		||||
		slog.Debug("vocabulary is larger than expected", "want", vocabSize, "got", len(t.Vocabulary.Tokens))
 | 
			
		||||
		p.VocabSize = uint32(len(t.Vocabulary.Tokens))
 | 
			
		||||
		p.TextModel.VocabSize = uint32(len(t.Vocabulary.Tokens))
 | 
			
		||||
	default:
 | 
			
		||||
		slog.Debug("vocabulary", "size", len(t.Vocabulary.Tokens))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ts, err := parseTensors(fsys, strings.NewReplacer(conv.Replacements()...))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return writeFile(f, conv.KV(t), conv.Tensors(ts))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func writeFile(f *os.File, kv ggml.KV, ts []*ggml.Tensor) error {
 | 
			
		||||
	for i := range ts {
 | 
			
		||||
		ts[i].Shape = slices.Clone(ts[i].Shape)
 | 
			
		||||
		slices.Reverse(ts[i].Shape)
 | 
			
		||||
	}
 | 
			
		||||
	return ggml.WriteGGUF(f, kv, ts)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										174
									
								
								convert/convert_bert.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								convert/convert_bert.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,174 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"cmp"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/fs/ggml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type bertModel struct {
 | 
			
		||||
	ModelParameters
 | 
			
		||||
	NLayers               uint32  `json:"n_layers"`
 | 
			
		||||
	NumHiddenLayers       uint32  `json:"num_hidden_layers"`
 | 
			
		||||
	NLayer                uint32  `json:"n_layer"`
 | 
			
		||||
	MaxPositionEmbeddings uint32  `json:"max_position_embeddings"`
 | 
			
		||||
	NCtx                  uint32  `json:"n_ctx"`
 | 
			
		||||
	HiddenSize            uint32  `json:"hidden_size"`
 | 
			
		||||
	NEmbd                 uint32  `json:"n_embd"`
 | 
			
		||||
	IntermediateSize      uint32  `json:"intermediate_size"`
 | 
			
		||||
	NInner                uint32  `json:"n_inner"`
 | 
			
		||||
	NumAttentionHeads     uint32  `json:"num_attention_heads"`
 | 
			
		||||
	NHead                 uint32  `json:"n_head"`
 | 
			
		||||
	NumKeyValueHeads      uint32  `json:"num_key_value_heads"`
 | 
			
		||||
	LayerNormEPS          float32 `json:"layer_norm_eps"`
 | 
			
		||||
	LayerNormEpsilon      float32 `json:"layer_norm_epsilon"`
 | 
			
		||||
	NormEpsilon           float32 `json:"norm_epsilon"`
 | 
			
		||||
 | 
			
		||||
	PoolingType uint32
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	_ ModelConverter = (*bertModel)(nil)
 | 
			
		||||
	_ moreParser     = (*bertModel)(nil)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (p *bertModel) parseMore(fsys fs.FS) error {
 | 
			
		||||
	bts, err := fs.ReadFile(fsys, "modules.json")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var modules []struct {
 | 
			
		||||
		Type string `json:"type"`
 | 
			
		||||
		Path string `json:"path"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := json.Unmarshal(bts, &modules); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var pooling string
 | 
			
		||||
	for _, m := range modules {
 | 
			
		||||
		if m.Type == "sentence_transformers.models.Pooling" {
 | 
			
		||||
			pooling = m.Path
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if pooling != "" {
 | 
			
		||||
		bts, err := fs.ReadFile(fsys, filepath.Join(pooling, "config.json"))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var pc struct {
 | 
			
		||||
			PoolingModeCLSToken   bool `json:"pooling_mode_cls_token"`
 | 
			
		||||
			PoolingModeMeanTokens bool `json:"pooling_mode_mean_tokens"`
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := json.Unmarshal(bts, &pc); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if pc.PoolingModeMeanTokens {
 | 
			
		||||
			p.PoolingType = 1
 | 
			
		||||
		} else if pc.PoolingModeCLSToken {
 | 
			
		||||
			p.PoolingType = 2
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *bertModel) KV(t *Tokenizer) ggml.KV {
 | 
			
		||||
	kv := p.ModelParameters.KV(t)
 | 
			
		||||
	kv["general.architecture"] = "bert"
 | 
			
		||||
	kv["bert.attention.causal"] = false
 | 
			
		||||
	kv["bert.pooling_type"] = p.PoolingType
 | 
			
		||||
 | 
			
		||||
	kv["bert.block_count"] = cmp.Or(p.NLayers, p.NumHiddenLayers, p.NLayer)
 | 
			
		||||
 | 
			
		||||
	if contextLength := cmp.Or(p.MaxPositionEmbeddings, p.NCtx); contextLength > 0 {
 | 
			
		||||
		kv["bert.context_length"] = contextLength
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if embeddingLength := cmp.Or(p.HiddenSize, p.NEmbd); embeddingLength > 0 {
 | 
			
		||||
		kv["bert.embedding_length"] = cmp.Or(p.HiddenSize, p.NEmbd)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if feedForwardLength := cmp.Or(p.IntermediateSize, p.NInner); feedForwardLength > 0 {
 | 
			
		||||
		kv["bert.feed_forward_length"] = cmp.Or(p.IntermediateSize, p.NInner)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if headCount := cmp.Or(p.NumAttentionHeads, p.NHead); headCount > 0 {
 | 
			
		||||
		kv["bert.attention.head_count"] = cmp.Or(p.NumAttentionHeads, p.NHead)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if layerNormEpsilon := cmp.Or(p.LayerNormEPS, p.LayerNormEpsilon, p.NormEpsilon); layerNormEpsilon > 0 {
 | 
			
		||||
		kv["bert.attention.layer_norm_epsilon"] = layerNormEpsilon
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	kv["tokenizer.ggml.model"] = "bert"
 | 
			
		||||
	kv["tokenizer.ggml.token_type_count"] = uint32(2)
 | 
			
		||||
 | 
			
		||||
	// convert to phantom space tokens
 | 
			
		||||
	for i, e := range t.Tokens {
 | 
			
		||||
		if strings.HasPrefix(e, "[") && strings.HasSuffix(e, "]") {
 | 
			
		||||
			// noop
 | 
			
		||||
		} else if strings.HasPrefix(e, "##") {
 | 
			
		||||
			t.Tokens[i] = e[2:]
 | 
			
		||||
		} else {
 | 
			
		||||
			t.Tokens[i] = "\u2581" + e
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	kv["tokenizer.ggml.tokens"] = t.Tokens
 | 
			
		||||
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *bertModel) Tensors(ts []Tensor) []*ggml.Tensor {
 | 
			
		||||
	var out []*ggml.Tensor
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		if slices.Contains([]string{
 | 
			
		||||
			"embeddings.position_ids",
 | 
			
		||||
			"pooler.dense.weight",
 | 
			
		||||
			"pooler.dense.bias",
 | 
			
		||||
		}, t.Name()) {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		out = append(out, &ggml.Tensor{
 | 
			
		||||
			Name:     t.Name(),
 | 
			
		||||
			Kind:     t.Kind(),
 | 
			
		||||
			Shape:    t.Shape(),
 | 
			
		||||
			WriterTo: t,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (bertModel) Replacements() []string {
 | 
			
		||||
	return []string{
 | 
			
		||||
		"encoder.layer", "blk",
 | 
			
		||||
		"encoder.layers", "blk",
 | 
			
		||||
		"embeddings.word_embeddings", "token_embd",
 | 
			
		||||
		"embeddings.token_type_embeddings", "token_types",
 | 
			
		||||
		"embeddings.LayerNorm", "token_embd_norm",
 | 
			
		||||
		"embeddings.position_embeddings", "position_embd",
 | 
			
		||||
		"attention.self.query", "attn_q",
 | 
			
		||||
		"attention.self.key", "attn_k",
 | 
			
		||||
		"attention.self.value", "attn_v",
 | 
			
		||||
		"attention.output.dense", "attn_output",
 | 
			
		||||
		"attention.output.LayerNorm", "attn_output_norm",
 | 
			
		||||
		"intermediate.dense", "ffn_up",
 | 
			
		||||
		"output.dense", "ffn_down",
 | 
			
		||||
		"output.LayerNorm", "layer_output_norm",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										76
									
								
								convert/convert_commandr.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								convert/convert_commandr.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"cmp"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/fs/ggml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type commandrModel struct {
 | 
			
		||||
	ModelParameters
 | 
			
		||||
	MaxPositionEmbeddings uint32  `json:"max_position_embeddings"`
 | 
			
		||||
	HiddenSize            uint32  `json:"hidden_size"`
 | 
			
		||||
	HiddenLayers          uint32  `json:"num_hidden_layers"`
 | 
			
		||||
	IntermediateSize      uint32  `json:"intermediate_size"`
 | 
			
		||||
	NumAttentionHeads     uint32  `json:"num_attention_heads"`
 | 
			
		||||
	NumKeyValueHeads      uint32  `json:"num_key_value_heads"`
 | 
			
		||||
	LayerNormEPS          float32 `json:"layer_norm_eps"`
 | 
			
		||||
	RopeTheta             float32 `json:"rope_theta"`
 | 
			
		||||
	UseQKNorm             bool    `json:"use_qk_norm"`
 | 
			
		||||
	MaxLength             uint32  `json:"model_max_length"`
 | 
			
		||||
	LogitScale            float32 `json:"logit_scale"`
 | 
			
		||||
	NCtx                  uint32  `json:"n_ctx"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ ModelConverter = (*commandrModel)(nil)
 | 
			
		||||
 | 
			
		||||
func (p *commandrModel) KV(t *Tokenizer) ggml.KV {
 | 
			
		||||
	kv := p.ModelParameters.KV(t)
 | 
			
		||||
	kv["general.architecture"] = "command-r"
 | 
			
		||||
	kv["general.name"] = "command-r"
 | 
			
		||||
	kv["command-r.context_length"] = cmp.Or(p.MaxLength, p.MaxPositionEmbeddings, p.NCtx)
 | 
			
		||||
	kv["command-r.embedding_length"] = p.HiddenSize
 | 
			
		||||
	kv["command-r.block_count"] = p.HiddenLayers
 | 
			
		||||
	kv["command-r.feed_forward_length"] = p.IntermediateSize
 | 
			
		||||
	kv["command-r.attention.head_count"] = p.NumAttentionHeads
 | 
			
		||||
	kv["command-r.attention.head_count_kv"] = p.NumKeyValueHeads
 | 
			
		||||
	kv["command-r.attention.layer_norm_epsilon"] = p.LayerNormEPS
 | 
			
		||||
	kv["command-r.rope.freq_base"] = p.RopeTheta
 | 
			
		||||
	kv["command-r.max_position_embeddings"] = cmp.Or(p.MaxLength, p.MaxPositionEmbeddings)
 | 
			
		||||
	kv["command-r.logit_scale"] = p.LogitScale
 | 
			
		||||
	kv["command-r.rope.scaling.type"] = "none"
 | 
			
		||||
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *commandrModel) Tensors(ts []Tensor) []*ggml.Tensor {
 | 
			
		||||
	var out []*ggml.Tensor
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		out = append(out, &ggml.Tensor{
 | 
			
		||||
			Name:     t.Name(),
 | 
			
		||||
			Kind:     t.Kind(),
 | 
			
		||||
			Shape:    t.Shape(),
 | 
			
		||||
			WriterTo: t,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *commandrModel) Replacements() []string {
 | 
			
		||||
	return []string{
 | 
			
		||||
		"self_attn.q_norm", "attn_q_norm",
 | 
			
		||||
		"self_attn.k_norm", "attn_k_norm",
 | 
			
		||||
		"model.layers", "blk",
 | 
			
		||||
		"input_layernorm", "attn_norm",
 | 
			
		||||
		"mlp.down_proj", "ffn_down",
 | 
			
		||||
		"mlp.gate_proj", "ffn_gate",
 | 
			
		||||
		"mlp.up_proj", "ffn_up",
 | 
			
		||||
		"self_attn.k_proj", "attn_k",
 | 
			
		||||
		"self_attn.o_proj", "attn_output",
 | 
			
		||||
		"self_attn.q_proj", "attn_q",
 | 
			
		||||
		"self_attn.v_proj", "attn_v",
 | 
			
		||||
		"model.norm", "output_norm",
 | 
			
		||||
		"model.embed_tokens", "token_embd",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										100
									
								
								convert/convert_gemma.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								convert/convert_gemma.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/pdevine/tensor"
 | 
			
		||||
	"github.com/pdevine/tensor/native"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/fs/ggml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type gemmaModel struct {
 | 
			
		||||
	ModelParameters
 | 
			
		||||
	MaxPositionEmbeddings uint32  `json:"max_position_embeddings"`
 | 
			
		||||
	HiddenSize            uint32  `json:"hidden_size"`
 | 
			
		||||
	HiddenLayers          uint32  `json:"num_hidden_layers"`
 | 
			
		||||
	IntermediateSize      uint32  `json:"intermediate_size"`
 | 
			
		||||
	NumAttentionHeads     uint32  `json:"num_attention_heads"`
 | 
			
		||||
	NumKeyValueHeads      uint32  `json:"num_key_value_heads"`
 | 
			
		||||
	RMSNormEPS            float32 `json:"rms_norm_eps"`
 | 
			
		||||
	HeadDim               uint32  `json:"head_dim"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ ModelConverter = (*gemmaModel)(nil)
 | 
			
		||||
 | 
			
		||||
func (p *gemmaModel) KV(t *Tokenizer) ggml.KV {
 | 
			
		||||
	kv := p.ModelParameters.KV(t)
 | 
			
		||||
	kv["general.architecture"] = "gemma"
 | 
			
		||||
	kv["gemma.context_length"] = p.MaxPositionEmbeddings
 | 
			
		||||
	kv["gemma.embedding_length"] = p.HiddenSize
 | 
			
		||||
	kv["gemma.block_count"] = p.HiddenLayers
 | 
			
		||||
	kv["gemma.feed_forward_length"] = p.IntermediateSize
 | 
			
		||||
	kv["gemma.attention.head_count"] = p.NumAttentionHeads
 | 
			
		||||
	kv["gemma.attention.head_count_kv"] = p.NumKeyValueHeads
 | 
			
		||||
	kv["gemma.attention.layer_norm_rms_epsilon"] = p.RMSNormEPS
 | 
			
		||||
	kv["gemma.attention.key_length"] = p.HeadDim
 | 
			
		||||
	kv["gemma.attention.value_length"] = p.HeadDim
 | 
			
		||||
	kv["tokenizer.ggml.eot_token_id"] = uint32(107)
 | 
			
		||||
	kv["tokenizer.ggml.middle_token_id"] = uint32(68)
 | 
			
		||||
	kv["tokenizer.ggml.prefix_token_id"] = uint32(67)
 | 
			
		||||
	kv["tokenizer.ggml.suffix_token_id"] = uint32(69)
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *gemmaModel) Tensors(ts []Tensor) []*ggml.Tensor {
 | 
			
		||||
	var out []*ggml.Tensor
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		if !strings.HasPrefix(t.Name(), "v.") && strings.HasSuffix(t.Name(), "_norm.weight") {
 | 
			
		||||
			t.SetRepacker(p.addOne)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		out = append(out, &ggml.Tensor{
 | 
			
		||||
			Name:     t.Name(),
 | 
			
		||||
			Kind:     t.Kind(),
 | 
			
		||||
			Shape:    t.Shape(),
 | 
			
		||||
			WriterTo: t,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *gemmaModel) Replacements() []string {
 | 
			
		||||
	return []string{
 | 
			
		||||
		"model.embed_tokens", "token_embd",
 | 
			
		||||
		"model.norm", "output_norm",
 | 
			
		||||
		"model.layers", "blk",
 | 
			
		||||
		"input_layernorm", "attn_norm",
 | 
			
		||||
		"self_attn.q_proj", "attn_q",
 | 
			
		||||
		"self_attn.k_proj", "attn_k",
 | 
			
		||||
		"self_attn.v_proj", "attn_v",
 | 
			
		||||
		"self_attn.o_proj", "attn_output",
 | 
			
		||||
		"mlp.gate_proj", "ffn_gate",
 | 
			
		||||
		"mlp.down_proj", "ffn_down",
 | 
			
		||||
		"mlp.up_proj", "ffn_up",
 | 
			
		||||
		"post_attention_layernorm", "ffn_norm",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*gemmaModel) addOne(_ string, data []float32, shape []uint64) ([]float32, error) {
 | 
			
		||||
	n := tensor.New(tensor.WithShape(int(shape[0])), tensor.WithBacking(data))
 | 
			
		||||
	ones := tensor.Ones(tensor.Float32, int(shape[0]))
 | 
			
		||||
 | 
			
		||||
	n, err := n.Add(ones)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ts, err := native.SelectF32(n, 0)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var f32s []float32
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		f32s = append(f32s, t...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return f32s, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										51
									
								
								convert/convert_gemma2.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								convert/convert_gemma2.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import "github.com/ollama/ollama/fs/ggml"
 | 
			
		||||
 | 
			
		||||
type gemma2Model struct {
 | 
			
		||||
	gemmaModel
 | 
			
		||||
	SlidingWindow         uint32  `json:"sliding_window"`
 | 
			
		||||
	AttentionLogitSoftcap float32 `json:"attn_logit_softcapping"`
 | 
			
		||||
	FinalLogitSoftcap     float32 `json:"final_logit_softcapping"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *gemma2Model) KV(t *Tokenizer) ggml.KV {
 | 
			
		||||
	kv := p.ModelParameters.KV(t)
 | 
			
		||||
	kv["general.architecture"] = "gemma2"
 | 
			
		||||
	kv["gemma2.context_length"] = p.MaxPositionEmbeddings
 | 
			
		||||
	kv["gemma2.embedding_length"] = p.HiddenSize
 | 
			
		||||
	kv["gemma2.block_count"] = p.HiddenLayers
 | 
			
		||||
	kv["gemma2.feed_forward_length"] = p.IntermediateSize
 | 
			
		||||
	kv["gemma2.attention.head_count"] = p.NumAttentionHeads
 | 
			
		||||
	kv["gemma2.attention.head_count_kv"] = p.NumKeyValueHeads
 | 
			
		||||
	kv["gemma2.attention.layer_norm_rms_epsilon"] = p.RMSNormEPS
 | 
			
		||||
	kv["gemma2.attention.key_length"] = p.HeadDim
 | 
			
		||||
	kv["gemma2.attention.value_length"] = p.HeadDim
 | 
			
		||||
	kv["gemma2.attention.sliding_window"] = p.SlidingWindow
 | 
			
		||||
	kv["gemma2.attn_logit_softcapping"] = p.AttentionLogitSoftcap
 | 
			
		||||
	kv["gemma2.final_logit_softcapping"] = p.FinalLogitSoftcap
 | 
			
		||||
	kv["tokenizer.ggml.eot_token_id"] = uint32(107)
 | 
			
		||||
	kv["tokenizer.ggml.middle_token_id"] = uint32(68)
 | 
			
		||||
	kv["tokenizer.ggml.prefix_token_id"] = uint32(67)
 | 
			
		||||
	kv["tokenizer.ggml.suffix_token_id"] = uint32(69)
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *gemma2Model) Replacements() []string {
 | 
			
		||||
	return []string{
 | 
			
		||||
		"model.embed_tokens", "token_embd",
 | 
			
		||||
		"model.norm", "output_norm",
 | 
			
		||||
		"model.layers", "blk",
 | 
			
		||||
		"input_layernorm", "attn_norm",
 | 
			
		||||
		"self_attn.q_proj", "attn_q",
 | 
			
		||||
		"self_attn.k_proj", "attn_k",
 | 
			
		||||
		"self_attn.v_proj", "attn_v",
 | 
			
		||||
		"self_attn.o_proj", "attn_output",
 | 
			
		||||
		"mlp.gate_proj", "ffn_gate",
 | 
			
		||||
		"mlp.down_proj", "ffn_down",
 | 
			
		||||
		"mlp.up_proj", "ffn_up",
 | 
			
		||||
		"post_attention_layernorm", "post_attention_norm",
 | 
			
		||||
		"pre_feedforward_layernorm", "ffn_norm",
 | 
			
		||||
		"post_feedforward_layernorm", "post_ffw_norm",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										91
									
								
								convert/convert_gemma2_adapter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								convert/convert_gemma2_adapter.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/pdevine/tensor"
 | 
			
		||||
	"github.com/pdevine/tensor/native"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/fs/ggml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type gemma2Adapter struct {
 | 
			
		||||
	AdapterParameters
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ AdapterConverter = (*gemma2Adapter)(nil)
 | 
			
		||||
 | 
			
		||||
func (p *gemma2Adapter) KV(baseKV ggml.KV) ggml.KV {
 | 
			
		||||
	kv := p.AdapterParameters.KV()
 | 
			
		||||
	kv["general.architecture"] = "gemma2"
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *gemma2Adapter) Tensors(ts []Tensor) []*ggml.Tensor {
 | 
			
		||||
	var out []*ggml.Tensor
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		shape := t.Shape()
 | 
			
		||||
		if (strings.HasSuffix(t.Name(), "weight.lora_a") && shape[0] > shape[1]) ||
 | 
			
		||||
			(strings.HasSuffix(t.Name(), "weight.lora_b") && shape[0] < shape[1]) {
 | 
			
		||||
			shape[0], shape[1] = shape[1], shape[0]
 | 
			
		||||
			t.SetRepacker(p.repack)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		out = append(out, &ggml.Tensor{
 | 
			
		||||
			Name:     t.Name(),
 | 
			
		||||
			Kind:     t.Kind(),
 | 
			
		||||
			Shape:    t.Shape(),
 | 
			
		||||
			WriterTo: t,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *gemma2Adapter) Replacements() []string {
 | 
			
		||||
	return []string{
 | 
			
		||||
		"base_model.model.", "",
 | 
			
		||||
		"model.layers", "blk",
 | 
			
		||||
		"self_attn.q_proj", "attn_q",
 | 
			
		||||
		"self_attn.k_proj", "attn_k",
 | 
			
		||||
		"self_attn.v_proj", "attn_v",
 | 
			
		||||
		"self_attn.o_proj", "attn_output",
 | 
			
		||||
		"mlp.gate_proj", "ffn_gate",
 | 
			
		||||
		"mlp.down_proj", "ffn_down",
 | 
			
		||||
		"mlp.up_proj", "ffn_up",
 | 
			
		||||
		"lora_A.weight", "weight.lora_a",
 | 
			
		||||
		"lora_B.weight", "weight.lora_b",
 | 
			
		||||
		"lora_a", "weight.lora_a",
 | 
			
		||||
		"lora_b", "weight.lora_b",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *gemma2Adapter) repack(name string, data []float32, shape []uint64) ([]float32, error) {
 | 
			
		||||
	dims := []int{int(shape[1]), int(shape[0])}
 | 
			
		||||
 | 
			
		||||
	n := tensor.New(tensor.WithShape(dims...), tensor.WithBacking(data))
 | 
			
		||||
 | 
			
		||||
	if err := n.T(1, 0); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := n.Reshape(dims...); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := n.Transpose(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ts, err := native.SelectF32(n, 1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var f32s []float32
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		f32s = append(f32s, t...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return f32s, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										142
									
								
								convert/convert_gemma3.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								convert/convert_gemma3.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"cmp"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/fs/ggml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type gemma3Model struct {
 | 
			
		||||
	gemmaModel
 | 
			
		||||
	Architecture string
 | 
			
		||||
	TextModel    struct {
 | 
			
		||||
		HeadDim          uint32 `json:"head_dim"`
 | 
			
		||||
		HiddenSize       uint32 `json:"hidden_size"`
 | 
			
		||||
		HiddenLayers     uint32 `json:"num_hidden_layers"`
 | 
			
		||||
		IntermediateSize uint32 `json:"intermediate_size"`
 | 
			
		||||
		SlidingWindow    uint32 `json:"sliding_window"`
 | 
			
		||||
	} `json:"text_config"`
 | 
			
		||||
	VisionModel struct {
 | 
			
		||||
		NumAttentionHeads uint32  `json:"num_attention_heads"` // attention.head_count 16
 | 
			
		||||
		LayerNormEpsilon  float32 `json:"layer_norm_eps"`      // attention.layer_norm_epsilon 1e-05
 | 
			
		||||
		NumHiddenLayers   uint32  `json:"num_hidden_layers"`   // block_count 32
 | 
			
		||||
		HiddenSize        uint32  `json:"hidden_size"`         // embedding_length 1280
 | 
			
		||||
		IntermediateSize  uint32  `json:"intermediate_size"`   // feed_forward_length 5120
 | 
			
		||||
		ImageSize         uint32  `json:"image_size"`          // image_size 560
 | 
			
		||||
		NumChannels       uint32  `json:"num_channels"`        // num_channels 3
 | 
			
		||||
		PatchSize         uint32  `json:"patch_size"`          // patch_size 14
 | 
			
		||||
	} `json:"vision_config"`
 | 
			
		||||
	MaxPositionEmbeddings    uint32  `json:"max_position_embeddings"`
 | 
			
		||||
	NumAttentionHeads        uint32  `json:"num_attention_heads"`
 | 
			
		||||
	NumKeyValueHeads         uint32  `json:"num_key_value_heads"`
 | 
			
		||||
	RMSNormEPS               float32 `json:"rms_norm_eps"`
 | 
			
		||||
	HeadDim                  uint32  `json:"head_dim"`
 | 
			
		||||
	FinalLogitSoftcap        float32 `json:"final_logit_softcapping"`
 | 
			
		||||
	RopeLocalTheta           float32 `json:"rope_local_base_freq"`
 | 
			
		||||
	RopeGlobalTheta          float32 `json:"rope_global_base_freq"`
 | 
			
		||||
	SlidingWindow            uint32  `json:"sliding_window"`
 | 
			
		||||
	MultiModalTokensPerImage uint32  `json:"mm_tokens_per_image"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	gemma4BLayerCount  = 34
 | 
			
		||||
	gemma12BLayerCount = 48
 | 
			
		||||
	gemma27BLayerCount = 62
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (p *gemma3Model) KV(t *Tokenizer) ggml.KV {
 | 
			
		||||
	kv := p.ModelParameters.KV(t)
 | 
			
		||||
	kv["general.architecture"] = "gemma3"
 | 
			
		||||
 | 
			
		||||
	numBlocks := cmp.Or(p.HiddenLayers, p.TextModel.HiddenLayers)
 | 
			
		||||
	kv["gemma3.block_count"] = numBlocks
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		numHeads   uint32
 | 
			
		||||
		numKVHeads uint32
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	switch numBlocks {
 | 
			
		||||
	case gemma4BLayerCount:
 | 
			
		||||
		numHeads = 8
 | 
			
		||||
		numKVHeads = 4
 | 
			
		||||
	case gemma12BLayerCount:
 | 
			
		||||
		numHeads = 16
 | 
			
		||||
		numKVHeads = 8
 | 
			
		||||
	case gemma27BLayerCount:
 | 
			
		||||
		numHeads = 32
 | 
			
		||||
		numKVHeads = 16
 | 
			
		||||
	default:
 | 
			
		||||
		numHeads = p.NumAttentionHeads
 | 
			
		||||
		numKVHeads = p.NumKeyValueHeads
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	kv["gemma3.attention.head_count"] = numHeads
 | 
			
		||||
	kv["gemma3.attention.head_count_kv"] = numKVHeads
 | 
			
		||||
 | 
			
		||||
	switch p.Architecture {
 | 
			
		||||
	case "Gemma3ForCausalLM":
 | 
			
		||||
		kv["gemma3.context_length"] = p.MaxPositionEmbeddings
 | 
			
		||||
		kv["gemma3.attention.layer_norm_rms_epsilon"] = p.RMSNormEPS
 | 
			
		||||
		kv["gemma3.attention.key_length"] = p.HeadDim
 | 
			
		||||
		kv["gemma3.attention.value_length"] = p.HeadDim
 | 
			
		||||
		kv["gemma3.attention.sliding_window"] = p.SlidingWindow
 | 
			
		||||
		kv["gemma3.final_logit_softcapping"] = cmp.Or(p.FinalLogitSoftcap, 30)
 | 
			
		||||
		kv["gemma3.rope.local.freq_base"] = cmp.Or(p.RopeLocalTheta, 10000.0)
 | 
			
		||||
		kv["gemma3.rope.global.freq_base"] = cmp.Or(p.RopeGlobalTheta, 1000000.0)
 | 
			
		||||
		kv["gemma3.embedding_length"] = p.HiddenSize
 | 
			
		||||
		kv["gemma3.feed_forward_length"] = p.IntermediateSize
 | 
			
		||||
	default:
 | 
			
		||||
		kv["gemma3.context_length"] = cmp.Or(p.MaxPositionEmbeddings, 131072)
 | 
			
		||||
		kv["gemma3.embedding_length"] = p.TextModel.HiddenSize
 | 
			
		||||
		kv["gemma3.feed_forward_length"] = p.TextModel.IntermediateSize
 | 
			
		||||
		kv["gemma3.attention.sliding_window"] = p.TextModel.SlidingWindow
 | 
			
		||||
		kv["gemma3.vision.block_count"] = p.VisionModel.NumHiddenLayers
 | 
			
		||||
		kv["gemma3.vision.embedding_length"] = p.VisionModel.HiddenSize
 | 
			
		||||
		kv["gemma3.vision.feed_forward_length"] = p.VisionModel.IntermediateSize
 | 
			
		||||
		kv["gemma3.vision.image_size"] = p.VisionModel.ImageSize
 | 
			
		||||
		kv["gemma3.vision.patch_size"] = p.VisionModel.PatchSize
 | 
			
		||||
		kv["gemma3.vision.num_channels"] = cmp.Or(p.VisionModel.NumChannels, 3)
 | 
			
		||||
		kv["gemma3.vision.attention.head_count"] = p.VisionModel.NumAttentionHeads
 | 
			
		||||
		kv["gemma3.vision.attention.layer_norm_epsilon"] = cmp.Or(p.VisionModel.LayerNormEpsilon, 1e-6)
 | 
			
		||||
		kv["gemma3.attention.key_length"] = cmp.Or(p.TextModel.HeadDim, 256)
 | 
			
		||||
		kv["gemma3.attention.value_length"] = cmp.Or(p.TextModel.HeadDim, 256)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.MultiModalTokensPerImage > 0 {
 | 
			
		||||
		kv["gemma3.mm.tokens_per_image"] = p.MultiModalTokensPerImage
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *gemma3Model) Replacements() []string {
 | 
			
		||||
	return []string{
 | 
			
		||||
		"lm_head", "output",
 | 
			
		||||
		"model.embed_tokens", "token_embd",
 | 
			
		||||
		"model.norm", "output_norm",
 | 
			
		||||
		"vision_tower.vision_model.embeddings", "v",
 | 
			
		||||
		"vision_tower.vision_model", "v",
 | 
			
		||||
		"vision_model.vision_model.embeddings", "v",
 | 
			
		||||
		"vision_model.vision_model", "v",
 | 
			
		||||
		"language_model.", "",
 | 
			
		||||
		"model.layers", "blk",
 | 
			
		||||
		"encoder.layers", "blk",
 | 
			
		||||
		"input_layernorm", "attn_norm",
 | 
			
		||||
		"self_attn.q_proj", "attn_q",
 | 
			
		||||
		"self_attn.q_norm", "attn_q_norm",
 | 
			
		||||
		"self_attn.k_proj", "attn_k",
 | 
			
		||||
		"self_attn.k_norm", "attn_k_norm",
 | 
			
		||||
		"self_attn.v_proj", "attn_v",
 | 
			
		||||
		"self_attn.o_proj", "attn_output",
 | 
			
		||||
		"self_attn.out_proj", "attn_output",
 | 
			
		||||
		"mlp.gate_proj", "ffn_gate",
 | 
			
		||||
		"mlp.down_proj", "ffn_down",
 | 
			
		||||
		"mlp.up_proj", "ffn_up",
 | 
			
		||||
		"post_attention_layernorm", "post_attention_norm",
 | 
			
		||||
		"pre_feedforward_layernorm", "ffn_norm",
 | 
			
		||||
		"post_feedforward_layernorm", "post_ffw_norm",
 | 
			
		||||
		"input_projection_weight", "input_projection.weight",
 | 
			
		||||
		"multi_modal_projector", "mm",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										220
									
								
								convert/convert_llama.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								convert/convert_llama.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,220 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"cmp"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"math"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/pdevine/tensor"
 | 
			
		||||
	"github.com/pdevine/tensor/native"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/fs/ggml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type llamaModel struct {
 | 
			
		||||
	ModelParameters
 | 
			
		||||
	NLayers               uint32  `json:"n_layers"`
 | 
			
		||||
	NumHiddenLayers       uint32  `json:"num_hidden_layers"`
 | 
			
		||||
	NLayer                uint32  `json:"n_layer"`
 | 
			
		||||
	MaxPositionEmbeddings uint32  `json:"max_position_embeddings"`
 | 
			
		||||
	NCtx                  uint32  `json:"n_ctx"`
 | 
			
		||||
	HiddenSize            uint32  `json:"hidden_size"`
 | 
			
		||||
	NEmbd                 uint32  `json:"n_embd"`
 | 
			
		||||
	IntermediateSize      uint32  `json:"intermediate_size"`
 | 
			
		||||
	NInner                uint32  `json:"n_inner"`
 | 
			
		||||
	NumAttentionHeads     uint32  `json:"num_attention_heads"`
 | 
			
		||||
	NHead                 uint32  `json:"n_head"`
 | 
			
		||||
	NumKeyValueHeads      uint32  `json:"num_key_value_heads"`
 | 
			
		||||
	RopeTheta             float32 `json:"rope_theta"`
 | 
			
		||||
	RopeScaling           struct {
 | 
			
		||||
		Type                          string  `json:"type"`
 | 
			
		||||
		RopeType                      string  `json:"rope_type"`
 | 
			
		||||
		Factor                        float32 `json:"factor"`
 | 
			
		||||
		LowFrequencyFactor            float32 `json:"low_freq_factor"`
 | 
			
		||||
		HighFrequencyFactor           float32 `json:"high_freq_factor"`
 | 
			
		||||
		OriginalMaxPositionEmbeddings uint32  `json:"original_max_position_embeddings"`
 | 
			
		||||
 | 
			
		||||
		factors ropeFactor
 | 
			
		||||
	} `json:"rope_scaling"`
 | 
			
		||||
	RMSNormEPS       float32 `json:"rms_norm_eps"`
 | 
			
		||||
	LayerNormEPS     float32 `json:"layer_norm_eps"`
 | 
			
		||||
	LayerNormEpsilon float32 `json:"layer_norm_epsilon"`
 | 
			
		||||
	NormEpsilon      float32 `json:"norm_epsilon"`
 | 
			
		||||
	HeadDim          uint32  `json:"head_dim"`
 | 
			
		||||
 | 
			
		||||
	skipRepack bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ ModelConverter = (*llamaModel)(nil)
 | 
			
		||||
 | 
			
		||||
func (p *llamaModel) KV(t *Tokenizer) ggml.KV {
 | 
			
		||||
	kv := p.ModelParameters.KV(t)
 | 
			
		||||
	kv["general.architecture"] = "llama"
 | 
			
		||||
	kv["llama.vocab_size"] = p.VocabSize
 | 
			
		||||
 | 
			
		||||
	kv["llama.block_count"] = cmp.Or(p.NLayers, p.NumHiddenLayers, p.NLayer)
 | 
			
		||||
 | 
			
		||||
	if contextLength := cmp.Or(p.MaxPositionEmbeddings, p.NCtx); contextLength > 0 {
 | 
			
		||||
		kv["llama.context_length"] = contextLength
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if embeddingLength := cmp.Or(p.HiddenSize, p.NEmbd); embeddingLength > 0 {
 | 
			
		||||
		kv["llama.embedding_length"] = cmp.Or(p.HiddenSize, p.NEmbd)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if feedForwardLength := cmp.Or(p.IntermediateSize, p.NInner); feedForwardLength > 0 {
 | 
			
		||||
		kv["llama.feed_forward_length"] = cmp.Or(p.IntermediateSize, p.NInner)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if headCount := cmp.Or(p.NumAttentionHeads, p.NHead); headCount > 0 {
 | 
			
		||||
		kv["llama.attention.head_count"] = cmp.Or(p.NumAttentionHeads, p.NHead)
 | 
			
		||||
		kv["llama.rope.dimension_count"] = p.HiddenSize / headCount
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.HeadDim > 0 {
 | 
			
		||||
		kv["llama.attention.head_dim"] = p.HeadDim
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.RopeTheta > 0 {
 | 
			
		||||
		kv["llama.rope.freq_base"] = p.RopeTheta
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.RopeScaling.Type == "linear" {
 | 
			
		||||
		kv["llama.rope.scaling.type"] = p.RopeScaling.Type
 | 
			
		||||
		kv["llama.rope.scaling.factor"] = p.RopeScaling.Factor
 | 
			
		||||
	} else if p.RopeScaling.RopeType == "llama3" {
 | 
			
		||||
		dim := p.HiddenSize / p.NumAttentionHeads
 | 
			
		||||
		for i := uint32(0); i < dim; i += 2 {
 | 
			
		||||
			factor := cmp.Or(p.RopeScaling.Factor, 8.0)
 | 
			
		||||
			factorLow := cmp.Or(p.RopeScaling.LowFrequencyFactor, 1.0)
 | 
			
		||||
			factorHigh := cmp.Or(p.RopeScaling.HighFrequencyFactor, 4.0)
 | 
			
		||||
 | 
			
		||||
			original := cmp.Or(p.RopeScaling.OriginalMaxPositionEmbeddings, 8192)
 | 
			
		||||
			lambdaLow := float32(original) / factorLow
 | 
			
		||||
			lambdaHigh := float32(original) / factorHigh
 | 
			
		||||
 | 
			
		||||
			lambda := 2 * math.Pi * math.Pow(float64(p.RopeTheta), float64(i)/float64(dim))
 | 
			
		||||
			if lambda < float64(lambdaHigh) {
 | 
			
		||||
				p.RopeScaling.factors = append(p.RopeScaling.factors, 1.0)
 | 
			
		||||
			} else if lambda > float64(lambdaLow) {
 | 
			
		||||
				p.RopeScaling.factors = append(p.RopeScaling.factors, factor)
 | 
			
		||||
			} else {
 | 
			
		||||
				smooth := (float32(original)/float32(lambda) - factorLow) / (factorHigh - factorLow)
 | 
			
		||||
				p.RopeScaling.factors = append(p.RopeScaling.factors, 1.0/((1-smooth)/factor+smooth))
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.NumKeyValueHeads > 0 {
 | 
			
		||||
		kv["llama.attention.head_count_kv"] = p.NumKeyValueHeads
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.RMSNormEPS > 0 {
 | 
			
		||||
		kv["llama.attention.layer_norm_rms_epsilon"] = p.RMSNormEPS
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if layerNormEpsilon := cmp.Or(p.LayerNormEPS, p.LayerNormEpsilon, p.NormEpsilon); layerNormEpsilon > 0 {
 | 
			
		||||
		kv["llama.attention.layer_norm_epsilon"] = layerNormEpsilon
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.HeadDim > 0 {
 | 
			
		||||
		kv["llama.attention.key_length"] = p.HeadDim
 | 
			
		||||
		kv["llama.attention.value_length"] = p.HeadDim
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *llamaModel) Tensors(ts []Tensor) []*ggml.Tensor {
 | 
			
		||||
	var out []*ggml.Tensor
 | 
			
		||||
 | 
			
		||||
	if p.RopeScaling.factors != nil {
 | 
			
		||||
		out = append(out, &ggml.Tensor{
 | 
			
		||||
			Name:     "rope_freqs.weight",
 | 
			
		||||
			Kind:     0,
 | 
			
		||||
			Shape:    []uint64{uint64(len(p.RopeScaling.factors))},
 | 
			
		||||
			WriterTo: p.RopeScaling.factors,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		if strings.HasSuffix(t.Name(), "attn_q.weight") || strings.HasSuffix(t.Name(), "attn_k.weight") {
 | 
			
		||||
			if !p.skipRepack {
 | 
			
		||||
				t.SetRepacker(p.repack)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		out = append(out, &ggml.Tensor{
 | 
			
		||||
			Name:     t.Name(),
 | 
			
		||||
			Kind:     t.Kind(),
 | 
			
		||||
			Shape:    t.Shape(),
 | 
			
		||||
			WriterTo: t,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *llamaModel) Replacements() []string {
 | 
			
		||||
	return []string{
 | 
			
		||||
		"lm_head", "output",
 | 
			
		||||
		"model.embed_tokens", "token_embd",
 | 
			
		||||
		"model.norm", "output_norm",
 | 
			
		||||
		"model.layers", "blk",
 | 
			
		||||
		"input_layernorm", "attn_norm",
 | 
			
		||||
		"self_attn.q_proj", "attn_q",
 | 
			
		||||
		"self_attn.k_proj", "attn_k",
 | 
			
		||||
		"self_attn.v_proj", "attn_v",
 | 
			
		||||
		"self_attn.o_proj", "attn_output",
 | 
			
		||||
		"mlp.gate_proj", "ffn_gate",
 | 
			
		||||
		"mlp.down_proj", "ffn_down",
 | 
			
		||||
		"mlp.up_proj", "ffn_up",
 | 
			
		||||
		"post_attention_layernorm", "ffn_norm",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *llamaModel) repack(name string, data []float32, shape []uint64) ([]float32, error) {
 | 
			
		||||
	var dims []int
 | 
			
		||||
	for _, dim := range shape {
 | 
			
		||||
		dims = append(dims, int(dim))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var heads uint32
 | 
			
		||||
	if strings.HasSuffix(name, "attn_q.weight") {
 | 
			
		||||
		heads = p.NumAttentionHeads
 | 
			
		||||
	} else if strings.HasSuffix(name, "attn_k.weight") {
 | 
			
		||||
		heads = cmp.Or(p.NumKeyValueHeads, p.NumAttentionHeads)
 | 
			
		||||
	} else {
 | 
			
		||||
		return nil, fmt.Errorf("unknown tensor for repack: %s", name)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	n := tensor.New(tensor.WithShape(dims...), tensor.WithBacking(data))
 | 
			
		||||
	if err := n.Reshape(append([]int{int(heads), 2, dims[0] / int(heads) / 2}, dims[1:]...)...); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := n.T(0, 2, 1, 3); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := n.Reshape(dims...); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := n.Transpose(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ts, err := native.SelectF32(n, 1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var f32s []float32
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		f32s = append(f32s, t...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return f32s, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										169
									
								
								convert/convert_llama4.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								convert/convert_llama4.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,169 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/pdevine/tensor"
 | 
			
		||||
	"github.com/pdevine/tensor/native"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/fs/ggml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type llama4Model struct {
 | 
			
		||||
	ModelParameters
 | 
			
		||||
	TextModel struct {
 | 
			
		||||
		llamaModel
 | 
			
		||||
		NumExpertsPerToken     uint32 `json:"num_experts_per_tok"`
 | 
			
		||||
		NumLocalExperts        uint32 `json:"num_local_experts"`
 | 
			
		||||
		InterleaveMOELayerStep uint32 `json:"interleave_moe_layer_step"`
 | 
			
		||||
		UseQKNorm              bool   `json:"use_qk_norm"`
 | 
			
		||||
		IntermediateSizeMLP    uint32 `json:"intermediate_size_mlp"`
 | 
			
		||||
		AttentionChunkSize     uint32 `json:"attention_chunk_size"`
 | 
			
		||||
	} `json:"text_config"`
 | 
			
		||||
	VisionModel struct {
 | 
			
		||||
		NumHiddenLayers   uint32  `json:"num_hidden_layers"`
 | 
			
		||||
		HiddenSize        uint32  `json:"hidden_size"`
 | 
			
		||||
		IntermediateSize  uint32  `json:"intermediate_size"`
 | 
			
		||||
		NumAttentionHeads uint32  `json:"num_attention_heads"`
 | 
			
		||||
		ImageSize         uint32  `json:"image_size"`
 | 
			
		||||
		PatchSize         uint32  `json:"patch_size"`
 | 
			
		||||
		RopeTheta         float32 `json:"rope_theta"`
 | 
			
		||||
		NormEpsilon       float32 `json:"norm_eps"`
 | 
			
		||||
		PixelShuffleRatio float32 `json:"pixel_shuffle_ratio"`
 | 
			
		||||
	} `json:"vision_config"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// KV implements ModelConverter.
 | 
			
		||||
func (p *llama4Model) KV(t *Tokenizer) ggml.KV {
 | 
			
		||||
	kv := p.ModelParameters.KV(t)
 | 
			
		||||
	kv["general.architecture"] = "llama4"
 | 
			
		||||
 | 
			
		||||
	for k, v := range p.TextModel.KV(t) {
 | 
			
		||||
		if strings.HasPrefix(k, "llama.") {
 | 
			
		||||
			kv[strings.ReplaceAll(k, "llama.", "llama4.")] = v
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	kv["llama4.feed_forward_length"] = p.TextModel.IntermediateSizeMLP
 | 
			
		||||
	kv["llama4.expert_feed_forward_length"] = p.TextModel.IntermediateSize
 | 
			
		||||
 | 
			
		||||
	kv["llama4.expert_count"] = p.TextModel.NumLocalExperts
 | 
			
		||||
	kv["llama4.expert_used_count"] = p.TextModel.NumExpertsPerToken
 | 
			
		||||
	kv["llama4.interleave_moe_layer_step"] = p.TextModel.InterleaveMOELayerStep
 | 
			
		||||
	kv["llama4.use_qk_norm"] = p.TextModel.UseQKNorm
 | 
			
		||||
	kv["llama4.attention.chunk_size"] = p.TextModel.AttentionChunkSize
 | 
			
		||||
 | 
			
		||||
	kv["llama4.vision.block_count"] = p.VisionModel.NumHiddenLayers
 | 
			
		||||
	kv["llama4.vision.embedding_length"] = p.VisionModel.HiddenSize
 | 
			
		||||
	kv["llama4.vision.feed_forward_length"] = p.VisionModel.IntermediateSize
 | 
			
		||||
	kv["llama4.vision.attention.head_count"] = p.VisionModel.NumAttentionHeads
 | 
			
		||||
	kv["llama4.vision.image_size"] = p.VisionModel.ImageSize
 | 
			
		||||
	kv["llama4.vision.patch_size"] = p.VisionModel.PatchSize
 | 
			
		||||
	kv["llama4.vision.rope.freq_base"] = p.VisionModel.RopeTheta
 | 
			
		||||
	kv["llama4.vision.layer_norm_epsilon"] = p.VisionModel.NormEpsilon
 | 
			
		||||
	kv["llama4.vision.pixel_shuffle_ratio"] = p.VisionModel.PixelShuffleRatio
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Replacements implements ModelConverter.
 | 
			
		||||
func (p *llama4Model) Replacements() []string {
 | 
			
		||||
	return append(
 | 
			
		||||
		p.TextModel.Replacements(),
 | 
			
		||||
		"language_model.", "",
 | 
			
		||||
		"vision_model", "v",
 | 
			
		||||
		"multi_modal_projector", "mm",
 | 
			
		||||
		"feed_forward.down_proj", "ffn_down",
 | 
			
		||||
		"feed_forward.up_proj", "ffn_up",
 | 
			
		||||
		"feed_forward.gate_proj", "ffn_gate",
 | 
			
		||||
		"feed_forward.", "ffn_",
 | 
			
		||||
		"shared_expert.down_proj", "down_shexp",
 | 
			
		||||
		"shared_expert.gate_proj", "gate_shexp",
 | 
			
		||||
		"shared_expert.up_proj", "up_shexp",
 | 
			
		||||
		"experts.down_proj", "down_exps.weight",
 | 
			
		||||
		"experts.gate_up_proj", "gate_up_exps.weight",
 | 
			
		||||
		"router", "gate_inp",
 | 
			
		||||
		"patch_embedding.linear", "patch_embedding",
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Tensors implements ModelConverter.
 | 
			
		||||
func (p *llama4Model) Tensors(ts []Tensor) []*ggml.Tensor {
 | 
			
		||||
	var out []*ggml.Tensor
 | 
			
		||||
 | 
			
		||||
	var textTensors []Tensor
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		if strings.HasPrefix(t.Name(), "v.") || strings.HasPrefix(t.Name(), "mm.") {
 | 
			
		||||
			out = append(out, &ggml.Tensor{
 | 
			
		||||
				Name:     t.Name(),
 | 
			
		||||
				Kind:     t.Kind(),
 | 
			
		||||
				Shape:    t.Shape(),
 | 
			
		||||
				WriterTo: t,
 | 
			
		||||
			})
 | 
			
		||||
		} else if strings.Contains(t.Name(), "ffn_gate_up_exps") {
 | 
			
		||||
			// gate and up projectors are fused
 | 
			
		||||
			// dims[1], dims[2] must be swapped
 | 
			
		||||
			// [experts, hidden_size, intermediate_size * 2] --> [experts, intermediate_size, hidden_size]
 | 
			
		||||
			halfDim := int(t.Shape()[2]) / 2
 | 
			
		||||
 | 
			
		||||
			newShape := slices.Clone(t.Shape())
 | 
			
		||||
			newShape[1], newShape[2] = newShape[2]/2, newShape[1]
 | 
			
		||||
			for i, name := range []string{"ffn_gate_exps", "ffn_up_exps"} {
 | 
			
		||||
				// clone tensor since we need separate repackers
 | 
			
		||||
				tt := t.Clone()
 | 
			
		||||
				tt.SetRepacker(p.repack(nil, nil, tensor.S(i*halfDim, (i+1)*halfDim)))
 | 
			
		||||
				out = append(out, &ggml.Tensor{
 | 
			
		||||
					Name:     strings.ReplaceAll(tt.Name(), "ffn_gate_up_exps", name),
 | 
			
		||||
					Kind:     tt.Kind(),
 | 
			
		||||
					Shape:    newShape,
 | 
			
		||||
					WriterTo: tt,
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		} else if strings.Contains(t.Name(), "ffn_down_exps") {
 | 
			
		||||
			// dims[1], dims[2] must be swapped
 | 
			
		||||
			// [experts, intermediate_size, hidden_size] --> [experts, hidden_size, intermediate_size]
 | 
			
		||||
			t.SetRepacker(p.repack())
 | 
			
		||||
			newShape := slices.Clone(t.Shape())
 | 
			
		||||
			newShape[1], newShape[2] = newShape[2], newShape[1]
 | 
			
		||||
			out = append(out, &ggml.Tensor{
 | 
			
		||||
				Name:     t.Name(),
 | 
			
		||||
				Kind:     t.Kind(),
 | 
			
		||||
				Shape:    newShape,
 | 
			
		||||
				WriterTo: t,
 | 
			
		||||
			})
 | 
			
		||||
		} else {
 | 
			
		||||
			textTensors = append(textTensors, t)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	p.TextModel.skipRepack = true
 | 
			
		||||
	out = append(out, p.TextModel.Tensors(textTensors)...)
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *llama4Model) repack(slice ...tensor.Slice) Repacker {
 | 
			
		||||
	return func(name string, data []float32, shape []uint64) ([]float32, error) {
 | 
			
		||||
		dims := make([]int, len(shape))
 | 
			
		||||
		for i, dim := range shape {
 | 
			
		||||
			dims[i] = int(dim)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var t tensor.Tensor = tensor.New(tensor.WithShape(dims...), tensor.WithBacking(data))
 | 
			
		||||
		t, err := t.Slice(slice...)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := t.T(0, 2, 1); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		t = tensor.Materialize(t)
 | 
			
		||||
		// flatten tensor so it can be return as a vector
 | 
			
		||||
		if err := t.Reshape(t.Shape().TotalSize()); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return native.VectorF32(t.(*tensor.Dense))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										169
									
								
								convert/convert_llama_adapter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								convert/convert_llama_adapter.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,169 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"cmp"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/pdevine/tensor"
 | 
			
		||||
	"github.com/pdevine/tensor/native"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/fs/ggml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type llamaAdapter struct {
 | 
			
		||||
	AdapterParameters
 | 
			
		||||
	NumAttentionHeads uint32 `json:"num_attention_heads"`
 | 
			
		||||
	NumKeyValueHeads  uint32 `json:"num_key_value_heads"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ AdapterConverter = (*llamaAdapter)(nil)
 | 
			
		||||
 | 
			
		||||
func (p *llamaAdapter) KV(baseKV ggml.KV) ggml.KV {
 | 
			
		||||
	kv := p.AdapterParameters.KV()
 | 
			
		||||
	kv["general.architecture"] = "llama"
 | 
			
		||||
	kv["llama.attention.head_count"] = baseKV["llama.attention.head_count"]
 | 
			
		||||
	kv["llama.attention.head_count_kv"] = baseKV["llama.attention.head_count_kv"]
 | 
			
		||||
 | 
			
		||||
	p.NumAttentionHeads = baseKV["llama.attention.head_count"].(uint32)
 | 
			
		||||
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *llamaAdapter) Tensors(ts []Tensor) []*ggml.Tensor {
 | 
			
		||||
	var out []*ggml.Tensor
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		shape := t.Shape()
 | 
			
		||||
		if (strings.HasSuffix(t.Name(), "weight.lora_a") && shape[0] > shape[1]) ||
 | 
			
		||||
			(strings.HasSuffix(t.Name(), "weight.lora_b") && shape[0] < shape[1]) {
 | 
			
		||||
			shape[0], shape[1] = shape[1], shape[0]
 | 
			
		||||
			t.SetRepacker(p.repackAndTranspose)
 | 
			
		||||
		} else {
 | 
			
		||||
			t.SetRepacker(p.repack)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		out = append(out, &ggml.Tensor{
 | 
			
		||||
			Name:     t.Name(),
 | 
			
		||||
			Kind:     t.Kind(),
 | 
			
		||||
			Shape:    shape,
 | 
			
		||||
			WriterTo: t,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *llamaAdapter) Replacements() []string {
 | 
			
		||||
	return []string{
 | 
			
		||||
		"base_model.model.", "",
 | 
			
		||||
		"model.layers", "blk",
 | 
			
		||||
		"self_attn.q_proj", "attn_q",
 | 
			
		||||
		"self_attn.k_proj", "attn_k",
 | 
			
		||||
		"self_attn.v_proj", "attn_v",
 | 
			
		||||
		"self_attn.o_proj", "attn_output",
 | 
			
		||||
		"mlp.gate_proj", "ffn_gate",
 | 
			
		||||
		"mlp.down_proj", "ffn_down",
 | 
			
		||||
		"mlp.up_proj", "ffn_up",
 | 
			
		||||
		"lora_A.weight", "weight.lora_a",
 | 
			
		||||
		"lora_B.weight", "weight.lora_b",
 | 
			
		||||
		"lora_a", "weight.lora_a",
 | 
			
		||||
		"lora_b", "weight.lora_b",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *llamaAdapter) repack(name string, data []float32, shape []uint64) ([]float32, error) {
 | 
			
		||||
	dims := []int{int(shape[1]), int(shape[0])}
 | 
			
		||||
 | 
			
		||||
	var heads uint32
 | 
			
		||||
	if strings.HasSuffix(name, "attn_q.weight.lora_a") {
 | 
			
		||||
		heads = p.NumAttentionHeads
 | 
			
		||||
	} else if strings.HasSuffix(name, "attn_k.weight.lora_a") {
 | 
			
		||||
		heads = cmp.Or(p.NumKeyValueHeads, p.NumAttentionHeads)
 | 
			
		||||
	} else {
 | 
			
		||||
		return data, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	n := tensor.New(tensor.WithShape(dims...), tensor.WithBacking(data))
 | 
			
		||||
 | 
			
		||||
	if err := n.Reshape(append([]int{int(heads), 2, dims[0] / int(heads) / 2}, dims[1:]...)...); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := n.T(0, 2, 1, 3); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := n.Reshape(dims...); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := n.Transpose(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ts, err := native.SelectF32(n, 1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var f32s []float32
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		f32s = append(f32s, t...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return f32s, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *llamaAdapter) repackAndTranspose(name string, data []float32, shape []uint64) ([]float32, error) {
 | 
			
		||||
	dims := []int{int(shape[1]), int(shape[0])}
 | 
			
		||||
 | 
			
		||||
	n := tensor.New(tensor.WithShape(dims...), tensor.WithBacking(data))
 | 
			
		||||
 | 
			
		||||
	var heads uint32
 | 
			
		||||
	if strings.HasSuffix(name, "attn_q.weight.lora_a") {
 | 
			
		||||
		heads = p.NumAttentionHeads
 | 
			
		||||
	} else if strings.HasSuffix(name, "attn_k.weight.lora_a") {
 | 
			
		||||
		heads = cmp.Or(p.NumKeyValueHeads, p.NumAttentionHeads)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if heads > 0 {
 | 
			
		||||
		if err := n.Reshape(append([]int{int(heads), 2, dims[0] / int(heads) / 2}, dims[1:]...)...); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := n.T(0, 2, 1, 3); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := n.Reshape(dims...); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := n.Transpose(); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := n.T(1, 0); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := n.Reshape(dims...); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := n.Transpose(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ts, err := native.SelectF32(n, 1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var f32s []float32
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		f32s = append(f32s, t...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return f32s, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										190
									
								
								convert/convert_mistral.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								convert/convert_mistral.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,190 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"cmp"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/pdevine/tensor"
 | 
			
		||||
	"github.com/pdevine/tensor/native"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/fs/ggml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type mistral3Model struct {
 | 
			
		||||
	ModelParameters
 | 
			
		||||
	ImageTokenIndex    uint32 `json:"image_token_index"`
 | 
			
		||||
	SpatialMergeSize   uint32 `json:"spatial_merge_size"`
 | 
			
		||||
	VisionFeatureLayer int32  `json:"vision_feature_layer"`
 | 
			
		||||
	TextModel          struct {
 | 
			
		||||
		NumHiddenLayers       uint32  `json:"num_hidden_layers"`
 | 
			
		||||
		MaxPositionEmbeddings uint32  `json:"max_position_embeddings"`
 | 
			
		||||
		HiddenSize            uint32  `json:"hidden_size"`
 | 
			
		||||
		IntermediateSize      uint32  `json:"intermediate_size"`
 | 
			
		||||
		NumAttentionHeads     uint32  `json:"num_attention_heads"`
 | 
			
		||||
		NumKeyValueHeads      uint32  `json:"num_key_value_heads"`
 | 
			
		||||
		RopeTheta             float32 `json:"rope_theta"`
 | 
			
		||||
		RMSNormEPS            float32 `json:"rms_norm_eps"`
 | 
			
		||||
		HeadDim               uint32  `json:"head_dim"`
 | 
			
		||||
		SlidingWindow         *uint32 `json:"sliding_window"`
 | 
			
		||||
		HiddenAct             string  `json:"hidden_act"`
 | 
			
		||||
		VocabSize             uint32  `json:"vocab_size"`
 | 
			
		||||
	} `json:"text_config"`
 | 
			
		||||
	VisionModel struct {
 | 
			
		||||
		NumAttentionHeads uint32  `json:"num_attention_heads"`
 | 
			
		||||
		NumHiddenLayers   uint32  `json:"num_hidden_layers"`
 | 
			
		||||
		HiddenSize        uint32  `json:"hidden_size"`
 | 
			
		||||
		IntermediateSize  uint32  `json:"intermediate_size"`
 | 
			
		||||
		ImageSize         uint32  `json:"image_size"`
 | 
			
		||||
		NumChannels       uint32  `json:"num_channels"`
 | 
			
		||||
		PatchSize         uint32  `json:"patch_size"`
 | 
			
		||||
		HeadDim           uint32  `json:"head_dim"`
 | 
			
		||||
		HiddenAct         string  `json:"hidden_act"`
 | 
			
		||||
		RopeTheta         float32 `json:"rope_theta"`
 | 
			
		||||
	} `json:"vision_config"`
 | 
			
		||||
	MultiModalProjectorBias bool   `json:"multimodal_projector_bias"`
 | 
			
		||||
	ProjectorHiddenAct      string `json:"projector_hidden_act"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *mistral3Model) KV(t *Tokenizer) ggml.KV {
 | 
			
		||||
	kv := p.ModelParameters.KV(t)
 | 
			
		||||
	kv["general.architecture"] = "mistral3"
 | 
			
		||||
	kv["mistral3.vocab_size"] = p.TextModel.VocabSize
 | 
			
		||||
 | 
			
		||||
	// Text configuration
 | 
			
		||||
	kv["mistral3.block_count"] = p.TextModel.NumHiddenLayers
 | 
			
		||||
	kv["mistral3.context_length"] = p.TextModel.MaxPositionEmbeddings
 | 
			
		||||
	kv["mistral3.embedding_length"] = p.TextModel.HiddenSize
 | 
			
		||||
	kv["mistral3.feed_forward_length"] = p.TextModel.IntermediateSize
 | 
			
		||||
	kv["mistral3.attention.head_count"] = p.TextModel.NumAttentionHeads
 | 
			
		||||
	kv["mistral3.attention.head_count_kv"] = p.TextModel.NumKeyValueHeads
 | 
			
		||||
	kv["mistral3.attention.layer_norm_rms_epsilon"] = p.TextModel.RMSNormEPS
 | 
			
		||||
	kv["mistral3.attention.key_length"] = p.TextModel.HeadDim
 | 
			
		||||
	kv["mistral3.attention.value_length"] = p.TextModel.HeadDim
 | 
			
		||||
	kv["mistral3.rope.dimension_count"] = p.TextModel.HiddenSize / p.TextModel.NumHiddenLayers
 | 
			
		||||
	kv["mistral3.rope.freq_base"] = p.TextModel.RopeTheta
 | 
			
		||||
 | 
			
		||||
	// Vision configuration
 | 
			
		||||
	kv["mistral3.vision.block_count"] = p.VisionModel.NumHiddenLayers
 | 
			
		||||
	kv["mistral3.vision.embedding_length"] = p.VisionModel.HiddenSize
 | 
			
		||||
	kv["mistral3.vision.feed_forward_length"] = p.VisionModel.IntermediateSize
 | 
			
		||||
	kv["mistral3.vision.attention.head_count"] = p.VisionModel.NumAttentionHeads
 | 
			
		||||
	kv["mistral3.vision.attention.key_length"] = p.VisionModel.HeadDim
 | 
			
		||||
	kv["mistral3.vision.image_size"] = p.VisionModel.ImageSize
 | 
			
		||||
	kv["mistral3.vision.patch_size"] = p.VisionModel.PatchSize
 | 
			
		||||
	kv["mistral3.vision.num_channels"] = p.VisionModel.NumChannels
 | 
			
		||||
	// kv["mistral3.vision.attention.layer_norm_epsilon"] = 1e-05 // Default value
 | 
			
		||||
	kv["mistral3.vision.rope.freq_base"] = p.VisionModel.RopeTheta
 | 
			
		||||
 | 
			
		||||
	// Multimodal configuration
 | 
			
		||||
	kv["mistral3.image_token_index"] = p.ImageTokenIndex
 | 
			
		||||
	kv["mistral3.spatial_merge_size"] = p.SpatialMergeSize
 | 
			
		||||
 | 
			
		||||
	kv["mistral3.mm.projector_bias"] = p.MultiModalProjectorBias
 | 
			
		||||
 | 
			
		||||
	if p.ProjectorHiddenAct != "" {
 | 
			
		||||
		kv["mistral3.mm.projector_hidden_act"] = p.ProjectorHiddenAct
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *mistral3Model) Tensors(ts []Tensor) []*ggml.Tensor {
 | 
			
		||||
	var out []*ggml.Tensor
 | 
			
		||||
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		if !strings.HasPrefix(t.Name(), "v.") {
 | 
			
		||||
			if strings.HasSuffix(t.Name(), ".attn_q.weight") ||
 | 
			
		||||
				strings.HasSuffix(t.Name(), ".attn_k.weight") {
 | 
			
		||||
				t.SetRepacker(p.repack)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		out = append(out, &ggml.Tensor{
 | 
			
		||||
			Name:     t.Name(),
 | 
			
		||||
			Kind:     t.Kind(),
 | 
			
		||||
			Shape:    t.Shape(),
 | 
			
		||||
			WriterTo: t,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *mistral3Model) Replacements() []string {
 | 
			
		||||
	return []string{
 | 
			
		||||
		"language_model.model.norm", "output_norm",
 | 
			
		||||
		"language_model.model.", "",
 | 
			
		||||
		"language_model.", "",
 | 
			
		||||
		"layers", "blk",
 | 
			
		||||
		"transformer.layers", "blk",
 | 
			
		||||
		"vision_tower", "v",
 | 
			
		||||
		"ln_pre", "encoder_norm",
 | 
			
		||||
		"input_layernorm", "attn_norm",
 | 
			
		||||
		"post_attention_layernorm", "ffn_norm",
 | 
			
		||||
		"embed_tokens", "token_embd",
 | 
			
		||||
		"self_attn.q_proj", "attn_q",
 | 
			
		||||
		"self_attn.k_proj", "attn_k",
 | 
			
		||||
		"self_attn.v_proj", "attn_v",
 | 
			
		||||
		"self_attn.o_proj", "attn_output",
 | 
			
		||||
		"mlp.down_proj", "ffn_down",
 | 
			
		||||
		"mlp.gate_proj", "ffn_gate",
 | 
			
		||||
		"mlp.up_proj", "ffn_up",
 | 
			
		||||
		"attention.q_proj", "attn_q",
 | 
			
		||||
		"attention.k_proj", "attn_k",
 | 
			
		||||
		"attention.v_proj", "attn_v",
 | 
			
		||||
		"attention.o_proj", "attn_output",
 | 
			
		||||
		"attention_norm", "attn_norm",
 | 
			
		||||
		"feed_forward.gate_proj", "ffn_gate",
 | 
			
		||||
		"feed_forward.down_proj", "ffn_down",
 | 
			
		||||
		"feed_forward.up_proj", "ffn_up",
 | 
			
		||||
		"multi_modal_projector", "mm",
 | 
			
		||||
		"ffn_norm", "ffn_norm",
 | 
			
		||||
		"lm_head", "output",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *mistral3Model) repack(name string, data []float32, shape []uint64) ([]float32, error) {
 | 
			
		||||
	var dims []int
 | 
			
		||||
	for _, dim := range shape {
 | 
			
		||||
		dims = append(dims, int(dim))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var heads uint32
 | 
			
		||||
	if strings.HasSuffix(name, ".attn_q.weight") {
 | 
			
		||||
		heads = p.TextModel.NumAttentionHeads
 | 
			
		||||
	} else if strings.HasSuffix(name, ".attn_k.weight") {
 | 
			
		||||
		heads = cmp.Or(p.TextModel.NumKeyValueHeads, p.TextModel.NumAttentionHeads)
 | 
			
		||||
	} else {
 | 
			
		||||
		return nil, fmt.Errorf("unknown tensor for repack: %s", name)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	n := tensor.New(tensor.WithShape(dims...), tensor.WithBacking(data))
 | 
			
		||||
	if err := n.Reshape(append([]int{int(heads), 2, dims[0] / int(heads) / 2}, dims[1:]...)...); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := n.T(0, 2, 1, 3); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := n.Reshape(dims...); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := n.Transpose(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ts, err := native.SelectF32(n, 1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var f32s []float32
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		f32s = append(f32s, t...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return f32s, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										94
									
								
								convert/convert_mixtral.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								convert/convert_mixtral.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/fs/ggml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type mixtralModel struct {
 | 
			
		||||
	llamaModel
 | 
			
		||||
	NumLocalExperts    uint32 `json:"num_local_experts"`
 | 
			
		||||
	NumExpertsPerToken uint32 `json:"num_experts_per_tok"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *mixtralModel) KV(t *Tokenizer) ggml.KV {
 | 
			
		||||
	kv := p.llamaModel.KV(t)
 | 
			
		||||
 | 
			
		||||
	if p.NumLocalExperts > 0 {
 | 
			
		||||
		kv["llama.expert_count"] = p.NumLocalExperts
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.NumExpertsPerToken > 0 {
 | 
			
		||||
		kv["llama.expert_used_count"] = p.NumExpertsPerToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *mixtralModel) Tensors(ts []Tensor) []*ggml.Tensor {
 | 
			
		||||
	oldnew := []string{
 | 
			
		||||
		"model.layers", "blk",
 | 
			
		||||
		"w1", "ffn_gate_exps",
 | 
			
		||||
		"w2", "ffn_down_exps",
 | 
			
		||||
		"w3", "ffn_up_exps",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i := range p.NumLocalExperts {
 | 
			
		||||
		oldnew = append(oldnew, fmt.Sprintf(".block_sparse_moe.experts.%d.", i), ".")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// group experts of the same layer (model.layers.%d) and type (w[123]) into a single tensor
 | 
			
		||||
	namer := strings.NewReplacer(oldnew...)
 | 
			
		||||
	experts := make(map[string]experts)
 | 
			
		||||
 | 
			
		||||
	// merge experts into a single tensor while removing them from ts
 | 
			
		||||
	ts = slices.DeleteFunc(ts, func(t Tensor) bool {
 | 
			
		||||
		if !strings.Contains(t.Name(), ".block_sparse_moe.experts.") {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		name := namer.Replace(t.Name())
 | 
			
		||||
		experts[name] = append(experts[name], t)
 | 
			
		||||
		return true
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	var out []*ggml.Tensor
 | 
			
		||||
	for n, e := range experts {
 | 
			
		||||
		// TODO(mxyng): sanity check experts
 | 
			
		||||
		out = append(out, &ggml.Tensor{
 | 
			
		||||
			Name:     n,
 | 
			
		||||
			Kind:     e[0].Kind(),
 | 
			
		||||
			Shape:    append([]uint64{uint64(len(e))}, e[0].Shape()...),
 | 
			
		||||
			WriterTo: e,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return append(out, p.llamaModel.Tensors(ts)...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *mixtralModel) Replacements() []string {
 | 
			
		||||
	return append(
 | 
			
		||||
		p.llamaModel.Replacements(),
 | 
			
		||||
		"block_sparse_moe.gate", "ffn_gate_inp",
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type experts []Tensor
 | 
			
		||||
 | 
			
		||||
func (e experts) WriteTo(w io.Writer) (int64, error) {
 | 
			
		||||
	// TODO(mxyng): experts _should_ be numerically sorted by expert but this should check
 | 
			
		||||
	for _, t := range e {
 | 
			
		||||
		// the canonical merged experts tensor stacks all experts along a new, 0 axis,
 | 
			
		||||
		// e.g. `tensor.Stack(0, e[0], e[1:]...)`, which requires allocating temporary buffers
 | 
			
		||||
		// this accomplishes the same thing by writing each expert tensor in sequence
 | 
			
		||||
		if _, err := t.WriteTo(w); err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return 0, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										160
									
								
								convert/convert_mllama.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								convert/convert_mllama.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,160 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/fs/ggml"
 | 
			
		||||
	"github.com/pdevine/tensor"
 | 
			
		||||
	"github.com/pdevine/tensor/native"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type mllamaModel struct {
 | 
			
		||||
	ModelParameters
 | 
			
		||||
	TextModel struct {
 | 
			
		||||
		llamaModel
 | 
			
		||||
 | 
			
		||||
		CrossAttentionLayers []int32 `json:"cross_attention_layers"`
 | 
			
		||||
	} `json:"text_config"`
 | 
			
		||||
	VisionModel struct {
 | 
			
		||||
		NumHiddenLayers           uint32  `json:"num_hidden_layers"`
 | 
			
		||||
		NumGlobalLayers           uint32  `json:"num_global_layers"`
 | 
			
		||||
		IntermediateLayersIndices []int32 `json:"intermediate_layers_indices"`
 | 
			
		||||
 | 
			
		||||
		HiddenSize       uint32 `json:"hidden_size"`
 | 
			
		||||
		IntermediateSize uint32 `json:"intermediate_size"`
 | 
			
		||||
 | 
			
		||||
		AttentionHeads uint32 `json:"attention_heads"`
 | 
			
		||||
 | 
			
		||||
		ImageSize   uint32  `json:"image_size"`
 | 
			
		||||
		PatchSize   uint32  `json:"patch_size"`
 | 
			
		||||
		NumChannels uint32  `json:"num_channels"`
 | 
			
		||||
		MaxNumTiles uint32  `json:"max_num_tiles"`
 | 
			
		||||
		NormEpsilon float32 `json:"norm_eps"`
 | 
			
		||||
		RopeTheta   float32 `json:"rope.freq_base"`
 | 
			
		||||
	} `json:"vision_config"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *mllamaModel) KV(t *Tokenizer) ggml.KV {
 | 
			
		||||
	kv := m.ModelParameters.KV(t)
 | 
			
		||||
	kv["general.architecture"] = "mllama"
 | 
			
		||||
 | 
			
		||||
	for k, v := range m.TextModel.KV(t) {
 | 
			
		||||
		if strings.HasPrefix(k, "llama.") {
 | 
			
		||||
			kv[strings.ReplaceAll(k, "llama.", "mllama.")] = v
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	kv["mllama.attention.cross_attention_layers"] = m.TextModel.CrossAttentionLayers
 | 
			
		||||
 | 
			
		||||
	kv["mllama.vision.block_count"] = m.VisionModel.NumHiddenLayers
 | 
			
		||||
	kv["mllama.vision.global.block_count"] = m.VisionModel.NumGlobalLayers
 | 
			
		||||
	kv["mllama.vision.intermediate_layers_indices"] = m.VisionModel.IntermediateLayersIndices
 | 
			
		||||
 | 
			
		||||
	kv["mllama.vision.embedding_length"] = m.VisionModel.HiddenSize
 | 
			
		||||
	kv["mllama.vision.feed_forward_length"] = m.VisionModel.IntermediateSize
 | 
			
		||||
 | 
			
		||||
	kv["mllama.vision.attention.head_count"] = m.VisionModel.AttentionHeads
 | 
			
		||||
	kv["mllama.vision.attention.layer_norm_epsilon"] = m.VisionModel.NormEpsilon
 | 
			
		||||
 | 
			
		||||
	kv["mllama.vision.image_size"] = m.VisionModel.ImageSize
 | 
			
		||||
	kv["mllama.vision.patch_size"] = m.VisionModel.PatchSize
 | 
			
		||||
	kv["mllama.vision.max_num_tiles"] = m.VisionModel.MaxNumTiles
 | 
			
		||||
	kv["mllama.vision.num_channels"] = m.VisionModel.NumChannels
 | 
			
		||||
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *mllamaModel) Replacements() []string {
 | 
			
		||||
	return append(
 | 
			
		||||
		m.TextModel.Replacements(),
 | 
			
		||||
		"language_model.", "",
 | 
			
		||||
		"gate_attn", "attn_gate",
 | 
			
		||||
		"gate_ffn", "ffn_gate",
 | 
			
		||||
		"cross_attn.", "cross_attn_",
 | 
			
		||||
		"vision_model", "v",
 | 
			
		||||
		"class_embedding", "class_embd",
 | 
			
		||||
		"patch_embedding", "patch_embd",
 | 
			
		||||
		"gated_positional_embedding.tile_embedding", "tile_position_embd",
 | 
			
		||||
		"gated_positional_embedding.embedding", "position_embd.weight",
 | 
			
		||||
		"gated_positional_embedding", "position_embd",
 | 
			
		||||
		"embedding.weight", "weight",
 | 
			
		||||
		"pre_tile_positional_embedding", "pre_tile_position_embd",
 | 
			
		||||
		"post_tile_positional_embedding", "post_tile_position_embd",
 | 
			
		||||
		"layernorm_pre", "pre_ln",
 | 
			
		||||
		"layernorm_post", "post_ln",
 | 
			
		||||
		"global_transformer.layers", "global.blk",
 | 
			
		||||
		"transformer.layers", "blk",
 | 
			
		||||
		"mlp.fc1", "ffn_up",
 | 
			
		||||
		"mlp.fc2", "ffn_down",
 | 
			
		||||
		"multi_modal_projector", "mm.0",
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *mllamaModel) Tensors(ts []Tensor) []*ggml.Tensor {
 | 
			
		||||
	var out []*ggml.Tensor
 | 
			
		||||
	var text []Tensor
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		if t.Name() == "v.position_embd.gate" {
 | 
			
		||||
			for _, name := range []string{"v.position_embd.gate", "v.tile_position_embd.gate"} {
 | 
			
		||||
				tt := t.Clone()
 | 
			
		||||
				tt.SetRepacker(m.repack(name))
 | 
			
		||||
				out = append(out, &ggml.Tensor{
 | 
			
		||||
					Name:     name,
 | 
			
		||||
					Kind:     t.Kind(),
 | 
			
		||||
					Shape:    t.Shape(),
 | 
			
		||||
					WriterTo: tt,
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		} else if t.Name() == "v.pre_tile_position_embd.gate" || t.Name() == "v.post_tile_position_embd.gate" {
 | 
			
		||||
			t.SetRepacker(m.repack(t.Name()))
 | 
			
		||||
			out = append(out, &ggml.Tensor{
 | 
			
		||||
				Name:     t.Name(),
 | 
			
		||||
				Kind:     t.Kind(),
 | 
			
		||||
				Shape:    t.Shape(),
 | 
			
		||||
				WriterTo: t,
 | 
			
		||||
			})
 | 
			
		||||
		} else if strings.HasPrefix(t.Name(), "v.") || strings.HasPrefix(t.Name(), "mm.") {
 | 
			
		||||
			out = append(out, &ggml.Tensor{
 | 
			
		||||
				Name:     t.Name(),
 | 
			
		||||
				Kind:     t.Kind(),
 | 
			
		||||
				Shape:    t.Shape(),
 | 
			
		||||
				WriterTo: t,
 | 
			
		||||
			})
 | 
			
		||||
		} else {
 | 
			
		||||
			text = append(text, t)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return append(out, m.TextModel.Tensors(text)...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *mllamaModel) repack(name string) Repacker {
 | 
			
		||||
	return func(_ string, data []float32, shape []uint64) (_ []float32, err error) {
 | 
			
		||||
		dims := make([]int, len(shape))
 | 
			
		||||
		for i, dim := range shape {
 | 
			
		||||
			dims[i] = int(dim)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var t tensor.Tensor = tensor.New(tensor.WithShape(dims...), tensor.WithBacking(data))
 | 
			
		||||
 | 
			
		||||
		t, err = tensor.Tanh(t)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if name == "v.position_embd.gate" {
 | 
			
		||||
			t, err = tensor.Sub(float32(1), t)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		t = tensor.Materialize(t)
 | 
			
		||||
		// flatten tensor so it can be return as a vector
 | 
			
		||||
		if err := t.Reshape(t.Shape().TotalSize()); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return native.VectorF32(t.(*tensor.Dense))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										122
									
								
								convert/convert_phi3.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								convert/convert_phi3.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,122 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"cmp"
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
	"io"
 | 
			
		||||
	"math"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/fs/ggml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type phi3Model struct {
 | 
			
		||||
	ModelParameters
 | 
			
		||||
	NumHiddenLayers   uint32  `json:"num_hidden_layers"`
 | 
			
		||||
	NLayers           uint32  `json:"n_layers"`
 | 
			
		||||
	HiddenSize        uint32  `json:"hidden_size"`
 | 
			
		||||
	NEmbd             uint32  `json:"n_embd"`
 | 
			
		||||
	IntermediateSize  uint32  `json:"intermediate_size"`
 | 
			
		||||
	NumAttentionHeads uint32  `json:"num_attention_heads"`
 | 
			
		||||
	NHead             uint32  `json:"n_head"`
 | 
			
		||||
	NumKeyValueHeads  uint32  `json:"num_key_value_heads"`
 | 
			
		||||
	NHeadKV           uint32  `json:"n_head_kv"`
 | 
			
		||||
	RopeTheta         float32 `json:"rope_theta"`
 | 
			
		||||
	RopeScaling       struct {
 | 
			
		||||
		Type        string     `json:"type"`
 | 
			
		||||
		LongFactor  ropeFactor `json:"long_factor"`
 | 
			
		||||
		ShortFactor ropeFactor `json:"short_factor"`
 | 
			
		||||
	} `json:"rope_scaling"`
 | 
			
		||||
	RMSNormEPS                    float32 `json:"rms_norm_eps"`
 | 
			
		||||
	NPositions                    uint32  `json:"n_positions"`
 | 
			
		||||
	MaxPositionEmbeddings         uint32  `json:"max_position_embeddings"`
 | 
			
		||||
	OriginalMaxPositionEmbeddings uint32  `json:"original_max_position_embeddings"`
 | 
			
		||||
	SlidingWindow                 uint32  `json:"sliding_window"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ ModelConverter = (*phi3Model)(nil)
 | 
			
		||||
 | 
			
		||||
func (p *phi3Model) KV(t *Tokenizer) ggml.KV {
 | 
			
		||||
	kv := p.ModelParameters.KV(t)
 | 
			
		||||
	kv["general.architecture"] = "phi3"
 | 
			
		||||
	kv["phi3.context_length"] = p.MaxPositionEmbeddings
 | 
			
		||||
	kv["phi3.embedding_length"] = cmp.Or(p.HiddenSize, p.NEmbd)
 | 
			
		||||
	kv["phi3.feed_forward_length"] = p.IntermediateSize
 | 
			
		||||
	kv["phi3.block_count"] = cmp.Or(p.NumHiddenLayers, p.NLayers)
 | 
			
		||||
	kv["phi3.attention.head_count"] = cmp.Or(p.NumAttentionHeads, p.NHead)
 | 
			
		||||
	kv["phi3.attention.head_count_kv"] = cmp.Or(p.NumKeyValueHeads, p.NHeadKV)
 | 
			
		||||
	kv["phi3.attention.layer_norm_rms_epsilon"] = p.RMSNormEPS
 | 
			
		||||
	kv["phi3.rope.dimension_count"] = p.HiddenSize / cmp.Or(p.NumAttentionHeads, p.NHead)
 | 
			
		||||
	kv["phi3.rope.freq_base"] = p.RopeTheta
 | 
			
		||||
	kv["phi3.rope.scaling.original_context_length"] = p.OriginalMaxPositionEmbeddings
 | 
			
		||||
	kv["phi3.attention.sliding_window"] = p.SlidingWindow
 | 
			
		||||
 | 
			
		||||
	scale := float64(p.MaxPositionEmbeddings) / float64(p.OriginalMaxPositionEmbeddings)
 | 
			
		||||
 | 
			
		||||
	switch p.RopeScaling.Type {
 | 
			
		||||
	case "":
 | 
			
		||||
		// no scaling
 | 
			
		||||
	case "su", "longrope":
 | 
			
		||||
		kv["phi3.rope.scaling.attn_factor"] = float32(max(math.Sqrt(1+math.Log(scale)/math.Log(float64(p.OriginalMaxPositionEmbeddings))), 1.0))
 | 
			
		||||
	case "yarn":
 | 
			
		||||
		kv["phi3.rope.scaling.attn_factor"] = float32(max(0.1*math.Log(scale)+1.0, 1.0))
 | 
			
		||||
	default:
 | 
			
		||||
		panic("unknown rope scaling type")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *phi3Model) Tensors(ts []Tensor) []*ggml.Tensor {
 | 
			
		||||
	var addRopeFactors sync.Once
 | 
			
		||||
 | 
			
		||||
	out := make([]*ggml.Tensor, 0, len(ts)+2)
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		if strings.HasPrefix(t.Name(), "blk.0.") {
 | 
			
		||||
			addRopeFactors.Do(func() {
 | 
			
		||||
				out = append(out, &ggml.Tensor{
 | 
			
		||||
					Name:     "rope_factors_long.weight",
 | 
			
		||||
					Kind:     0,
 | 
			
		||||
					Shape:    []uint64{uint64(len(p.RopeScaling.LongFactor))},
 | 
			
		||||
					WriterTo: p.RopeScaling.LongFactor,
 | 
			
		||||
				}, &ggml.Tensor{
 | 
			
		||||
					Name:     "rope_factors_short.weight",
 | 
			
		||||
					Kind:     0,
 | 
			
		||||
					Shape:    []uint64{uint64(len(p.RopeScaling.ShortFactor))},
 | 
			
		||||
					WriterTo: p.RopeScaling.ShortFactor,
 | 
			
		||||
				})
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		out = append(out, &ggml.Tensor{
 | 
			
		||||
			Name:     t.Name(),
 | 
			
		||||
			Kind:     t.Kind(),
 | 
			
		||||
			Shape:    t.Shape(),
 | 
			
		||||
			WriterTo: t,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *phi3Model) Replacements() []string {
 | 
			
		||||
	return []string{
 | 
			
		||||
		"lm_head", "output",
 | 
			
		||||
		"model.embed_tokens", "token_embd",
 | 
			
		||||
		"model.norm", "output_norm",
 | 
			
		||||
		"model.layers", "blk",
 | 
			
		||||
		"input_layernorm", "attn_norm",
 | 
			
		||||
		"self_attn.qkv_proj", "attn_qkv",
 | 
			
		||||
		"self_attn.o_proj", "attn_output",
 | 
			
		||||
		"mlp.down_proj", "ffn_down",
 | 
			
		||||
		"mlp.gate_up_proj", "ffn_up",
 | 
			
		||||
		"post_attention_layernorm", "ffn_norm",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ropeFactor []float32
 | 
			
		||||
 | 
			
		||||
func (r ropeFactor) WriteTo(w io.Writer) (int64, error) {
 | 
			
		||||
	return 0, binary.Write(w, binary.LittleEndian, r)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										78
									
								
								convert/convert_qwen2.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								convert/convert_qwen2.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import "github.com/ollama/ollama/fs/ggml"
 | 
			
		||||
 | 
			
		||||
type qwen2Model struct {
 | 
			
		||||
	ModelParameters
 | 
			
		||||
	MaxPositionEmbeddings uint32  `json:"max_position_embeddings"`
 | 
			
		||||
	HiddenSize            uint32  `json:"hidden_size"`
 | 
			
		||||
	HiddenLayers          uint32  `json:"num_hidden_layers"`
 | 
			
		||||
	IntermediateSize      uint32  `json:"intermediate_size"`
 | 
			
		||||
	NumAttentionHeads     uint32  `json:"num_attention_heads"`
 | 
			
		||||
	NumKeyValueHeads      uint32  `json:"num_key_value_heads"`
 | 
			
		||||
	RopeTheta             float32 `json:"rope_theta"`
 | 
			
		||||
	RopeScaling           struct {
 | 
			
		||||
		Type                          string     `json:"type"`
 | 
			
		||||
		Factor                        ropeFactor `json:"factor"`
 | 
			
		||||
		OriginalMaxPositionEmbeddings uint32     `json:"original_max_position_embeddings"`
 | 
			
		||||
	} `json:"rope_scaling"`
 | 
			
		||||
	RMSNormEPS float32 `json:"rms_norm_eps"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ ModelConverter = (*qwen2Model)(nil)
 | 
			
		||||
 | 
			
		||||
func (q *qwen2Model) KV(t *Tokenizer) ggml.KV {
 | 
			
		||||
	kv := q.ModelParameters.KV(t)
 | 
			
		||||
	kv["general.architecture"] = "qwen2"
 | 
			
		||||
	kv["qwen2.block_count"] = q.HiddenLayers
 | 
			
		||||
	kv["qwen2.context_length"] = q.MaxPositionEmbeddings
 | 
			
		||||
	kv["qwen2.embedding_length"] = q.HiddenSize
 | 
			
		||||
	kv["qwen2.feed_forward_length"] = q.IntermediateSize
 | 
			
		||||
	kv["qwen2.attention.head_count"] = q.NumAttentionHeads
 | 
			
		||||
	kv["qwen2.attention.head_count_kv"] = q.NumKeyValueHeads
 | 
			
		||||
	kv["qwen2.rope.freq_base"] = q.RopeTheta
 | 
			
		||||
	kv["qwen2.attention.layer_norm_rms_epsilon"] = q.RMSNormEPS
 | 
			
		||||
 | 
			
		||||
	switch q.RopeScaling.Type {
 | 
			
		||||
	case "":
 | 
			
		||||
		// no scaling
 | 
			
		||||
	case "yarn":
 | 
			
		||||
		kv["qwen2.rope.scaling.type"] = q.RopeScaling.Type
 | 
			
		||||
		kv["qwen2.rope.scaling.factor"] = q.RopeScaling.Factor
 | 
			
		||||
	default:
 | 
			
		||||
		panic("unknown rope scaling type")
 | 
			
		||||
	}
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (q *qwen2Model) Tensors(ts []Tensor) []*ggml.Tensor {
 | 
			
		||||
	var out []*ggml.Tensor
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		out = append(out, &ggml.Tensor{
 | 
			
		||||
			Name:     t.Name(),
 | 
			
		||||
			Kind:     t.Kind(),
 | 
			
		||||
			Shape:    t.Shape(),
 | 
			
		||||
			WriterTo: t,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *qwen2Model) Replacements() []string {
 | 
			
		||||
	return []string{
 | 
			
		||||
		"lm_head", "output",
 | 
			
		||||
		"model.embed_tokens", "token_embd",
 | 
			
		||||
		"model.layers", "blk",
 | 
			
		||||
		"input_layernorm", "attn_norm",
 | 
			
		||||
		"self_attn.k_proj", "attn_k",
 | 
			
		||||
		"self_attn.v_proj", "attn_v",
 | 
			
		||||
		"self_attn.q_proj", "attn_q",
 | 
			
		||||
		"self_attn.o_proj", "attn_output",
 | 
			
		||||
		"mlp.down_proj", "ffn_down",
 | 
			
		||||
		"mlp.gate_proj", "ffn_gate",
 | 
			
		||||
		"mlp.up_proj", "ffn_up",
 | 
			
		||||
		"post_attention_layernorm", "ffn_norm",
 | 
			
		||||
		"model.norm", "output_norm",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										478
									
								
								convert/convert_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										478
									
								
								convert/convert_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,478 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/exp/maps"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/fs/ggml"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type tensorData struct {
 | 
			
		||||
	Offsets []int  `json:"data_offsets"`
 | 
			
		||||
	Type    string `json:"dtype"`
 | 
			
		||||
	Shape   []int  `json:"shape"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func convertFull(t *testing.T, fsys fs.FS) (*os.File, ggml.KV, ggml.Tensors) {
 | 
			
		||||
	t.Helper()
 | 
			
		||||
 | 
			
		||||
	f, err := os.CreateTemp(t.TempDir(), "f16")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer f.Close()
 | 
			
		||||
 | 
			
		||||
	if err := ConvertModel(fsys, f); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, err := os.Open(f.Name())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	t.Cleanup(func() { r.Close() })
 | 
			
		||||
 | 
			
		||||
	m, _, err := ggml.Decode(r, -1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, err := r.Seek(0, io.SeekStart); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return r, m.KV(), m.Tensors()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func generateResultsJSON(t *testing.T, f *os.File, kv ggml.KV, tensors ggml.Tensors) map[string]string {
 | 
			
		||||
	actual := make(map[string]string)
 | 
			
		||||
	for k, v := range kv {
 | 
			
		||||
		if s, ok := v.(json.Marshaler); !ok {
 | 
			
		||||
			actual[k] = fmt.Sprintf("%v", v)
 | 
			
		||||
		} else {
 | 
			
		||||
			bts, err := json.Marshal(s)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			actual[k] = fmt.Sprintf("%x", sha256.Sum256(bts))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tensor := range tensors.Items() {
 | 
			
		||||
		sha256sum := sha256.New()
 | 
			
		||||
		sr := io.NewSectionReader(f, int64(tensors.Offset+tensor.Offset), int64(tensor.Size()))
 | 
			
		||||
		if _, err := io.Copy(sha256sum, sr); err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		actual[tensor.Name] = hex.EncodeToString(sha256sum.Sum(nil))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return actual
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMain(m *testing.M) {
 | 
			
		||||
	var level slog.Level
 | 
			
		||||
	flag.TextVar(&level, "level", slog.LevelInfo, "log level")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
	slog.SetLogLoggerLevel(level)
 | 
			
		||||
	os.Exit(m.Run())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestConvertModel(t *testing.T) {
 | 
			
		||||
	cases := []string{
 | 
			
		||||
		"Meta-Llama-3-8B-Instruct",
 | 
			
		||||
		"Meta-Llama-3.1-8B-Instruct",
 | 
			
		||||
		"Mistral-7B-Instruct-v0.2",
 | 
			
		||||
		"Mixtral-8x7B-Instruct-v0.1",
 | 
			
		||||
		"gemma-2b-it",
 | 
			
		||||
		"gemma-2-2b-it",
 | 
			
		||||
		// microsoft/Phi-3-mini-128-instruct@d548c233192db00165d842bf8edff054bb3212f8
 | 
			
		||||
		"Phi-3-mini-128k-instruct",
 | 
			
		||||
		"all-MiniLM-L6-v2",
 | 
			
		||||
		"gemma-2-9b-it",
 | 
			
		||||
		"Qwen2.5-0.5B-Instruct",
 | 
			
		||||
		"c4ai-command-r-v01",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i := range cases {
 | 
			
		||||
		tt := cases[i]
 | 
			
		||||
		t.Run(tt, func(t *testing.T) {
 | 
			
		||||
			t.Parallel()
 | 
			
		||||
 | 
			
		||||
			p := filepath.Join("testdata", tt)
 | 
			
		||||
			if testing.Short() {
 | 
			
		||||
				t.Skip("skipping in short mode")
 | 
			
		||||
			} else if _, err := os.Stat(p); err != nil {
 | 
			
		||||
				t.Skipf("%s not found", p)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			f, kv, tensors := convertFull(t, os.DirFS(p))
 | 
			
		||||
			actual := generateResultsJSON(t, f, kv, tensors)
 | 
			
		||||
 | 
			
		||||
			expectFile, err := os.Open(filepath.Join("testdata", fmt.Sprintf("%s.json", tt)))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
			defer expectFile.Close()
 | 
			
		||||
 | 
			
		||||
			var expect map[string]string
 | 
			
		||||
			if err := json.NewDecoder(expectFile).Decode(&expect); err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			keys := maps.Keys(expect)
 | 
			
		||||
			slices.Sort(keys)
 | 
			
		||||
			for _, k := range keys {
 | 
			
		||||
				if v, ok := actual[k]; !ok {
 | 
			
		||||
					t.Errorf("missing %s", k)
 | 
			
		||||
				} else if v != expect[k] {
 | 
			
		||||
					t.Errorf("unexpected %s: want %s, got %s", k, expect[k], v)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestConvertInvalidTensorNames(t *testing.T) {
 | 
			
		||||
	f, err := os.CreateTemp(t.TempDir(), "testmodel")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer f.Close()
 | 
			
		||||
 | 
			
		||||
	tempDir := t.TempDir()
 | 
			
		||||
 | 
			
		||||
	td := map[string]*tensorData{}
 | 
			
		||||
	offset := 4096
 | 
			
		||||
 | 
			
		||||
	td["model.layers.0.self_attn.q_proj.weight"] = &tensorData{
 | 
			
		||||
		Offsets: []int{0, offset},
 | 
			
		||||
		Type:    "F32",
 | 
			
		||||
		Shape:   []int{4096, 4096},
 | 
			
		||||
	}
 | 
			
		||||
	td["blk.0.attn_q.weight"] = &tensorData{
 | 
			
		||||
		Offsets: []int{offset, offset * 2},
 | 
			
		||||
		Type:    "F32",
 | 
			
		||||
		Shape:   []int{4096, 4096},
 | 
			
		||||
	}
 | 
			
		||||
	generateSafetensorTestData(t, tempDir, td)
 | 
			
		||||
 | 
			
		||||
	err = ConvertModel(os.DirFS(tempDir), f)
 | 
			
		||||
	if err == nil || !strings.HasPrefix(err.Error(), "duplicate tensor name") {
 | 
			
		||||
		t.Errorf("expected error but didn't get one")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestConvertInvalidDatatype(t *testing.T) {
 | 
			
		||||
	f, err := os.CreateTemp(t.TempDir(), "testmodel")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer f.Close()
 | 
			
		||||
 | 
			
		||||
	tempDir := t.TempDir()
 | 
			
		||||
 | 
			
		||||
	td := map[string]*tensorData{}
 | 
			
		||||
	offset := 4096 * 14336
 | 
			
		||||
 | 
			
		||||
	td["model.layers.0.mlp.down_proj.weight"] = &tensorData{
 | 
			
		||||
		Offsets: []int{0, offset},
 | 
			
		||||
		Type:    "I8",
 | 
			
		||||
		Shape:   []int{4096, 14336},
 | 
			
		||||
	}
 | 
			
		||||
	td["model.layers.0.mlp.down_proj.weight_format"] = &tensorData{
 | 
			
		||||
		Offsets: []int{offset, offset},
 | 
			
		||||
		Type:    "U8",
 | 
			
		||||
		Shape:   []int{},
 | 
			
		||||
	}
 | 
			
		||||
	generateSafetensorTestData(t, tempDir, td)
 | 
			
		||||
 | 
			
		||||
	err = ConvertModel(os.DirFS(tempDir), f)
 | 
			
		||||
	if err == nil || err.Error() != "unsupported safetensors model" {
 | 
			
		||||
		t.Errorf("expected error but didn't get one")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func generateSafetensorTestData(t *testing.T, tempDir string, tensorData map[string]*tensorData) {
 | 
			
		||||
	data, err := json.Marshal(tensorData)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var buf bytes.Buffer
 | 
			
		||||
 | 
			
		||||
	l := int64(len(data))
 | 
			
		||||
	err = binary.Write(&buf, binary.LittleEndian, l)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = buf.Write(data)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fdata, err := os.Create(filepath.Join(tempDir, "model-00001-of-00001.safetensors"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer fdata.Close()
 | 
			
		||||
 | 
			
		||||
	_, err = fdata.Write(buf.Bytes())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	configData := `
 | 
			
		||||
{
 | 
			
		||||
  "architectures": [
 | 
			
		||||
    "LlamaForCausalLM"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
	f, err := os.Create(filepath.Join(tempDir, "config.json"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer f.Close()
 | 
			
		||||
 | 
			
		||||
	_, err = f.WriteString(configData)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tokenizerData := `
 | 
			
		||||
{
 | 
			
		||||
}
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
	f, err = os.Create(filepath.Join(tempDir, "tokenizer.json"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer f.Close()
 | 
			
		||||
 | 
			
		||||
	_, err = f.WriteString(tokenizerData)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestConvertAdapter(t *testing.T) {
 | 
			
		||||
	type AdapterCase struct {
 | 
			
		||||
		Name     string
 | 
			
		||||
		BaseKV   map[string]any
 | 
			
		||||
		Expected map[string]string
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cases := []AdapterCase{
 | 
			
		||||
		{
 | 
			
		||||
			Name: "discollama",
 | 
			
		||||
			BaseKV: map[string]any{
 | 
			
		||||
				"general.architecture":          "llama",
 | 
			
		||||
				"llama.attention.head_count":    uint32(32),
 | 
			
		||||
				"llama.attention.head_count_kv": uint32(8),
 | 
			
		||||
			},
 | 
			
		||||
			Expected: map[string]string{
 | 
			
		||||
				"general.architecture":          "llama",
 | 
			
		||||
				"general.file_type":             "1",
 | 
			
		||||
				"general.parameter_count":       "106496",
 | 
			
		||||
				"general.type":                  "adapter",
 | 
			
		||||
				"general.version":               "v0.2",
 | 
			
		||||
				"adapter.lora.alpha":            "16",
 | 
			
		||||
				"adapter.type":                  "lora",
 | 
			
		||||
				"llama.attention.head_count":    "32",
 | 
			
		||||
				"llama.attention.head_count_kv": "8",
 | 
			
		||||
				"blk.31.attn_q.weight.lora_a":   "0eb3318b02cd313429bcc7621b539fdbb10240fea190c56c9e5f93fcd37a4e50",
 | 
			
		||||
				"blk.31.attn_q.weight.lora_b":   "0eb3318b02cd313429bcc7621b539fdbb10240fea190c56c9e5f93fcd37a4e50",
 | 
			
		||||
				"blk.31.attn_v.weight.lora_a":   "0eb3318b02cd313429bcc7621b539fdbb10240fea190c56c9e5f93fcd37a4e50",
 | 
			
		||||
				"blk.31.attn_v.weight.lora_b":   "071dcafe89df065d6e1c935ecb8fdf6479b3c202eb912e7da938597673ff5857",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, c := range cases {
 | 
			
		||||
		t.Run(c.Name, func(t *testing.T) {
 | 
			
		||||
			t.Parallel()
 | 
			
		||||
 | 
			
		||||
			f, err := os.CreateTemp(t.TempDir(), "f16")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
			defer f.Close()
 | 
			
		||||
 | 
			
		||||
			tempDir := t.TempDir()
 | 
			
		||||
			generateLoraTestData(t, tempDir)
 | 
			
		||||
 | 
			
		||||
			if err = ConvertAdapter(os.DirFS(tempDir), f, c.BaseKV); err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			r, err := os.Open(f.Name())
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
			defer r.Close()
 | 
			
		||||
 | 
			
		||||
			m, _, err := ggml.Decode(r, -1)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if _, err := r.Seek(0, io.SeekStart); err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			actual := generateResultsJSON(t, r, m.KV(), m.Tensors())
 | 
			
		||||
 | 
			
		||||
			keys := maps.Keys(c.Expected)
 | 
			
		||||
			slices.Sort(keys)
 | 
			
		||||
			for _, k := range keys {
 | 
			
		||||
				if v, ok := actual[k]; !ok {
 | 
			
		||||
					t.Errorf("missing %s", k)
 | 
			
		||||
				} else if v != c.Expected[k] {
 | 
			
		||||
					t.Errorf("unexpected %s: want %s, got %s", k, c.Expected[k], v)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func generateLoraTestData(t *testing.T, tempDir string) {
 | 
			
		||||
	offset := 4096 * 8 * 4
 | 
			
		||||
 | 
			
		||||
	td := map[string]*tensorData{"__metadata__": nil}
 | 
			
		||||
	td["model.layers.31.self_attn.q_proj.lora_a"] = &tensorData{
 | 
			
		||||
		Offsets: []int{0, offset},
 | 
			
		||||
		Type:    "F32",
 | 
			
		||||
		Shape:   []int{4096, 8},
 | 
			
		||||
	}
 | 
			
		||||
	td["model.layers.31.self_attn.q_proj.lora_b"] = &tensorData{
 | 
			
		||||
		Offsets: []int{offset, offset * 2},
 | 
			
		||||
		Type:    "F32",
 | 
			
		||||
		Shape:   []int{8, 4096},
 | 
			
		||||
	}
 | 
			
		||||
	td["model.layers.31.self_attn.v_proj.lora_a"] = &tensorData{
 | 
			
		||||
		Offsets: []int{offset * 2, offset * 3},
 | 
			
		||||
		Type:    "F32",
 | 
			
		||||
		Shape:   []int{4096, 8},
 | 
			
		||||
	}
 | 
			
		||||
	td["model.layers.31.self_attn.v_proj.lora_b"] = &tensorData{
 | 
			
		||||
		Offsets: []int{offset * 3, offset*3 + 8*1024*4},
 | 
			
		||||
		Type:    "F32",
 | 
			
		||||
		Shape:   []int{8, 1024},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data, err := json.Marshal(td)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var buf bytes.Buffer
 | 
			
		||||
 | 
			
		||||
	l := int64(len(data))
 | 
			
		||||
	err = binary.Write(&buf, binary.LittleEndian, l)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = buf.Write(data)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// write some data for the tensors
 | 
			
		||||
 | 
			
		||||
	ones := make([]float32, 4096*8)
 | 
			
		||||
	for i := range ones {
 | 
			
		||||
		ones[i] = float32(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for range 3 {
 | 
			
		||||
		err = binary.Write(&buf, binary.LittleEndian, ones)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ones = make([]float32, 1024*8)
 | 
			
		||||
	for i := range ones {
 | 
			
		||||
		ones[i] = float32(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = binary.Write(&buf, binary.LittleEndian, ones)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fdata, err := os.Create(filepath.Join(tempDir, "adapters.safetensors"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer fdata.Close()
 | 
			
		||||
 | 
			
		||||
	_, err = fdata.Write(buf.Bytes())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	configData := `
 | 
			
		||||
{
 | 
			
		||||
    "adapter_path": "adapters-test",
 | 
			
		||||
    "batch_size": 8,
 | 
			
		||||
    "config": "config-tiny.json",
 | 
			
		||||
    "data": "../discollama-completion",
 | 
			
		||||
    "grad_checkpoint": null,
 | 
			
		||||
    "iters": 1000,
 | 
			
		||||
    "learning_rate": 1e-05,
 | 
			
		||||
    "lora_layers": 1,
 | 
			
		||||
    "lora_parameters": {
 | 
			
		||||
        "rank": 8,
 | 
			
		||||
        "alpha": 16,
 | 
			
		||||
        "dropout": 0.0,
 | 
			
		||||
        "scale": 2.0
 | 
			
		||||
    },
 | 
			
		||||
    "lr_schedule": null,
 | 
			
		||||
    "max_seq_length": 2048,
 | 
			
		||||
    "model": "/Users/pdevine/git/Meta-Llama-3-8B-Instruct",
 | 
			
		||||
    "resume_adapter_file": null,
 | 
			
		||||
    "save_every": 100,
 | 
			
		||||
    "seed": 0,
 | 
			
		||||
    "steps_per_eval": 200,
 | 
			
		||||
    "steps_per_report": 10,
 | 
			
		||||
    "test": false,
 | 
			
		||||
    "test_batches": 500,
 | 
			
		||||
    "train": true,
 | 
			
		||||
    "use_dora": false,
 | 
			
		||||
    "val_batches": 25
 | 
			
		||||
}
 | 
			
		||||
`
 | 
			
		||||
	f, err := os.Create(filepath.Join(tempDir, "adapter_config.json"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer f.Close()
 | 
			
		||||
 | 
			
		||||
	_, err = f.WriteString(configData)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										88
									
								
								convert/reader.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								convert/reader.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Tensor interface {
 | 
			
		||||
	Name() string
 | 
			
		||||
	Shape() []uint64
 | 
			
		||||
	Kind() uint32
 | 
			
		||||
	SetRepacker(Repacker)
 | 
			
		||||
	WriteTo(io.Writer) (int64, error)
 | 
			
		||||
	Clone() Tensor
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type tensorBase struct {
 | 
			
		||||
	name     string
 | 
			
		||||
	shape    []uint64
 | 
			
		||||
	repacker Repacker
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t tensorBase) Name() string {
 | 
			
		||||
	return t.name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t tensorBase) Shape() []uint64 {
 | 
			
		||||
	return t.shape
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	tensorKindF32 uint32 = iota
 | 
			
		||||
	tensorKindF16
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (t tensorBase) Kind() uint32 {
 | 
			
		||||
	if strings.HasSuffix(t.name, ".ffn_gate_inp.weight") ||
 | 
			
		||||
		t.name == "token_types.weight" ||
 | 
			
		||||
		t.name == "v.positional_embedding_vlm" ||
 | 
			
		||||
		t.name == "v.tile_position_embd.weight" ||
 | 
			
		||||
		t.name == "v.pre_tile_position_embd.weight" ||
 | 
			
		||||
		t.name == "v.post_tile_position_embd.weight" {
 | 
			
		||||
		// these tensors are always F32
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch len(t.shape) {
 | 
			
		||||
	case 0:
 | 
			
		||||
		panic("invalid tensor shape")
 | 
			
		||||
	case 1:
 | 
			
		||||
		return tensorKindF32
 | 
			
		||||
	default:
 | 
			
		||||
		return tensorKindF16
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *tensorBase) SetRepacker(fn Repacker) {
 | 
			
		||||
	t.repacker = fn
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Repacker func(string, []float32, []uint64) ([]float32, error)
 | 
			
		||||
 | 
			
		||||
func parseTensors(fsys fs.FS, replacer *strings.Replacer) ([]Tensor, error) {
 | 
			
		||||
	patterns := []struct {
 | 
			
		||||
		Pattern string
 | 
			
		||||
		Func    func(fs.FS, *strings.Replacer, ...string) ([]Tensor, error)
 | 
			
		||||
	}{
 | 
			
		||||
		{"*.safetensors", parseSafetensors},
 | 
			
		||||
		{"pytorch_model-*-of-*.bin", parseTorch},
 | 
			
		||||
		{"pytorch_model.bin", parseTorch},
 | 
			
		||||
		{"consolidated.*.pth", parseTorch},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, pattern := range patterns {
 | 
			
		||||
		matches, err := fs.Glob(fsys, pattern.Pattern)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(matches) > 0 {
 | 
			
		||||
			return pattern.Func(fsys, replacer, matches...)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil, errors.New("unknown tensor format")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										178
									
								
								convert/reader_safetensors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								convert/reader_safetensors.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,178 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/binary"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/d4l3k/go-bfloat16"
 | 
			
		||||
	"github.com/x448/float16"
 | 
			
		||||
	"golang.org/x/exp/maps"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type safetensorMetadata struct {
 | 
			
		||||
	Type    string   `json:"dtype"`
 | 
			
		||||
	Shape   []uint64 `json:"shape"`
 | 
			
		||||
	Offsets []int64  `json:"data_offsets"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseSafetensors(fsys fs.FS, replacer *strings.Replacer, ps ...string) ([]Tensor, error) {
 | 
			
		||||
	var ts []Tensor
 | 
			
		||||
	for _, p := range ps {
 | 
			
		||||
		f, err := fsys.Open(p)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		defer f.Close()
 | 
			
		||||
 | 
			
		||||
		var n int64
 | 
			
		||||
		if err := binary.Read(f, binary.LittleEndian, &n); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		b := bytes.NewBuffer(make([]byte, 0, n))
 | 
			
		||||
		if _, err = io.CopyN(b, f, n); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var headers map[string]safetensorMetadata
 | 
			
		||||
		if err := json.NewDecoder(b).Decode(&headers); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		keys := maps.Keys(headers)
 | 
			
		||||
		slices.Sort(keys)
 | 
			
		||||
 | 
			
		||||
		names := make(map[string]struct{}, len(keys))
 | 
			
		||||
 | 
			
		||||
		for _, key := range keys {
 | 
			
		||||
			if value := headers[key]; value.Type != "" {
 | 
			
		||||
				// bitsandbytes quantized models are unsupported
 | 
			
		||||
				if len(value.Shape) == 0 {
 | 
			
		||||
					return nil, errors.New("unsupported safetensors model")
 | 
			
		||||
				}
 | 
			
		||||
				ggufName := replacer.Replace(key)
 | 
			
		||||
				if _, ok := names[ggufName]; ok {
 | 
			
		||||
					return nil, fmt.Errorf("duplicate tensor name '%s' was found for this model", ggufName)
 | 
			
		||||
				}
 | 
			
		||||
				names[ggufName] = struct{}{}
 | 
			
		||||
				ts = append(ts, safetensor{
 | 
			
		||||
					fs:     fsys,
 | 
			
		||||
					path:   p,
 | 
			
		||||
					dtype:  value.Type,
 | 
			
		||||
					offset: safetensorsPad(n, value.Offsets[0]),
 | 
			
		||||
					size:   safetensorsPad(n, value.Offsets[1]) - safetensorsPad(n, value.Offsets[0]),
 | 
			
		||||
					tensorBase: &tensorBase{
 | 
			
		||||
						name:  ggufName,
 | 
			
		||||
						shape: value.Shape,
 | 
			
		||||
					},
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ts, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// safetensorsPad returns the padded size of the safetensors file given a length n and offset s
 | 
			
		||||
func safetensorsPad(n, offset int64) int64 {
 | 
			
		||||
	return 8 + n + offset
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type safetensor struct {
 | 
			
		||||
	fs     fs.FS
 | 
			
		||||
	path   string
 | 
			
		||||
	dtype  string
 | 
			
		||||
	offset int64
 | 
			
		||||
	size   int64
 | 
			
		||||
	*tensorBase
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (st safetensor) Clone() Tensor {
 | 
			
		||||
	return &safetensor{
 | 
			
		||||
		fs:     st.fs,
 | 
			
		||||
		path:   st.path,
 | 
			
		||||
		dtype:  st.dtype,
 | 
			
		||||
		offset: st.offset,
 | 
			
		||||
		size:   st.size,
 | 
			
		||||
		tensorBase: &tensorBase{
 | 
			
		||||
			name:     st.name,
 | 
			
		||||
			repacker: st.repacker,
 | 
			
		||||
			shape:    slices.Clone(st.shape),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (st safetensor) WriteTo(w io.Writer) (int64, error) {
 | 
			
		||||
	f, err := st.fs.Open(st.path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
	defer f.Close()
 | 
			
		||||
 | 
			
		||||
	if seeker, ok := f.(io.Seeker); ok {
 | 
			
		||||
		if _, err := seeker.Seek(st.offset, io.SeekStart); err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		if _, err := io.CopyN(io.Discard, f, st.offset); err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var f32s []float32
 | 
			
		||||
	switch st.dtype {
 | 
			
		||||
	case "F32":
 | 
			
		||||
		f32s = make([]float32, st.size/4)
 | 
			
		||||
		if err = binary.Read(f, binary.LittleEndian, f32s); err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
	case "F16":
 | 
			
		||||
		u16s := make([]uint16, st.size/2)
 | 
			
		||||
		if err = binary.Read(f, binary.LittleEndian, u16s); err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		f32s = make([]float32, len(u16s))
 | 
			
		||||
		for i := range u16s {
 | 
			
		||||
			f32s[i] = float16.Frombits(u16s[i]).Float32()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	case "BF16":
 | 
			
		||||
		u8s := make([]uint8, st.size)
 | 
			
		||||
		if err = binary.Read(f, binary.LittleEndian, u8s); err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		f32s = bfloat16.DecodeFloat32(u8s)
 | 
			
		||||
	default:
 | 
			
		||||
		return 0, fmt.Errorf("unknown data type: %s", st.dtype)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if st.repacker != nil {
 | 
			
		||||
		f32s, err = st.repacker(st.Name(), f32s, st.Shape())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch st.Kind() {
 | 
			
		||||
	case tensorKindF32:
 | 
			
		||||
		return 0, binary.Write(w, binary.LittleEndian, f32s)
 | 
			
		||||
	case tensorKindF16:
 | 
			
		||||
		f16s := make([]uint16, len(f32s))
 | 
			
		||||
		for i := range f32s {
 | 
			
		||||
			f16s[i] = float16.Fromfloat32(f32s[i]).Bits()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return 0, binary.Write(w, binary.LittleEndian, f16s)
 | 
			
		||||
	default:
 | 
			
		||||
		return 0, fmt.Errorf("unknown storage type: %d", st.Kind())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user