A local-first private AI assistant for everyday use. Runs on-device models with encrypted P2P sync, and supports sharing chats publicly on ATProto.
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #97 from tilesprivacy/build/pkg-build-enhancements

portable installer + pkg enhancements

authored by

Anandu Pavanan and committed by
GitHub
09005518 2d836329

+461 -138
+1
.gitignore
··· 8 8 .DS_Store 9 9 pkgroot/ 10 10 *.pkg 11 + models/ 11 12 pkgroot_models/
+10
CHANGELOG.md
··· 5 5 6 6 ## [Unreleased] 7 7 8 + ## [0.4.4] - 2026-03-16 9 + 10 + ### Added 11 + - Added a core daemon process which will be useful for handling background processes in https://github.com/tilesprivacy/tiles/pull/102 12 + 13 + - Use `tiles daemon stop` and `tiles daemon start` for starting and stopping the daemon explicitly. NOTE: daemon will auto-start when you run `tiles`. 14 + 15 + - Added support for fully offline/portable installer in https://github.com/tilesprivacy/tiles/pull/97 16 + 17 + 8 18 ## [0.4.3] - 2026-03-08 9 19 10 20 ### Added
+1 -1
Cargo.lock
··· 4365 4365 4366 4366 [[package]] 4367 4367 name = "tiles" 4368 - version = "0.4.3" 4368 + version = "0.4.4" 4369 4369 dependencies = [ 4370 4370 "anyhow", 4371 4371 "async-std",
+8
justfile
··· 23 23 24 24 bundle_pkg: 25 25 ./pkg/build.sh 26 + ./pkg/bundle_network_installer.sh 27 + 28 + bundle_model_pkg: 29 + ./pkg/build_model.sh 30 + 31 + bundle_pkg_full: 32 + ./pkg/build.sh 33 + ./pkg/build_full.sh
-1
mem-agent
··· 1 - FROM driaforall/mem-agent-mlx-4bit
+1
modelfiles/qwen
··· 1 1 FROM mlx-community/Qwen3.5-4B-MLX-4bit 2 + # FROM mlx-community/Qwen3-0.6B-4bit
+9 -7
pkg/build.sh
··· 8 8 MODELFILE_DIR="modelfiles" 9 9 SERVER_DIR="server" 10 10 BINARY_NAME="tiles" 11 - 11 + MODELS_DIR="models" 12 12 VERSION=$(grep '^version' tiles/Cargo.toml | head -1 | awk -F'"' '{print $2}') 13 13 OS=$(uname -s | tr '[:upper:]' '[:lower:]') 14 14 ARCH=$(uname -m) ··· 33 33 cp "target/${TARGET}/${BINARY_NAME}" "${CLI_BIN_PATH}" 34 34 chmod +x "${CLI_BIN_PATH}/tiles" 35 35 36 + # Signing the tiles binary 37 + codesign --force \ 38 + --sign "$DEVELOPER_ID_APPLICATION"\ 39 + --options runtime \ 40 + --timestamp \ 41 + --strict \ 42 + "${CLI_BIN_PATH}/tiles" 36 43 37 44 # Build venvstack and move to /usr/local/share/tiles 38 45 # ··· 64 71 65 72 66 73 # Creating .pkg 67 - pkgbuild --root pkgroot --scripts pkg/scripts --identifier com.tilesprivacy.tiles --version "$VERSION" "tiles-${VERSION}".pkg 68 - 74 + pkgbuild --root pkgroot --scripts pkg/scripts --identifier com.tilesprivacy.tiles --version "$VERSION" pkg/tiles-unsigned.pkg 69 75 70 - # signing 71 - 72 - 73 - # notarizing
+31
pkg/build_full.sh
··· 1 + #!/usr/bin/env bash 2 + 3 + set -euo pipefail 4 + 5 + VERSION=$(grep '^version' tiles/Cargo.toml | head -1 | awk -F'"' '{print $2}') 6 + 7 + 8 + productbuild \ 9 + --distribution pkg/distribution.xml \ 10 + --resources pkg/resources \ 11 + --package-path pkg/ \ 12 + pkg/tiles-full-unsigned.pkg 13 + 14 + 15 + # signing 16 + # 17 + productsign \ 18 + --sign "$DEVELOPER_ID_INSTALLER" \ 19 + pkg/tiles-full-unsigned.pkg \ 20 + pkg/tiles-full.pkg 21 + 22 + # notarizing 23 + # 24 + # xcrun notarytool submit pkg/tiles-full.pkg \ 25 + # --keychain-profile "tiles-notary-profile" \ 26 + # --wait 27 + 28 + # # staple the approval ticket to pkg 29 + # xcrun stapler staple pkg/tiles-full.pkg 30 + 31 + rm pkg/tiles-full-unsigned.pkg
+4
pkg/build_model.sh
··· 1 + # model pkg command, run when model changes, or need a local copy for final pkg 2 + MODELS_VERSION=1.0 3 + 4 + pkgbuild --root pkgroot_models --identifier com.tilesprivacy.tiles_models --version "$MODELS_VERSION" pkg/tiles-model.pkg
+29
pkg/bundle_network_installer.sh
··· 1 + #!/usr/bin/env bash 2 + 3 + set -euo pipefail 4 + 5 + VERSION=$(grep '^version' tiles/Cargo.toml | head -1 | awk -F'"' '{print $2}') 6 + 7 + productbuild \ 8 + --distribution pkg/distribution_network.xml \ 9 + --resources pkg/resources \ 10 + --package-path pkg/ \ 11 + pkg/tiles-dist-unsigned.pkg 12 + 13 + 14 + # signing 15 + productsign \ 16 + --sign "$DEVELOPER_ID_INSTALLER" \ 17 + pkg/tiles-dist-unsigned.pkg \ 18 + pkg/tiles.pkg 19 + 20 + # notarizing 21 + xcrun notarytool submit pkg/tiles.pkg \ 22 + --keychain-profile "tiles-notary-profile" \ 23 + --wait 24 + 25 + # staple the approval ticket to pkg 26 + xcrun stapler staple pkg/tiles.pkg 27 + 28 + rm pkg/tiles-unsigned.pkg 29 + rm pkg/tiles-dist-unsigned.pkg
+32
pkg/distribution.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <installer-gui-script minSpecVersion="1"> 3 + <title>Tiles</title> 4 + 5 + 6 + <!-- <background file="sidebar.png" mime-type="image/png" scaling="proportionally" alignment="left"/> --> 7 + 8 + 9 + <!-- <background-darkAqua file="sidebar.png" mime-type="image/png" scaling="proportionally" alignment="left"/> --> 10 + 11 + 12 + <options customize="never"/> 13 + 14 + <welcome file="welcome.html"/> 15 + 16 + <conclusion file="conclusion.html"/> 17 + 18 + <choices-outline> 19 + <line choice="app" /> 20 + <line choice="model" /> 21 + </choices-outline> 22 + 23 + <choice id="app" visible="true" start_selected="true" title="app"> 24 + <pkg-ref id="com.tilesprivacy.tiles" onConclusion="none">tiles-unsigned.pkg</pkg-ref> 25 + </choice> 26 + 27 + <choice id="model" visible="true" start_selected="true" title="model"> 28 + <pkg-ref id="com.tilesprivacy.tiles_models" onConclusion="none">tiles-model.pkg</pkg-ref> 29 + </choice> 30 + 31 + 32 + </installer-gui-script>
+29
pkg/distribution_network.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <installer-gui-script minSpecVersion="1"> 3 + <title>Tiles</title> 4 + 5 + 6 + <!-- <background file="sidebar.png" mime-type="image/png" scaling="proportionally" alignment="left"/> --> 7 + 8 + 9 + <!-- <background-darkAqua file="sidebar.png" mime-type="image/png" scaling="proportionally" alignment="left"/> --> 10 + 11 + 12 + <options customize="never"/> 13 + 14 + <welcome file="welcome_network.html"/> 15 + 16 + <conclusion file="conclusion.html"/> 17 + 18 + <choices-outline> 19 + <line choice="app" /> 20 + </choices-outline> 21 + 22 + <choice id="app" visible="true" start_selected="true" title="app"> 23 + <pkg-ref id="com.tilesprivacy.tiles" onConclusion="none">tiles-unsigned.pkg</pkg-ref> 24 + </choice> 25 + 26 + 27 + </installer-gui-script> 28 + 29 +
+43
pkg/pkg_building.md
··· 1 + ## How the Tiles pkgs are build 2 + 3 + ### Network Installer 4 + 5 + Network installer is basically Tiles without any ML models included in it. 6 + So when model is needed, Tiles will download it. (Maybe in a later version 7 + a user should be able to download from its peers locally too). 8 + 9 + ``` 10 + just bundle_pkg 11 + ``` 12 + 13 + Creates tiles.pkg, signs and notarize it 14 + 15 + 16 + ### Offline Installer 17 + 18 + Offline Installer includes the default model too in it, so once 19 + downloaded provides a portable installer, and can work w/o 20 + internet forever and ever... 21 + 22 + ``` 23 + just bundle_model_pkg 24 + 25 + ``` 26 + 27 + This will bundle only the model in a .pkg. 28 + 29 + > We run this command only when a model is updated/added etc.. 30 + Since this is a time taking process and is not needed to run 31 + in every release build 32 + 33 + The basic approach we will take for offline installer building is that 34 + we build 2 pkgs essentially, the network installer and a pkg with 35 + only models. Then we create a final package that has these 2 pkgs with 36 + the command below. 37 + 38 + 39 + ``` 40 + just bundle_pkg_full 41 + 42 + ``` 43 + Creates tiles-full.pkg, signs and notarize it
+29
pkg/resources/conclusion.html
··· 1 + <html> 2 + <head> 3 + <meta charset="utf-8"> 4 + <style> 5 + body { 6 + font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; 7 + font-size: 13px; 8 + } 9 + </style> 10 + </head> 11 + 12 + <body> 13 + <h2>Tiles Installed 🚀</h2> 14 + <p>The <b>tiles</b> CLI is now installed.</p> 15 + 16 + <p>Open Terminal and run:</p> 17 + 18 + <pre>tiles</pre><br> 19 + 20 + <p>Then complete CLI onboarding to set up your account and start using the chat interface.</p> 21 + 22 + <p>For more options run:</p> 23 + 24 + <pre>tiles --help</pre><br><br> 25 + 26 + <p style="font-size: 0.8em;"><i>keep on tiling...</i></p> 27 + 28 + </body> 29 + </html>
pkg/resources/sidebar.png

This is a binary file and will not be displayed.

+23
pkg/resources/welcome.html
··· 1 + <html> 2 + <head> 3 + <style> 4 + body { 5 + font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; 6 + font-size: 13px; 7 + } 8 + </style> 9 + </head> 10 + 11 + <body> 12 + <h2>Welcome to the Offline Tiles installer.</h2> 13 + 14 + <p> 15 + Tiles is your private and secure AI assistant for everyday use. Developed as an independent open source project, made possible by wonderful sponsors.<br><br> 16 + 17 + This setup installs the Tiles runtime, dependencies, and the default <i>gpt-oss-20b</i> model so the system works fully offline with no additional downloads.<br><br> 18 + 19 + Tiles installs its CLI and runtime in <i><b>/usr/local</b></i> so that it is available system-wide. MacOS will request administrator authorization (password or Touch ID) to allow this installation. 20 + 21 + </p> 22 + </body> 23 + </html>
+24
pkg/resources/welcome_network.html
··· 1 + <html> 2 + <head> 3 + <style> 4 + body { 5 + font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; 6 + font-size: 13px; 7 + } 8 + </style> 9 + </head> 10 + 11 + <body> 12 + <h2>Welcome to the Network Tiles installer.</h2> 13 + 14 + <p> 15 + Tiles is your private and secure AI assistant for everyday use. Developed as an independent open source project, made possible by wonderful sponsors.<br><br> 16 + 17 + This setup installs the Tiles runtime and required dependencies. 18 + During onboarding you will be prompted to download a model.<br><br> 19 + 20 + Tiles installs its CLI and runtime in <i><b>/usr/local</b></i> so that it is available system-wide. MacOS will request administrator authorization (password or Touch ID) to allow this installation. 21 + 22 + </p> 23 + </body> 24 + </html>
+3 -5
scripts/install.sh
··· 3 3 4 4 ENV="prod" # prod is another env, try taking it from github env 5 5 REPO="tilesprivacy/tiles" 6 - # VERSION="${TILES_VERSION:-latest}" 6 + VERSION=$(grep '^version' tiles/Cargo.toml | head -1 | awk -F'"' '{print $2}') 7 + 7 8 VERSION="0.4.3" 8 - # INSTALL_DIR="$HOME/.local/bin" # CLI install location 9 9 INSTALL_DIR="/usr/local/bin" # CLI install location 10 - # SERVER_DIR="$HOME/.local/lib/tiles/server" # Python server folder 11 - # MODELFILE_DIR="$HOME/.local/lib/tiles/modelfiles" # Python server folder 12 10 13 11 SERVER_DIR="/usr/local/share/tiles/server" # Python server folder 14 - MODELFILE_DIR="/usr/local/share/tiles/modelfiles" # Python server folder 12 + MODELFILE_DIR="/usr/local/share/tiles/modelfiles" # Modelfile server folder 15 13 16 14 TMPDIR="$(mktemp -d)" 17 15 OS=$(uname -s | tr '[:upper:]' '[:lower:]')
+2 -1
server/api.py
··· 47 47 async def start_model(request: StartRequest): 48 48 """Load the model and start the agent""" 49 49 global _messages, _runner, _memory_path 50 + print(f"CACHE PATH{request.model_cache_path}") 50 51 51 52 _messages = [ChatMessage(role="system", content=request.system_prompt)] 52 53 _memory_path = request.memory_path 53 54 logger.info(f"{runtime.backend}") 54 - runtime.backend.get_or_load_model(request.model) 55 + runtime.backend.get_or_load_model(request.model, request.model_cache_path) 55 56 return {"message": "Model loaded"} 56 57 57 58
+36 -48
server/backend/mlx.py
··· 3 3 import time 4 4 import uuid 5 5 from collections.abc import AsyncGenerator 6 - 6 + from pathlib import Path 7 7 from fastapi import HTTPException 8 8 from openai_harmony import ( 9 9 Conversation, ··· 54 54 raise HTTPException(status_code=400, detail="Downloading model failed") 55 55 56 56 57 - def get_or_load_model(model_spec: str, verbose: bool = True) -> MLXRunner: 57 + def get_or_load_model( 58 + model_spec: str, model_cache_path: str | None = None, verbose: bool = True 59 + ) -> MLXRunner: 58 60 """Get model from cache or load it if not cached.""" 59 61 global _model_cache, _current_model_path 60 - 61 - # Use the existing model path resolution from cache_utils 62 - 63 - try: 64 - model_path, model_name, commit_hash = get_model_path(model_spec) 65 - if not model_path.exists(): 66 - logger.info(f"Model {model_spec} not found in cache") 67 - raise HTTPException( 68 - status_code=404, detail=f"Model {model_spec} not found in cache" 69 - ) 70 - except Exception as e: 71 - logger.info(f"Model {model_spec} not found in: {str(e)}") 72 - raise HTTPException( 73 - status_code=404, detail=f"Model {model_spec} not found: {str(e)}" 74 - ) 75 - 76 - # Check if it's an MLX model 77 - 78 - model_path_str = str(model_path) 79 - 80 - # Check if we need to load a different model 81 - if _current_model_path != model_path_str: 82 - # Proactively clean up any previously loaded runner to release memory 83 - if _model_cache: 84 - try: 85 - for _old_runner in list(_model_cache.values()): 86 - try: 87 - _old_runner.cleanup() 88 - except Exception: 89 - pass 90 - finally: 91 - _model_cache.clear() 62 + model_name = model_spec 63 + if isinstance(model_cache_path, str): 64 + model_path_str = model_cache_path 65 + # Check if we need to load a different model 66 + if _current_model_path != model_path_str: 67 + # Proactively clean up any previously loaded runner to release memory 68 + if _model_cache: 69 + try: 70 + for _old_runner in list(_model_cache.values()): 71 + try: 72 + _old_runner.cleanup() 73 + except Exception: 74 + pass 75 + finally: 76 + _model_cache.clear() 92 77 93 - # Load new model 94 - if verbose: 95 - print(f"Loading model: {model_name}") 78 + # Load new model 79 + if verbose: 80 + print(f"Loading model: {model_name}") 96 81 97 - logger.info(f"Loading model: {model_name}") 98 - runner = MLXRunner(model_path_str, verbose=verbose) 99 - runner.load_model() 82 + logger.info(f"Loading model: {model_name}") 83 + runner = MLXRunner(model_path_str, verbose=verbose) 84 + runner.load_model() 100 85 101 - _model_cache[model_path_str] = runner 102 - _current_model_path = model_path_str 86 + _model_cache[model_path_str] = runner 87 + _current_model_path = model_path_str 88 + return runner 89 + else: 90 + logger.info(f"Model {model_name} already in memory") 91 + return _model_cache[_current_model_path] # pyright: ignore 103 92 else: 104 - logger.info(f"Model {model_name} already in memory") 105 - 106 - return _model_cache[model_path_str] 93 + logger.info(f"Model Path {_current_model_path} already in memory") 94 + return _model_cache[_current_model_path] # pyright: ignore 107 95 108 96 109 97 async def generate_chat_stream( ··· 114 102 _messages = messages 115 103 completion_id = f"chatcmpl-{uuid.uuid4()}" 116 104 created = int(time.time()) 117 - runner = get_or_load_model(request.model) 105 + runner = get_or_load_model(request.model, None) 118 106 if request.chat_start: 119 107 _messages.extend(request.messages) 120 108 # Convert messages to dict format for runner ··· 312 300 """Generate streaming chat responses for OpenResponses API.""" 313 301 model = request.model 314 302 created = int(time.time()) 315 - runner = get_or_load_model(model) 303 + runner = get_or_load_model(model, None) 316 304 metrics = None 317 305 318 306 user_input_content = "" ··· 491 479 response_id = f"resp-{uuid.uuid4()}" 492 480 msg_id = f"msg_{uuid.uuid4()}" 493 481 created = int(time.time()) 494 - runner = get_or_load_model(model) 482 + runner = get_or_load_model(model, None) 495 483 496 484 user_input_content = "" 497 485
+1
server/schemas.py
··· 81 81 model: str 82 82 memory_path: str 83 83 system_prompt: str 84 + model_cache_path: str 84 85 85 86 86 87 class downloadRequest(BaseModel):
+1 -1
server/stack/requirements/app-server/packages-app-server.txt
··· 4 4 anyio==4.12.1 5 5 black==25.9.0 6 6 certifi==2026.2.25 7 - charset-normalizer==3.4.5 7 + charset-normalizer==3.4.6 8 8 click==8.3.1 9 9 fastapi==0.119.0 10 10 filelock==3.25.2
+2 -2
server/stack/requirements/app-server/pylock.app-server.meta.json
··· 1 1 { 2 2 "lock_input_hash": "sha256:c836d5cfb697330a57241b2b8f275a804178488ec906b19866809ef33c95ba81", 3 3 "lock_version": 1, 4 - "locked_at": "2026-03-13T17:42:07.680623+00:00", 4 + "locked_at": "2026-03-15T22:15:15.536434+00:00", 5 5 "other_inputs_hash": "sha256:63b3c2cfe2ec414938e81dace7aac779c7b902bae681618cd8827e9f16880985", 6 - "requirements_hash": "sha256:641adc25f61b50b1a2c832d6e2bca50ff879f623e3956bb04a5792ee8679ed57", 6 + "requirements_hash": "sha256:71fb833c54864760da900c69c2a0829e19fef2c6b6e8c174162fdb7f021a4eb3", 7 7 "version_inputs_hash": "sha256:58db986b7cd72eeded675f7c9afd8138fe024fb51451131b5562922bbde3cf43" 8 8 }
+17 -17
server/stack/requirements/app-server/pylock.app-server.toml
··· 75 75 76 76 [[packages]] 77 77 name = "charset-normalizer" 78 - version = "3.4.5" 78 + version = "3.4.6" 79 79 index = "https://pypi.org/simple" 80 80 81 81 [[packages.wheels]] 82 - url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl" 83 - upload-time = 2026-03-06T06:01:37Z 84 - size = 280788 82 + url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl" 83 + upload-time = 2026-03-15T18:51:15Z 84 + size = 294823 85 85 86 86 [packages.wheels.hashes] 87 - sha256 = "ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23" 87 + sha256 = "11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f" 88 88 89 89 [[packages.wheels]] 90 - url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" 91 - upload-time = 2026-03-06T06:01:43Z 92 - size = 195572 90 + url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" 91 + upload-time = 2026-03-15T18:51:21Z 92 + size = 206587 93 93 94 94 [packages.wheels.hashes] 95 - sha256 = "0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819" 95 + sha256 = "530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9" 96 96 97 97 [[packages.wheels]] 98 - url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl" 99 - upload-time = 2026-03-06T06:01:56Z 100 - size = 197509 98 + url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl" 99 + upload-time = 2026-03-15T18:51:34Z 100 + size = 208652 101 101 102 102 [packages.wheels.hashes] 103 - sha256 = "c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6" 103 + sha256 = "6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2" 104 104 105 105 [[packages.wheels]] 106 - url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl" 107 - upload-time = 2026-03-06T06:03:17Z 108 - size = 55455 106 + url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl" 107 + upload-time = 2026-03-15T18:53:23Z 108 + size = 61455 109 109 110 110 [packages.wheels.hashes] 111 - sha256 = "9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0" 111 + sha256 = "947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69" 112 112 113 113 [[packages]] 114 114 name = "click"
+1 -1
tiles/Cargo.toml
··· 1 1 [package] 2 2 name = "tiles" 3 - version = "0.4.3" 3 + version = "0.4.4" 4 4 edition = "2024" 5 5 6 6 [dependencies]
+46 -31
tiles/src/runtime/mlx.rs
··· 2 2 use crate::core::chats::{Message, save_chat}; 3 3 use crate::core::storage::db::get_db_conn; 4 4 use crate::runtime::RunArgs; 5 - use crate::utils::config::{ConfigProvider, DefaultProvider, get_memory_path}; 5 + use crate::utils::config::{ConfigProvider, DefaultProvider, get_memory_path, get_model_cache}; 6 6 use crate::utils::hf_model_downloader::*; 7 7 use anyhow::{Context, Result, anyhow}; 8 8 use futures_util::StreamExt; ··· 90 90 } 91 91 }; 92 92 93 - run_model_with_server(self, modelfile, default_modelfile, &run_args) 94 - .await 95 - .inspect_err(|e| eprintln!("Failed to run the model due to {e}")) 93 + run_model_with_server(self, modelfile, default_modelfile, &run_args).await 96 94 } 97 95 98 96 #[allow(clippy::zombie_processes)] ··· 404 402 default_modelfile: &Modelfile, 405 403 memory_path: &str, 406 404 ) -> Result<()> { 407 - let client = Client::new(); 408 405 let model_name = modelfile.from.clone().unwrap(); 409 - let body = json!({ 410 - "model": model_name, 411 - "memory_path": memory_path, 412 - "system_prompt": modelfile.system.clone().unwrap_or(default_modelfile.system.clone().unwrap_or("".to_owned())) 413 - }); 414 406 415 - let res = client 416 - .post("http://127.0.0.1:6969/start") 417 - .json(&body) 418 - .send() 419 - .await?; 420 - match res.status() { 421 - StatusCode::OK => Ok(()), 422 - StatusCode::NOT_FOUND => { 423 - println!("Downloading {}\n", model_name); 424 - match pull_model(&model_name).await { 425 - Ok(_) => { 426 - println!("\nDownloading completed \n"); 427 - Ok(()) 428 - } 429 - Err(err) => Err(anyhow::anyhow!(format!("Download failed due to {:?}", err))), 430 - } 431 - } 432 - _ => Err(anyhow::anyhow!(format!( 433 - "Failed to load model {} due to {:?}", 434 - model_name, res 435 - ))), 407 + if let Ok(model_cache_path) = get_model_cache(&model_name) { 408 + load_model_in_py(modelfile, default_modelfile, memory_path, &model_cache_path).await 409 + } else { 410 + download_model(&model_name).await?; 411 + let model_cache_path = get_model_cache(&model_name)?; 412 + load_model_in_py(modelfile, default_modelfile, memory_path, &model_cache_path).await 436 413 } 437 414 } 438 415 ··· 649 626 vec![dev_msg, input] 650 627 } 651 628 } 629 + 630 + async fn load_model_in_py( 631 + modelfile: &Modelfile, 632 + default_modelfile: &Modelfile, 633 + memory_path: &str, 634 + model_cache_path: &PathBuf, 635 + ) -> Result<()> { 636 + let client = Client::new(); 637 + let model_name = modelfile.from.clone().unwrap(); 638 + let body = json!({ 639 + "model": model_name, 640 + "memory_path": memory_path, 641 + "model_cache_path": model_cache_path, 642 + "system_prompt": modelfile.system.clone().unwrap_or(default_modelfile.system.clone().unwrap_or("".to_owned())) 643 + }); 644 + let res = client 645 + .post("http://127.0.0.1:6969/start") 646 + .json(&body) 647 + .send() 648 + .await?; 649 + match res.status() { 650 + StatusCode::OK => Ok(()), 651 + _ => Err(anyhow::anyhow!(format!( 652 + "Failed to load model {} due to {:?}", 653 + model_name, res 654 + ))), 655 + } 656 + } 657 + 658 + async fn download_model(model_name: &str) -> Result<()> { 659 + match pull_model(model_name).await { 660 + Ok(_) => { 661 + println!("\nDownloading completed \n"); 662 + Ok(()) 663 + } 664 + Err(err) => Err(anyhow::anyhow!(format!("Download failed due to {:?}", err))), 665 + } 666 + }
+69 -1
tiles/src/utils/config.rs
··· 12 12 /// - /usr/local/share/tiles (lib dir) - Some internal App files, libraries etc go here.. 13 13 /// - /modelfiles 14 14 /// - /server 15 + /// - /models - Where the pre-downloaded models. 15 16 use anyhow::{Context, Result, anyhow}; 16 17 use std::fs::File; 17 18 use std::path::PathBuf; 18 19 use std::str::FromStr; 20 + use std::time::SystemTime; 19 21 use std::{env, fs}; 20 22 use toml::Table; 21 23 24 + const MODEL_SUB_PATH: &str = "models/huggingface/hub"; 22 25 pub trait ConfigProvider { 23 26 fn get_config_dir(&self) -> Result<PathBuf>; 24 27 fn get_or_create_config_dir(&self) -> Result<PathBuf>; ··· 115 118 fn get_lib_dir(&self) -> Result<PathBuf> { 116 119 if cfg!(debug_assertions) { 117 120 let base_dir = env::current_dir().context("Failed to fetch CURRENT_DIR")?; 118 - Ok(base_dir) 121 + Ok(base_dir.join(".tiles_dev/tiles")) 119 122 } else { 120 123 let data_dir = PathBuf::from_str("/usr/local/share")?; 121 124 Ok(data_dir.join("tiles")) ··· 229 232 fs::copy(&tmp_path, &config_path)?; 230 233 fs::remove_file(tmp_path)?; 231 234 Ok(()) 235 + } 236 + 237 + // Get the apt path where the model lies 238 + pub fn get_model_cache(model_name: &str) -> Result<PathBuf> { 239 + let hf_model_dir = if model_name.starts_with("mlx-community/") { 240 + let model_spec_parts = model_name.split("/").collect::<Vec<&str>>(); 241 + format!("models--{}--{}", model_spec_parts[0], model_spec_parts[1]) 242 + } else { 243 + return Err(anyhow!("Not implemented for non-mlx models")); 244 + }; 245 + 246 + let lib_dir = DefaultProvider.get_lib_dir()?; 247 + let pre_downloaded_model_path = lib_dir.join(MODEL_SUB_PATH).join(&hf_model_dir); 248 + let data_dir = DefaultProvider.get_user_data_dir()?; 249 + let user_data_dir_model_path = data_dir.join(MODEL_SUB_PATH).join(&hf_model_dir); 250 + 251 + let legacy_model_path = PathBuf::from(format!( 252 + "{}/.cache/huggingface/hub", 253 + env::home_dir().unwrap().to_str().unwrap() 254 + )) 255 + .join(&hf_model_dir); 256 + 257 + if pre_downloaded_model_path.exists() { 258 + get_commit_path(pre_downloaded_model_path) 259 + } else if user_data_dir_model_path.exists() { 260 + get_commit_path(user_data_dir_model_path) 261 + } else if legacy_model_path.exists() { 262 + get_commit_path(legacy_model_path) 263 + } else { 264 + Err(anyhow!("Model doesnt exist")) 265 + } 266 + } 267 + 268 + fn get_commit_path(base_path: PathBuf) -> Result<PathBuf> { 269 + let mut snapshots: Vec<(PathBuf, SystemTime)> = vec![]; 270 + let snapshot_path = base_path.join("snapshots"); 271 + if snapshot_path.exists() { 272 + for entry in snapshot_path.read_dir()? { 273 + if let Ok(item) = entry 274 + && item.path().is_dir() 275 + { 276 + snapshots.push((item.path(), item.path().metadata()?.modified()?)); 277 + } 278 + } 279 + if snapshots.is_empty() { 280 + Ok(base_path) 281 + } else { 282 + let latest_snapshot = snapshots 283 + .iter() 284 + .max_by_key(|a| a.1) 285 + .expect("Failed fetching latest snapshot"); 286 + Ok(latest_snapshot.0.clone()) 287 + } 288 + } else { 289 + Ok(base_path) 290 + } 291 + } 292 + 293 + pub fn get_or_create_model_download_path() -> Result<PathBuf> { 294 + let data_dir = DefaultProvider.get_user_data_dir()?; 295 + let model_dir = data_dir.join(MODEL_SUB_PATH); 296 + if !model_dir.exists() { 297 + fs::create_dir_all(&model_dir)?; 298 + } 299 + Ok(model_dir) 232 300 } 233 301 234 302 //TODO: Add more tests for config.toml
+9 -22
tiles/src/utils/hf_model_downloader.rs
··· 1 1 /// Manages model snapshot downloading from HuggingFace 2 - use std::{env, path::PathBuf}; 3 - 2 + use anyhow::{Result, anyhow}; 4 3 use hf_hub::api::{ 5 4 Siblings, 6 5 tokio::{ApiBuilder, ApiError}, 7 6 }; 8 7 8 + use crate::utils::config::get_or_create_model_download_path; 9 + 9 10 /// Download the entire model (including snapshot) for the given model name 10 - pub async fn pull_model(model_name: &str) -> Result<(), String> { 11 + pub async fn pull_model(model_name: &str) -> Result<()> { 11 12 snapshot_download(model_name).await 12 13 } 13 14 14 - pub async fn snapshot_download(modelname: &str) -> Result<(), String> { 15 + pub async fn snapshot_download(modelname: &str) -> Result<()> { 15 16 let allow_patterns = [ 16 17 ".json", 17 18 ".txt", ··· 22 23 ]; 23 24 let api_build_result = ApiBuilder::new() 24 25 .with_progress(true) 25 - .with_cache_dir(PathBuf::from(get_model_cache())) 26 + .with_cache_dir(get_or_create_model_download_path()?) 26 27 .build(); 27 28 28 29 match api_build_result { ··· 42 43 43 44 for sibling in filtered_siblings { 44 45 if repo.get(&sibling.rfilename).await.is_err() { 45 - return Err(format!( 46 + return Err(anyhow!( 46 47 "{:?} failed to download, retry again", 47 48 &sibling.rfilename, 48 49 )); 49 50 } 50 51 } 51 52 } 52 - Err(err) => return Err(format_hf_api_error(err)), 53 + Err(err) => return Err(anyhow!(format_hf_api_error(err))), 53 54 }; 54 55 } 55 - Err(err) => return Err(format_hf_api_error(err)), 56 + Err(err) => return Err(anyhow!(format_hf_api_error(err))), 56 57 } 57 58 58 59 Ok(()) ··· 64 65 ApiError::TooManyRetries(err) => err.to_string(), 65 66 _err => "Something unexpected happened, check your internet connection".to_owned(), 66 67 } 67 - } 68 - 69 - fn get_model_cache() -> String { 70 - let default_cache = format!( 71 - "{}/.cache/huggingface", 72 - env::home_dir().unwrap().to_str().unwrap() 73 - ); 74 - let cache_root = if let Ok(home) = env::var("HF_HOME") { 75 - home.to_owned() 76 - } else { 77 - default_cache 78 - }; 79 - 80 - format!("{}/hub", cache_root) 81 68 } 82 69 83 70 #[cfg(test)]