llms-py 2.0.3__tar.gz → 2.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {llms_py-2.0.3/llms_py.egg-info → llms_py-2.0.5}/PKG-INFO +1 -1
- {llms_py-2.0.3 → llms_py-2.0.5}/llms.py +170 -40
- {llms_py-2.0.3 → llms_py-2.0.5/llms_py.egg-info}/PKG-INFO +1 -1
- {llms_py-2.0.3 → llms_py-2.0.5}/pyproject.toml +1 -1
- {llms_py-2.0.3 → llms_py-2.0.5}/setup.py +1 -1
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/app.css +1 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/LICENSE +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/MANIFEST.in +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/README.md +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/index.html +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/llms.json +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/llms_py.egg-info/SOURCES.txt +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/llms_py.egg-info/dependency_links.txt +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/llms_py.egg-info/entry_points.txt +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/llms_py.egg-info/not-zip-safe +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/llms_py.egg-info/requires.txt +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/llms_py.egg-info/top_level.txt +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/requirements.txt +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/setup.cfg +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/App.mjs +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/ChatPrompt.mjs +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/Main.mjs +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/Recents.mjs +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/Sidebar.mjs +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/fav.svg +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/lib/highlight.min.mjs +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/lib/idb.min.mjs +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/lib/marked.min.mjs +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/lib/servicestack-client.min.mjs +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/lib/servicestack-vue.min.mjs +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/lib/vue-router.min.mjs +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/lib/vue.min.mjs +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/lib/vue.mjs +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/markdown.mjs +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/tailwind.input.css +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/threadStore.mjs +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/typography.css +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui/utils.mjs +0 -0
- {llms_py-2.0.3 → llms_py-2.0.5}/ui.json +0 -0
|
@@ -12,6 +12,8 @@ import subprocess
|
|
|
12
12
|
import base64
|
|
13
13
|
import mimetypes
|
|
14
14
|
import traceback
|
|
15
|
+
import sys
|
|
16
|
+
import site
|
|
15
17
|
|
|
16
18
|
import aiohttp
|
|
17
19
|
from aiohttp import web
|
|
@@ -19,7 +21,8 @@ from aiohttp import web
|
|
|
19
21
|
from pathlib import Path
|
|
20
22
|
from importlib import resources # Py≥3.9 (pip install importlib_resources for 3.7/3.8)
|
|
21
23
|
|
|
22
|
-
VERSION = "2.0.
|
|
24
|
+
VERSION = "2.0.5"
|
|
25
|
+
_ROOT = None
|
|
23
26
|
g_config_path = None
|
|
24
27
|
g_ui_path = None
|
|
25
28
|
g_config = None
|
|
@@ -785,9 +788,117 @@ def disable_provider(provider):
|
|
|
785
788
|
save_config(g_config)
|
|
786
789
|
init_llms(g_config)
|
|
787
790
|
|
|
791
|
+
def resolve_root():
|
|
792
|
+
# Try to find the resource root directory
|
|
793
|
+
# When installed as a package, static files may be in different locations
|
|
794
|
+
|
|
795
|
+
# Method 1: Try importlib.resources for package data (Python 3.9+)
|
|
796
|
+
try:
|
|
797
|
+
try:
|
|
798
|
+
# Try to access the package resources
|
|
799
|
+
pkg_files = resources.files("llms")
|
|
800
|
+
# Check if ui directory exists in package resources
|
|
801
|
+
if hasattr(pkg_files, 'is_dir') and (pkg_files / "ui").is_dir():
|
|
802
|
+
_log(f"RESOURCE ROOT (package): {pkg_files}")
|
|
803
|
+
return pkg_files
|
|
804
|
+
except (FileNotFoundError, AttributeError, TypeError):
|
|
805
|
+
# Package doesn't have the resources, try other methods
|
|
806
|
+
pass
|
|
807
|
+
except ImportError:
|
|
808
|
+
# importlib.resources not available (Python < 3.9)
|
|
809
|
+
pass
|
|
810
|
+
|
|
811
|
+
# Method 2: Try to find data files in sys.prefix (where data_files are installed)
|
|
812
|
+
# Get all possible installation directories
|
|
813
|
+
possible_roots = [
|
|
814
|
+
Path(sys.prefix), # Standard installation
|
|
815
|
+
Path(sys.prefix) / "share", # Some distributions
|
|
816
|
+
Path(sys.base_prefix), # Virtual environments
|
|
817
|
+
Path(sys.base_prefix) / "share",
|
|
818
|
+
]
|
|
819
|
+
|
|
820
|
+
# Add site-packages directories
|
|
821
|
+
for site_dir in site.getsitepackages():
|
|
822
|
+
possible_roots.extend([
|
|
823
|
+
Path(site_dir),
|
|
824
|
+
Path(site_dir).parent,
|
|
825
|
+
Path(site_dir).parent / "share",
|
|
826
|
+
])
|
|
827
|
+
|
|
828
|
+
# Add user site directory
|
|
829
|
+
try:
|
|
830
|
+
user_site = site.getusersitepackages()
|
|
831
|
+
if user_site:
|
|
832
|
+
possible_roots.extend([
|
|
833
|
+
Path(user_site),
|
|
834
|
+
Path(user_site).parent,
|
|
835
|
+
Path(user_site).parent / "share",
|
|
836
|
+
])
|
|
837
|
+
except AttributeError:
|
|
838
|
+
pass
|
|
839
|
+
|
|
840
|
+
for root in possible_roots:
|
|
841
|
+
try:
|
|
842
|
+
if root.exists() and (root / "index.html").exists() and (root / "ui").is_dir():
|
|
843
|
+
_log(f"RESOURCE ROOT (data files): {root}")
|
|
844
|
+
return root
|
|
845
|
+
except (OSError, PermissionError):
|
|
846
|
+
continue
|
|
847
|
+
|
|
848
|
+
# Method 3: Development mode - look relative to this file
|
|
849
|
+
# __file__ is *this* module; look in same directory first, then parent
|
|
850
|
+
dev_roots = [
|
|
851
|
+
Path(__file__).resolve().parent, # Same directory as llms.py
|
|
852
|
+
Path(__file__).resolve().parent.parent, # Parent directory (repo root)
|
|
853
|
+
]
|
|
854
|
+
|
|
855
|
+
for root in dev_roots:
|
|
856
|
+
try:
|
|
857
|
+
if (root / "index.html").exists() and (root / "ui").is_dir():
|
|
858
|
+
_log(f"RESOURCE ROOT (development): {root}")
|
|
859
|
+
return root
|
|
860
|
+
except (OSError, PermissionError):
|
|
861
|
+
continue
|
|
862
|
+
|
|
863
|
+
# Fallback: use the directory containing this file
|
|
864
|
+
from_file = Path(__file__).resolve().parent
|
|
865
|
+
_log(f"RESOURCE ROOT (fallback): {from_file}")
|
|
866
|
+
return from_file
|
|
867
|
+
|
|
868
|
+
def resource_exists(resource_path):
|
|
869
|
+
# Check if resource files exist (handle both Path and Traversable objects)
|
|
870
|
+
try:
|
|
871
|
+
if hasattr(resource_path, 'is_file'):
|
|
872
|
+
return resource_path.is_file()
|
|
873
|
+
else:
|
|
874
|
+
return os.path.exists(resource_path)
|
|
875
|
+
except (OSError, AttributeError):
|
|
876
|
+
pass
|
|
877
|
+
|
|
878
|
+
def read_resource_text(resource_path):
|
|
879
|
+
if hasattr(resource_path, 'read_text'):
|
|
880
|
+
return resource_path.read_text()
|
|
881
|
+
else:
|
|
882
|
+
with open(resource_path, "r") as f:
|
|
883
|
+
return f.read()
|
|
884
|
+
|
|
885
|
+
def read_resource_file_bytes(resource_file):
|
|
886
|
+
try:
|
|
887
|
+
if hasattr(_ROOT, 'joinpath'):
|
|
888
|
+
# importlib.resources Traversable
|
|
889
|
+
index_resource = _ROOT.joinpath(resource_file)
|
|
890
|
+
if index_resource.is_file():
|
|
891
|
+
return index_resource.read_bytes()
|
|
892
|
+
else:
|
|
893
|
+
# Regular Path object
|
|
894
|
+
index_path = _ROOT / resource_file
|
|
895
|
+
if index_path.exists():
|
|
896
|
+
return index_path.read_bytes()
|
|
897
|
+
except (OSError, PermissionError, AttributeError) as e:
|
|
898
|
+
_log(f"Error reading resource bytes: {e}")
|
|
788
899
|
|
|
789
900
|
def main():
|
|
790
|
-
global g_verbose, g_default_model, g_logprefix, g_config_path, g_ui_path
|
|
901
|
+
global _ROOT, g_verbose, g_default_model, g_logprefix, g_config_path, g_ui_path
|
|
791
902
|
|
|
792
903
|
parser = argparse.ArgumentParser(description=f"llms v{VERSION}")
|
|
793
904
|
parser.add_argument('--config', default=None, help='Path to config file', metavar='FILE')
|
|
@@ -827,20 +938,14 @@ def main():
|
|
|
827
938
|
if cli_args.config is not None:
|
|
828
939
|
g_config_path = os.path.join(os.path.dirname(__file__), cli_args.config)
|
|
829
940
|
|
|
830
|
-
|
|
831
|
-
from importlib.resources import files
|
|
832
|
-
_ROOT = files("llms")
|
|
833
|
-
_log(f"RESOURCE ROOT: {_ROOT}")
|
|
834
|
-
except ModuleNotFoundError:
|
|
835
|
-
# package not installed
|
|
836
|
-
# __file__ is *this* module; climb two levels to repo root
|
|
837
|
-
_ROOT = Path(__file__).resolve().parent.parent / "llms"
|
|
838
|
-
_log(f"__file__: {Path(__file__).resolve()}")
|
|
839
|
-
_log(f"PATH ROOT: {_ROOT}")
|
|
840
|
-
|
|
941
|
+
_ROOT = resolve_root()
|
|
841
942
|
if cli_args.root:
|
|
842
943
|
_ROOT = Path(cli_args.root)
|
|
843
944
|
|
|
945
|
+
if not _ROOT:
|
|
946
|
+
print("Resource root not found")
|
|
947
|
+
exit(1)
|
|
948
|
+
|
|
844
949
|
g_config_path = os.path.join(os.path.dirname(__file__), cli_args.config) if cli_args.config else get_config_path()
|
|
845
950
|
g_ui_path = get_ui_path()
|
|
846
951
|
|
|
@@ -865,21 +970,32 @@ def main():
|
|
|
865
970
|
|
|
866
971
|
if not g_config_path or not os.path.exists(g_config_path):
|
|
867
972
|
# copy llms.json and ui.json to llms_home
|
|
868
|
-
|
|
973
|
+
|
|
974
|
+
if not os.path.exists(home_config_path) and resource_exists(resource_config_path):
|
|
869
975
|
llms_home = os.path.dirname(home_config_path)
|
|
870
976
|
os.makedirs(llms_home, exist_ok=True)
|
|
871
|
-
|
|
872
|
-
|
|
977
|
+
|
|
978
|
+
# Read config from resource (handle both Path and Traversable objects)
|
|
979
|
+
try:
|
|
980
|
+
config_json = read_resource_text(resource_config_path)
|
|
873
981
|
with open(home_config_path, "w") as f:
|
|
874
982
|
f.write(config_json)
|
|
875
983
|
_log(f"Created default config at {home_config_path}")
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
984
|
+
except (OSError, AttributeError) as e:
|
|
985
|
+
_log(f"Error reading resource config: {e}")
|
|
986
|
+
|
|
987
|
+
# Read UI config from resource
|
|
988
|
+
if not os.path.exists(home_ui_path) and resource_exists(resource_ui_path):
|
|
989
|
+
try:
|
|
990
|
+
ui_json = read_resource_text(resource_ui_path)
|
|
880
991
|
with open(home_ui_path, "w") as f:
|
|
881
992
|
f.write(ui_json)
|
|
882
993
|
_log(f"Created default ui config at {home_ui_path}")
|
|
994
|
+
except (OSError, AttributeError) as e:
|
|
995
|
+
_log(f"Error reading resource ui config: {e}")
|
|
996
|
+
|
|
997
|
+
# Update g_config_path to point to the copied file
|
|
998
|
+
g_config_path = home_config_path
|
|
883
999
|
else:
|
|
884
1000
|
print("Config file not found. Create one with --init or use --config <path>")
|
|
885
1001
|
exit(1)
|
|
@@ -964,17 +1080,32 @@ def main():
|
|
|
964
1080
|
|
|
965
1081
|
async def ui_static(request: web.Request) -> web.Response:
|
|
966
1082
|
path = Path(request.match_info["path"])
|
|
967
|
-
|
|
968
|
-
if not resource.is_file():
|
|
969
|
-
raise web.HTTPNotFound
|
|
1083
|
+
|
|
970
1084
|
try:
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1085
|
+
# Handle both Path objects and importlib.resources Traversable objects
|
|
1086
|
+
if hasattr(_ROOT, 'joinpath'):
|
|
1087
|
+
# importlib.resources Traversable
|
|
1088
|
+
resource = _ROOT.joinpath("ui").joinpath(str(path))
|
|
1089
|
+
if not resource.is_file():
|
|
1090
|
+
raise web.HTTPNotFound
|
|
1091
|
+
content = resource.read_bytes()
|
|
1092
|
+
else:
|
|
1093
|
+
# Regular Path object
|
|
1094
|
+
resource = _ROOT / "ui" / path
|
|
1095
|
+
if not resource.is_file():
|
|
1096
|
+
raise web.HTTPNotFound
|
|
1097
|
+
try:
|
|
1098
|
+
resource.relative_to(Path(_ROOT)) # basic directory-traversal guard
|
|
1099
|
+
except ValueError:
|
|
1100
|
+
raise web.HTTPBadRequest(text="Invalid path")
|
|
1101
|
+
content = resource.read_bytes()
|
|
1102
|
+
|
|
1103
|
+
content_type, _ = mimetypes.guess_type(str(path))
|
|
1104
|
+
if content_type is None:
|
|
1105
|
+
content_type = "application/octet-stream"
|
|
1106
|
+
return web.Response(body=content, content_type=content_type)
|
|
1107
|
+
except (OSError, PermissionError, AttributeError):
|
|
1108
|
+
raise web.HTTPNotFound
|
|
978
1109
|
|
|
979
1110
|
app.router.add_get("/ui/{path:.*}", ui_static, name="ui_static")
|
|
980
1111
|
|
|
@@ -983,16 +1114,15 @@ def main():
|
|
|
983
1114
|
app.router.add_get('/favicon.ico', not_found_handler)
|
|
984
1115
|
|
|
985
1116
|
# Serve index.html from root
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
app.router.add_route('*', '/{tail:.*}', fallback_route_handler)
|
|
1117
|
+
async def index_handler(request):
|
|
1118
|
+
index_content = read_resource_file_bytes("index.html")
|
|
1119
|
+
if index_content is None:
|
|
1120
|
+
raise web.HTTPNotFound
|
|
1121
|
+
return web.Response(body=index_content, content_type='text/html')
|
|
1122
|
+
app.router.add_get('/', index_handler)
|
|
1123
|
+
|
|
1124
|
+
# Serve index.html as fallback route (SPA routing)
|
|
1125
|
+
app.router.add_route('*', '/{tail:.*}', index_handler)
|
|
996
1126
|
|
|
997
1127
|
if os.path.exists(g_ui_path):
|
|
998
1128
|
async def ui_json_handler(request):
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "llms-py"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.5"
|
|
8
8
|
description = "A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "BSD-3-Clause"
|
|
@@ -16,7 +16,7 @@ with open(os.path.join(this_directory, "requirements.txt"), encoding="utf-8") as
|
|
|
16
16
|
|
|
17
17
|
setup(
|
|
18
18
|
name="llms-py",
|
|
19
|
-
version="2.0.
|
|
19
|
+
version="2.0.5",
|
|
20
20
|
author="ServiceStack",
|
|
21
21
|
author_email="team@servicestack.net",
|
|
22
22
|
description="A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|