llms-py 3.0.10__py3-none-any.whl → 3.0.18__py3-none-any.whl

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.
Files changed (41) hide show
  1. llms/extensions/app/__init__.py +0 -1
  2. llms/extensions/app/db.py +7 -3
  3. llms/extensions/app/ui/threadStore.mjs +10 -3
  4. llms/extensions/computer/README.md +96 -0
  5. llms/extensions/computer/__init__.py +59 -0
  6. llms/extensions/computer/base.py +80 -0
  7. llms/extensions/computer/bash.py +185 -0
  8. llms/extensions/computer/computer.py +523 -0
  9. llms/extensions/computer/edit.py +299 -0
  10. llms/extensions/computer/filesystem.py +542 -0
  11. llms/extensions/computer/platform.py +461 -0
  12. llms/extensions/computer/run.py +37 -0
  13. llms/extensions/core_tools/__init__.py +0 -38
  14. llms/extensions/providers/anthropic.py +28 -1
  15. llms/extensions/providers/cerebras.py +0 -1
  16. llms/extensions/providers/google.py +112 -34
  17. llms/extensions/skills/LICENSE +202 -0
  18. llms/extensions/skills/__init__.py +130 -0
  19. llms/extensions/skills/errors.py +25 -0
  20. llms/extensions/skills/models.py +39 -0
  21. llms/extensions/skills/parser.py +178 -0
  22. llms/extensions/skills/ui/index.mjs +376 -0
  23. llms/extensions/skills/ui/skills/create-plan/SKILL.md +74 -0
  24. llms/extensions/skills/validator.py +177 -0
  25. llms/extensions/system_prompts/ui/index.mjs +6 -10
  26. llms/extensions/tools/__init__.py +5 -82
  27. llms/extensions/tools/ui/index.mjs +194 -63
  28. llms/main.py +502 -146
  29. llms/ui/ai.mjs +1 -1
  30. llms/ui/app.css +530 -0
  31. llms/ui/ctx.mjs +53 -6
  32. llms/ui/modules/chat/ChatBody.mjs +200 -20
  33. llms/ui/modules/chat/index.mjs +108 -104
  34. llms/ui/tailwind.input.css +10 -0
  35. llms/ui/utils.mjs +25 -1
  36. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/METADATA +2 -2
  37. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/RECORD +41 -24
  38. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/WHEEL +1 -1
  39. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/entry_points.txt +0 -0
  40. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/licenses/LICENSE +0 -0
  41. {llms_py-3.0.10.dist-info → llms_py-3.0.18.dist-info}/top_level.txt +0 -0
llms/main.py CHANGED
@@ -18,17 +18,33 @@ import mimetypes
18
18
  import os
19
19
  import re
20
20
  import secrets
21
+ import shlex
21
22
  import shutil
22
23
  import site
23
24
  import subprocess
24
25
  import sys
26
+ import tempfile
25
27
  import time
26
28
  import traceback
27
29
  from datetime import datetime
30
+ from enum import Enum, IntEnum
28
31
  from importlib import resources # Py≥3.9 (pip install importlib_resources for 3.7/3.8)
29
32
  from io import BytesIO
30
33
  from pathlib import Path
31
- from typing import Optional, get_type_hints
34
+ from typing import (
35
+ Annotated,
36
+ Any,
37
+ Callable,
38
+ Dict,
39
+ List,
40
+ Literal,
41
+ Optional,
42
+ Tuple,
43
+ Union,
44
+ get_args,
45
+ get_origin,
46
+ get_type_hints,
47
+ )
32
48
  from urllib.parse import parse_qs, urlencode, urljoin
33
49
 
34
50
  import aiohttp
@@ -41,7 +57,7 @@ try:
41
57
  except ImportError:
42
58
  HAS_PIL = False
43
59
 
44
- VERSION = "3.0.10"
60
+ VERSION = "3.0.18"
45
61
  _ROOT = None
46
62
  DEBUG = os.getenv("DEBUG") == "1"
47
63
  MOCK = os.getenv("MOCK") == "1"
@@ -59,6 +75,12 @@ g_oauth_states = {} # CSRF protection: {state: {created, redirect_uri}}
59
75
  g_app = None # ExtensionsContext Singleton
60
76
 
61
77
 
78
+ class ExitCode(IntEnum):
79
+ SUCCESS = 0
80
+ FAILED = 1
81
+ UNHANDLED = 9
82
+
83
+
62
84
  def _log(message):
63
85
  if g_verbose:
64
86
  print(f"{g_logprefix}{message}", flush=True)
@@ -340,22 +362,102 @@ def to_content(result):
340
362
  return str(result)
341
363
 
342
364
 
365
+ def get_literal_values(typ):
366
+ """Recursively extract values from Literal and Union types."""
367
+ origin = get_origin(typ)
368
+ if origin is Literal:
369
+ return list(get_args(typ))
370
+ elif origin is Union:
371
+ values = []
372
+ for arg in get_args(typ):
373
+ # Recurse for nested Unions or Literals
374
+ nested_values = get_literal_values(arg)
375
+ if nested_values:
376
+ for v in nested_values:
377
+ if v not in values:
378
+ values.append(v)
379
+ return values
380
+ return None
381
+
382
+
383
+ def _py_type_to_json_type(param_type):
384
+ param_type_name = "string"
385
+ enum_values = None
386
+ items = None
387
+
388
+ # Check for Enum
389
+ if inspect.isclass(param_type) and issubclass(param_type, Enum):
390
+ enum_values = [e.value for e in param_type]
391
+ elif get_origin(param_type) is list or get_origin(param_type) is list:
392
+ param_type_name = "array"
393
+ args = get_args(param_type)
394
+ if args:
395
+ items_type, _, _ = _py_type_to_json_type(args[0])
396
+ items = {"type": items_type}
397
+ elif get_origin(param_type) is dict:
398
+ param_type_name = "object"
399
+ else:
400
+ # Check for Literal / Union[Literal]
401
+ enum_values = get_literal_values(param_type)
402
+
403
+ if enum_values:
404
+ # Infer type from the first value
405
+ value_type = type(enum_values[0])
406
+ if value_type is int:
407
+ param_type_name = "integer"
408
+ elif value_type is float:
409
+ param_type_name = "number"
410
+ elif value_type is bool:
411
+ param_type_name = "boolean"
412
+
413
+ elif param_type is int:
414
+ param_type_name = "integer"
415
+ elif param_type is float:
416
+ param_type_name = "number"
417
+ elif param_type is bool:
418
+ param_type_name = "boolean"
419
+
420
+ return param_type_name, enum_values, items
421
+
422
+
343
423
  def function_to_tool_definition(func):
344
- type_hints = get_type_hints(func)
424
+ type_hints = get_type_hints(func, include_extras=True)
345
425
  signature = inspect.signature(func)
346
426
  parameters = {"type": "object", "properties": {}, "required": []}
347
427
 
348
428
  for name, param in signature.parameters.items():
349
429
  param_type = type_hints.get(name, str)
350
- param_type_name = "string"
351
- if param_type is int:
352
- param_type_name = "integer"
353
- elif param_type is float:
354
- param_type_name = "number"
355
- elif param_type is bool:
356
- param_type_name = "boolean"
430
+ description = None
431
+
432
+ # Check for Annotated (for description)
433
+ if get_origin(param_type) is Annotated:
434
+ args = get_args(param_type)
435
+ param_type = args[0]
436
+ for arg in args[1:]:
437
+ if isinstance(arg, str):
438
+ description = arg
439
+ break
440
+
441
+ # Unwrap Optional / Union[T, None]
442
+ origin = get_origin(param_type)
443
+ if origin is Union:
444
+ args = get_args(param_type)
445
+ # Filter out NoneType
446
+ non_none_args = [arg for arg in args if arg is not type(None)]
447
+ if len(non_none_args) == 1:
448
+ param_type = non_none_args[0]
449
+
450
+ param_type_name, enum_values, items = _py_type_to_json_type(param_type)
451
+
452
+ prop = {"type": param_type_name}
453
+ if description:
454
+ prop["description"] = description
455
+ if enum_values:
456
+ prop["enum"] = enum_values
457
+ if items:
458
+ prop["items"] = items
459
+ parameters["properties"][name] = prop
357
460
 
358
- parameters["properties"][name] = {"type": param_type_name}
359
461
  if param.default == inspect.Parameter.empty:
360
462
  parameters["required"].append(name)
361
463
 
@@ -363,7 +465,7 @@ def function_to_tool_definition(func):
363
465
  "type": "function",
364
466
  "function": {
365
467
  "name": func.__name__,
366
- "description": func.__doc__ or "",
468
+ "description": (func.__doc__ or "").strip(),
367
469
  "parameters": parameters,
368
470
  },
369
471
  }
@@ -783,7 +885,7 @@ def chat_to_prompt(chat):
783
885
  prompt = ""
784
886
  if "messages" in chat:
785
887
  for message in chat["messages"]:
786
- if message["role"] == "user":
888
+ if message.get("role") == "user":
787
889
  # if content is string
788
890
  if isinstance(message["content"], str):
789
891
  if prompt:
@@ -802,7 +904,7 @@ def chat_to_prompt(chat):
802
904
  def chat_to_system_prompt(chat):
803
905
  if "messages" in chat:
804
906
  for message in chat["messages"]:
805
- if message["role"] == "system":
907
+ if message.get("role") == "system":
806
908
  # if content is string
807
909
  if isinstance(message["content"], str):
808
910
  return message["content"]
@@ -830,7 +932,7 @@ def last_user_prompt(chat):
830
932
  prompt = ""
831
933
  if "messages" in chat:
832
934
  for message in chat["messages"]:
833
- if message["role"] == "user":
935
+ if message.get("role") == "user":
834
936
  # if content is string
835
937
  if isinstance(message["content"], str):
836
938
  prompt = message["content"]
@@ -1114,8 +1216,8 @@ class OpenAiCompatible:
1114
1216
  def chat_summary(self, chat):
1115
1217
  return chat_summary(chat)
1116
1218
 
1117
- def process_chat(self, chat, provider_id=None):
1118
- return process_chat(chat, provider_id)
1219
+ async def process_chat(self, chat, provider_id=None):
1220
+ return await process_chat(chat, provider_id)
1119
1221
 
1120
1222
  async def chat(self, chat, context=None):
1121
1223
  chat["model"] = self.provider_model(chat["model"]) or chat["model"]
@@ -1170,7 +1272,7 @@ class OpenAiCompatible:
1170
1272
  if self.enable_thinking is not None:
1171
1273
  chat["enable_thinking"] = self.enable_thinking
1172
1274
 
1173
- chat = await process_chat(chat, provider_id=self.id)
1275
+ chat = await self.process_chat(chat, provider_id=self.id)
1174
1276
  _log(f"POST {self.chat_url}")
1175
1277
  _log(chat_summary(chat))
1176
1278
  # remove metadata if any (conflicts with some providers, e.g. Z.ai)
@@ -1202,6 +1304,15 @@ class GroqProvider(OpenAiCompatible):
1202
1304
  kwargs["api"] = "https://api.groq.com/openai/v1"
1203
1305
  super().__init__(**kwargs)
1204
1306
 
1307
+ async def process_chat(self, chat, provider_id=None):
1308
+ ret = await process_chat(chat, provider_id)
1309
+ chat.pop("modalities", None) # groq doesn't support modalities
1310
+ messages = chat.get("messages", []).copy()
1311
+ for message in messages:
1312
+ message.pop("timestamp", None) # groq doesn't support timestamp
1313
+ ret["messages"] = messages
1314
+ return ret
1315
+
1205
1316
 
1206
1317
  class XaiProvider(OpenAiCompatible):
1207
1318
  sdk = "@ai-sdk/xai"
@@ -1505,6 +1616,7 @@ def g_tool_result(result, function_name: Optional[str] = None, function_args: Op
1505
1616
  content = []
1506
1617
  resources = []
1507
1618
  args = function_args or {}
1619
+ _dbg(f"{function_name} tool result type: {type(result)}")
1508
1620
  if isinstance(result, dict):
1509
1621
  text, res = tool_result_part(result, function_name, args)
1510
1622
  if text:
@@ -1525,9 +1637,118 @@ def g_tool_result(result, function_name: Optional[str] = None, function_args: Op
1525
1637
  return text, resources
1526
1638
 
1527
1639
 
1640
+ def convert_tool_args(function_name, function_args):
1641
+ """
1642
+ Convert tool arg values to their specified types.
1643
+ types: string, number, integer, boolean, object, array, null
1644
+ example prop_def = [
1645
+ {
1646
+ "type": "string"
1647
+ },
1648
+ {
1649
+ "default": "name",
1650
+ "type": "string",
1651
+ "enum": ["name", "size"]
1652
+ },
1653
+ {
1654
+ "default": [],
1655
+ "type": "array",
1656
+ "items": {
1657
+ "type": "string"
1658
+ }
1659
+ },
1660
+ {
1661
+ "anyOf": [
1662
+ {
1663
+ "type": "string"
1664
+ },
1665
+ {
1666
+ "type": "null"
1667
+ }
1668
+ ],
1669
+ "default": null,
1670
+ },
1671
+ ]
1672
+ """
1673
+ tool_def = g_app.get_tool_definition(function_name)
1674
+ if not tool_def:
1675
+ return function_args
1676
+
1677
+ if "function" in tool_def and "parameters" in tool_def["function"]:
1678
+ parameters = tool_def.get("function", {}).get("parameters")
1679
+ properties = parameters.get("properties", {})
1680
+ required = parameters.get("required", [])
1681
+ new_args = function_args.copy()
1682
+
1683
+ for key, value in function_args.items():
1684
+ if key in properties and isinstance(value, str):
1685
+ prop_type = properties[key].get("type")
1686
+ str_val = value.strip()
1687
+
1688
+ if str_val == "":
1689
+ if prop_type in ("integer", "number"):
1690
+ new_args[key] = None
1691
+ else:
1692
+ new_args.pop(key)
1693
+ continue
1694
+
1695
+ if prop_type == "integer":
1696
+ with contextlib.suppress(ValueError, TypeError):
1697
+ new_args[key] = int(str_val)
1698
+
1699
+ elif prop_type == "number":
1700
+ with contextlib.suppress(ValueError, TypeError):
1701
+ new_args[key] = float(str_val)
1702
+
1703
+ elif prop_type == "boolean":
1704
+ lower_val = str_val.lower()
1705
+ if lower_val in ("true", "1", "yes"):
1706
+ new_args[key] = True
1707
+ elif lower_val in ("false", "0", "no"):
1708
+ new_args[key] = False
1709
+
1710
+ elif prop_type == "object":
1711
+ if str_val == "":
1712
+ new_args[key] = None
1713
+ else:
1714
+ with contextlib.suppress(json.JSONDecodeError, TypeError):
1715
+ new_args[key] = json.loads(str_val)
1716
+
1717
+ elif prop_type == "array":
1718
+ if str_val == "":
1719
+ new_args[key] = []
1720
+ else:
1721
+ # Simple CSV split for arrays; could be more robust with JSON parsing if wrapped in brackets
1722
+ # Check if it looks like a JSON array
1723
+ if str_val.startswith("[") and str_val.endswith("]"):
1724
+ with contextlib.suppress(json.JSONDecodeError):
1725
+ items = json.loads(str_val)
1726
+ else:
1727
+ items = [s.strip() for s in str_val.split(",")]
1728
+ item_type = properties[key].get("items", {}).get("type")
1729
+ if item_type == "integer":
1730
+ items = [int(i) for i in items]
1731
+ elif item_type == "number":
1732
+ items = [float(i) for i in items]
1733
+ new_args[key] = items
1734
+
1735
+ # Validate required parameters
1736
+ missing = [key for key in required if key not in new_args]
1737
+ if missing:
1738
+ raise ValueError(f"Missing required arguments: {', '.join(missing)}")
1739
+
1740
+ return new_args
1741
+
1742
+ return function_args
1743
+
1744
+
1528
1745
  async def g_exec_tool(function_name, function_args):
1746
+ _log(f"g_exec_tool: {function_name}")
1529
1747
  if function_name in g_app.tools:
1530
1748
  try:
1749
+ # Type conversion based on tool definition
1750
+ function_args = convert_tool_args(function_name, function_args)
1751
+
1531
1752
  func = g_app.tools[function_name]
1532
1753
  is_async = inspect.iscoroutinefunction(func)
1533
1754
  _dbg(f"Executing {'async' if is_async else 'sync'} tool '{function_name}' with args: {function_args}")
@@ -1536,7 +1757,7 @@ async def g_exec_tool(function_name, function_args):
1536
1757
  else:
1537
1758
  return g_tool_result(func(**function_args), function_name, function_args)
1538
1759
  except Exception as e:
1539
- return f"Error executing tool '{function_name}': {to_error_message(e)}", None
1760
+ return f"Error executing tool '{function_name}':\n{to_error_message(e)}", None
1540
1761
  return f"Error: Tool '{function_name}' not found", None
1541
1762
 
1542
1763
 
@@ -1547,7 +1768,7 @@ def group_resources(resources: list):
1547
1768
  {"images": [{"type": "image_url", "image_url": {"url": "/image.jpg"}}] }
1548
1769
  """
1549
1770
  grouped = {}
1550
- for res in resources:
1771
+ for res in resources or []:
1551
1772
  type = res.get("type")
1552
1773
  if not type:
1553
1774
  continue
@@ -1576,6 +1797,9 @@ async def g_chat_completion(chat, context=None):
1576
1797
  if context is None:
1577
1798
  context = {"chat": chat, "tools": "all"}
1578
1799
 
1800
+ if "request_id" not in context:
1801
+ context["request_id"] = str(int(time.time() * 1000))
1802
+
1579
1803
  # get first provider that has the model
1580
1804
  candidate_providers = [name for name, provider in g_handlers.items() if provider.provider_model(model)]
1581
1805
  if len(candidate_providers) == 0:
@@ -1620,10 +1844,15 @@ async def g_chat_completion(chat, context=None):
1620
1844
  tool_history = []
1621
1845
  final_response = None
1622
1846
 
1623
- for _ in range(max_iterations):
1847
+ for request_count in range(max_iterations):
1624
1848
  if should_cancel_thread(context):
1625
1849
  return
1626
1850
 
1851
+ if DEBUG:
1852
+ messages = current_chat.get("messages", [])
1853
+ last_message = messages[-1] if messages else None
1854
+ _dbg(f"Provider {provider_name}, request {request_count}:\n{json.dumps(last_message, indent=2)}")
1855
+
1627
1856
  response = await provider.chat(current_chat, context=context)
1628
1857
 
1629
1858
  if should_cancel_thread(context):
@@ -1722,7 +1951,12 @@ async def g_chat_completion(chat, context=None):
1722
1951
  continue
1723
1952
 
1724
1953
  # If we get here, all providers failed
1725
- raise first_exception
1954
+ if first_exception:
1955
+ raise first_exception
1956
+
1957
+ e = Exception("All providers failed")
1958
+ await g_app.on_chat_error(e, context or {"chat": chat})
1959
+ raise e
1726
1960
 
1727
1961
 
1728
1962
  async def cli_chat(chat, tools=None, image=None, audio=None, file=None, args=None, raw=False):
@@ -1853,12 +2087,18 @@ def config_str(key):
1853
2087
  return key in g_config and g_config[key] or None
1854
2088
 
1855
2089
 
1856
- def load_config(config, providers, verbose=None):
2090
+ def load_config(config, providers, verbose=None, debug=None, disable_extensions: List[str] = None):
1857
2091
  global g_config, g_providers, g_verbose
1858
2092
  g_config = config
1859
2093
  g_providers = providers
1860
- if verbose:
2094
+ if verbose is not None:
1861
2095
  g_verbose = verbose
2096
+ if debug is not None:
2097
+ global DEBUG
2098
+ DEBUG = debug
2099
+ if disable_extensions:
2100
+ global DISABLE_EXTENSIONS
2101
+ DISABLE_EXTENSIONS = disable_extensions
1862
2102
 
1863
2103
 
1864
2104
  def init_llms(config, providers):
@@ -2539,7 +2779,7 @@ class AppExtensions:
2539
2779
  APIs extensions can use to extend the app
2540
2780
  """
2541
2781
 
2542
- def __init__(self, cli_args, extra_args):
2782
+ def __init__(self, cli_args: argparse.Namespace, extra_args: Dict[str, Any]):
2543
2783
  self.cli_args = cli_args
2544
2784
  self.extra_args = extra_args
2545
2785
  self.config = None
@@ -2563,6 +2803,7 @@ class AppExtensions:
2563
2803
  self.tool_groups = {}
2564
2804
  self.index_headers = []
2565
2805
  self.index_footers = []
2806
+ self.allowed_directories = []
2566
2807
  self.request_args = {
2567
2808
  "image_config": dict, # e.g. { "aspect_ratio": "1:1" }
2568
2809
  "temperature": float, # e.g: 0.7
@@ -2615,12 +2856,28 @@ class AppExtensions:
2615
2856
  "ctx.mjs": "/ui/ctx.mjs",
2616
2857
  }
2617
2858
 
2618
- def set_config(self, config):
2859
+ def set_config(self, config: Dict[str, Any]):
2619
2860
  self.config = config
2620
2861
  self.auth_enabled = self.config.get("auth", {}).get("enabled", False)
2621
2862
 
2863
+ def set_allowed_directories(
2864
+ self, directories: List[Annotated[str, "List of absolute paths that are allowed to be accessed."]]
2865
+ ) -> None:
2866
+ """Set the list of allowed directories."""
2867
+ self.allowed_directories = [os.path.abspath(d) for d in directories]
2868
+
2869
+ def add_allowed_directory(self, path: str) -> None:
2870
+ """Add an allowed directory."""
2871
+ abs_path = os.path.abspath(path)
2872
+ if abs_path not in self.allowed_directories:
2873
+ self.allowed_directories.append(abs_path)
2874
+
2875
+ def get_allowed_directories(self) -> List[str]:
2876
+ """Get the list of allowed directories."""
2877
+ return self.allowed_directories
2878
+
2622
2879
  # Authentication middleware helper
2623
- def check_auth(self, request):
2880
+ def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
2624
2881
  """Check if request is authenticated. Returns (is_authenticated, user_data)"""
2625
2882
  if not self.auth_enabled:
2626
2883
  return True, None
@@ -2639,7 +2896,7 @@ class AppExtensions:
2639
2896
 
2640
2897
  return False, None
2641
2898
 
2642
- def get_session(self, request):
2899
+ def get_session(self, request: web.Request) -> Optional[Dict[str, Any]]:
2643
2900
  session_token = get_session_token(request)
2644
2901
 
2645
2902
  if not session_token or session_token not in g_sessions:
@@ -2648,58 +2905,71 @@ class AppExtensions:
2648
2905
  session_data = g_sessions[session_token]
2649
2906
  return session_data
2650
2907
 
2651
- def get_username(self, request):
2908
+ def get_username(self, request: web.Request) -> Optional[str]:
2652
2909
  session = self.get_session(request)
2653
2910
  if session:
2654
2911
  return session.get("userName")
2655
2912
  return None
2656
2913
 
2657
- def get_user_path(self, username=None):
2914
+ def get_user_path(self, username: Optional[str] = None) -> str:
2658
2915
  if username:
2659
2916
  return home_llms_path(os.path.join("user", username))
2660
2917
  return home_llms_path(os.path.join("user", "default"))
2661
2918
 
2662
- def chat_request(self, template=None, text=None, model=None, system_prompt=None):
2919
+ def get_providers(self) -> Dict[str, Any]:
2920
+ return g_handlers
2921
+
2922
+ def chat_request(
2923
+ self,
2924
+ template: Optional[str] = None,
2925
+ text: Optional[str] = None,
2926
+ model: Optional[str] = None,
2927
+ system_prompt: Optional[str] = None,
2928
+ ) -> Dict[str, Any]:
2663
2929
  return g_chat_request(template=template, text=text, model=model, system_prompt=system_prompt)
2664
2930
 
2665
- async def chat_completion(self, chat, context=None):
2931
+ async def chat_completion(self, chat: Dict[str, Any], context: Optional[Dict[str, Any]] = None) -> Any:
2666
2932
  response = await g_chat_completion(chat, context)
2667
2933
  return response
2668
2934
 
2669
- def on_cache_saved_filters(self, context):
2935
+ def on_cache_saved_filters(self, context: Dict[str, Any]):
2670
2936
  # _log(f"on_cache_saved_filters {len(self.cache_saved_filters)}: {context['url']}")
2671
2937
  for filter_func in self.cache_saved_filters:
2672
2938
  filter_func(context)
2673
2939
 
2674
- async def on_chat_error(self, e, context):
2940
+ async def on_chat_error(self, e: Exception, context: Dict[str, Any]):
2675
2941
  # Apply chat error filters
2676
2942
  if "stackTrace" not in context:
2677
2943
  context["stackTrace"] = traceback.format_exc()
2678
2944
  for filter_func in self.chat_error_filters:
2679
2945
  try:
2680
- await filter_func(e, context)
2946
+ task = filter_func(e, context)
2947
+ if asyncio.isfuture(task):
2948
+ await task
2681
2949
  except Exception as e:
2682
2950
  _err("chat error filter failed", e)
2683
2951
 
2684
- async def on_chat_tool(self, chat, context):
2952
+ async def on_chat_tool(self, chat: Dict[str, Any], context: Dict[str, Any]):
2685
2953
  m_len = len(chat.get("messages", []))
2686
2954
  t_len = len(self.chat_tool_filters)
2687
2955
  _dbg(
2688
- f"on_tool_call for thread {context.get('threadId', None)} with {m_len} {pluralize('message', m_len)}, invoking {t_len} {pluralize('filter', t_len)}:"
2956
+ f"on_tool_call for thread {context.get('threadId')} with {m_len} {pluralize('message', m_len)}, invoking {t_len} {pluralize('filter', t_len)}:"
2689
2957
  )
2690
2958
  for filter_func in self.chat_tool_filters:
2691
2959
  await filter_func(chat, context)
2692
2960
 
2693
- def exit(self, exit_code=0):
2961
+ def shutdown(self):
2694
2962
  if len(self.shutdown_handlers) > 0:
2695
2963
  _dbg(f"running {len(self.shutdown_handlers)} shutdown handlers...")
2696
2964
  for handler in self.shutdown_handlers:
2697
2965
  handler()
2698
2966
 
2967
+ def exit(self, exit_code: int = 0):
2968
+ self.shutdown()
2699
2969
  _dbg(f"exit({exit_code})")
2700
2970
  sys.exit(exit_code)
2701
2971
 
2702
- def create_chat_with_tools(self, chat, use_tools="all"):
2972
+ def create_chat_with_tools(self, chat: Dict[str, Any], use_tools: str = "all") -> Dict[str, Any]:
2703
2973
  # Inject global tools if present
2704
2974
  current_chat = chat.copy()
2705
2975
  tools = current_chat.get("tools")
@@ -2713,6 +2983,11 @@ class AppExtensions:
2713
2983
  if "tools" not in current_chat:
2714
2984
  current_chat["tools"] = []
2715
2985
 
2986
+ _dbg(
2987
+ f"create_chat_with_tools: all_tools:{include_all_tools}, only_tools:{only_tools_list}, chat tools: "
2988
+ + str(len(current_chat["tools"]))
2989
+ )
2990
+
2716
2991
  existing_tools = {t["function"]["name"] for t in current_chat["tools"]}
2717
2992
  for tool_def in self.tool_definitions:
2718
2993
  name = tool_def["function"]["name"]
@@ -2720,6 +2995,12 @@ class AppExtensions:
2720
2995
  current_chat["tools"].append(tool_def)
2721
2996
  return current_chat
2722
2997
 
2998
+ def get_tool_definition(self, name: str) -> Optional[Dict[str, Any]]:
2999
+ for tool_def in self.tool_definitions:
3000
+ if tool_def["function"]["name"] == name:
3001
+ return tool_def
3002
+ return None
3003
+
2723
3004
 
2724
3005
  def handler_name(handler):
2725
3006
  if hasattr(handler, "__name__"):
@@ -2728,7 +3009,7 @@ def handler_name(handler):
2728
3009
 
2729
3010
 
2730
3011
  class ExtensionContext:
2731
- def __init__(self, app, path):
3012
+ def __init__(self, app: AppExtensions, path: str):
2732
3013
  self.app = app
2733
3014
  self.cli_args = app.cli_args
2734
3015
  self.extra_args = app.extra_args
@@ -2746,101 +3027,121 @@ class ExtensionContext:
2746
3027
  self.request_args = app.request_args
2747
3028
  self.disabled = False
2748
3029
 
2749
- def chat_to_prompt(self, chat):
3030
+ def set_allowed_directories(
3031
+ self, directories: List[Annotated[str, "List of absolute paths that are allowed to be accessed."]]
3032
+ ) -> None:
3033
+ """Set the list of allowed directories."""
3034
+ self.app.set_allowed_directories(directories)
3035
+
3036
+ def add_allowed_directory(self, path: str) -> None:
3037
+ """Add an allowed directory."""
3038
+ self.app.add_allowed_directory(path)
3039
+
3040
+ def get_allowed_directories(self) -> List[str]:
3041
+ """Get the list of allowed directories."""
3042
+ return self.app.get_allowed_directories()
3043
+
3044
+ def chat_to_prompt(self, chat: Dict[str, Any]) -> str:
2750
3045
  return chat_to_prompt(chat)
2751
3046
 
2752
- def chat_to_system_prompt(self, chat):
3047
+ def chat_to_system_prompt(self, chat: Dict[str, Any]) -> str:
2753
3048
  return chat_to_system_prompt(chat)
2754
3049
 
2755
- def chat_response_to_message(self, response):
3050
+ def chat_response_to_message(self, response: Dict[str, Any]) -> Dict[str, Any]:
2756
3051
  return chat_response_to_message(response)
2757
3052
 
2758
- def last_user_prompt(self, chat):
3053
+ def last_user_prompt(self, chat: Dict[str, Any]) -> str:
2759
3054
  return last_user_prompt(chat)
2760
3055
 
2761
- def to_file_info(self, chat, info=None, response=None):
3056
+ def to_file_info(
3057
+ self, chat: Dict[str, Any], info: Optional[Dict[str, Any]] = None, response: Optional[Dict[str, Any]] = None
3058
+ ) -> Dict[str, Any]:
2762
3059
  return to_file_info(chat, info=info, response=response)
2763
3060
 
2764
- def save_image_to_cache(self, base64_data, filename, image_info, ignore_info=False):
3061
+ def save_image_to_cache(
3062
+ self, base64_data: Union[str, bytes], filename: str, image_info: Dict[str, Any], ignore_info: bool = False
3063
+ ) -> Tuple[str, Optional[Dict[str, Any]]]:
2765
3064
  return save_image_to_cache(base64_data, filename, image_info, ignore_info=ignore_info)
2766
3065
 
2767
- def save_bytes_to_cache(self, bytes_data, filename, file_info):
3066
+ def save_bytes_to_cache(
3067
+ self, bytes_data: Union[str, bytes], filename: str, file_info: Optional[Dict[str, Any]]
3068
+ ) -> Tuple[str, Optional[Dict[str, Any]]]:
2768
3069
  return save_bytes_to_cache(bytes_data, filename, file_info)
2769
3070
 
2770
- def text_from_file(self, path):
3071
+ def text_from_file(self, path: str) -> str:
2771
3072
  return text_from_file(path)
2772
3073
 
2773
- def json_from_file(self, path):
3074
+ def json_from_file(self, path: str) -> Any:
2774
3075
  return json_from_file(path)
2775
3076
 
2776
- def download_file(self, url):
3077
+ def download_file(self, url: str) -> Tuple[bytes, Dict[str, Any]]:
2777
3078
  return download_file(url)
2778
3079
 
2779
- def session_download_file(self, session, url):
3080
+ def session_download_file(self, session: aiohttp.ClientSession, url: str) -> Tuple[bytes, Dict[str, Any]]:
2780
3081
  return session_download_file(session, url)
2781
3082
 
2782
- def read_binary_file(self, url):
3083
+ def read_binary_file(self, url: str) -> Tuple[bytes, Dict[str, Any]]:
2783
3084
  return read_binary_file(url)
2784
3085
 
2785
- def log(self, message):
3086
+ def log(self, message: Any):
2786
3087
  if self.verbose:
2787
3088
  print(f"[{self.name}] {message}", flush=True)
2788
3089
  return message
2789
3090
 
2790
- def log_json(self, obj):
3091
+ def log_json(self, obj: Any):
2791
3092
  if self.verbose:
2792
3093
  print(f"[{self.name}] {json.dumps(obj, indent=2)}", flush=True)
2793
3094
  return obj
2794
3095
 
2795
- def dbg(self, message):
3096
+ def dbg(self, message: Any):
2796
3097
  if self.debug:
2797
3098
  print(f"DEBUG [{self.name}]: {message}", flush=True)
2798
3099
 
2799
- def err(self, message, e):
3100
+ def err(self, message: str, e: Exception):
2800
3101
  print(f"ERROR [{self.name}]: {message}", e)
2801
3102
  if self.verbose:
2802
3103
  print(traceback.format_exc(), flush=True)
2803
3104
 
2804
- def error_message(self, e):
3105
+ def error_message(self, e: Exception) -> str:
2805
3106
  return to_error_message(e)
2806
3107
 
2807
- def error_response(self, e, stacktrace=False):
3108
+ def error_response(self, e: Exception, stacktrace: bool = False) -> Dict[str, Any]:
2808
3109
  return to_error_response(e, stacktrace=stacktrace)
2809
3110
 
2810
- def add_provider(self, provider):
3111
+ def add_provider(self, provider: Any):
2811
3112
  self.log(f"Registered provider: {provider.__name__}")
2812
3113
  self.app.all_providers.append(provider)
2813
3114
 
2814
- def register_ui_extension(self, index):
3115
+ def register_ui_extension(self, index: str):
2815
3116
  path = os.path.join(self.ext_prefix, index)
2816
3117
  self.log(f"Registered UI extension: {path}")
2817
3118
  self.app.ui_extensions.append({"id": self.name, "path": path})
2818
3119
 
2819
- def register_chat_request_filter(self, handler):
3120
+ def register_chat_request_filter(self, handler: Callable):
2820
3121
  self.log(f"Registered chat request filter: {handler_name(handler)}")
2821
3122
  self.app.chat_request_filters.append(handler)
2822
3123
 
2823
- def register_chat_tool_filter(self, handler):
3124
+ def register_chat_tool_filter(self, handler: Callable):
2824
3125
  self.log(f"Registered chat tool filter: {handler_name(handler)}")
2825
3126
  self.app.chat_tool_filters.append(handler)
2826
3127
 
2827
- def register_chat_response_filter(self, handler):
3128
+ def register_chat_response_filter(self, handler: Callable):
2828
3129
  self.log(f"Registered chat response filter: {handler_name(handler)}")
2829
3130
  self.app.chat_response_filters.append(handler)
2830
3131
 
2831
- def register_chat_error_filter(self, handler):
3132
+ def register_chat_error_filter(self, handler: Callable):
2832
3133
  self.log(f"Registered chat error filter: {handler_name(handler)}")
2833
3134
  self.app.chat_error_filters.append(handler)
2834
3135
 
2835
- def register_cache_saved_filter(self, handler):
3136
+ def register_cache_saved_filter(self, handler: Callable):
2836
3137
  self.log(f"Registered cache saved filter: {handler_name(handler)}")
2837
3138
  self.app.cache_saved_filters.append(handler)
2838
3139
 
2839
- def register_shutdown_handler(self, handler):
3140
+ def register_shutdown_handler(self, handler: Callable):
2840
3141
  self.log(f"Registered shutdown handler: {handler_name(handler)}")
2841
3142
  self.app.shutdown_handlers.append(handler)
2842
3143
 
2843
- def add_static_files(self, ext_dir):
3144
+ def add_static_files(self, ext_dir: str):
2844
3145
  self.log(f"Registered static files: {ext_dir}")
2845
3146
 
2846
3147
  async def serve_static(request):
@@ -2852,57 +3153,66 @@ class ExtensionContext:
2852
3153
 
2853
3154
  self.app.server_add_get.append((os.path.join(self.ext_prefix, "{path:.*}"), serve_static, {}))
2854
3155
 
2855
- def web_path(self, method, path):
3156
+ def web_path(self, method: str, path: str) -> str:
2856
3157
  full_path = os.path.join(self.ext_prefix, path) if path else self.ext_prefix
2857
3158
  self.dbg(f"Registered {method:<6} {full_path}")
2858
3159
  return full_path
2859
3160
 
2860
- def add_get(self, path, handler, **kwargs):
3161
+ def add_get(self, path: str, handler: Callable, **kwargs: Any):
2861
3162
  self.app.server_add_get.append((self.web_path("GET", path), handler, kwargs))
2862
3163
 
2863
- def add_post(self, path, handler, **kwargs):
3164
+ def add_post(self, path: str, handler: Callable, **kwargs: Any):
2864
3165
  self.app.server_add_post.append((self.web_path("POST", path), handler, kwargs))
2865
3166
 
2866
- def add_put(self, path, handler, **kwargs):
3167
+ def add_put(self, path: str, handler: Callable, **kwargs: Any):
2867
3168
  self.app.server_add_put.append((self.web_path("PUT", path), handler, kwargs))
2868
3169
 
2869
- def add_delete(self, path, handler, **kwargs):
3170
+ def add_delete(self, path: str, handler: Callable, **kwargs: Any):
2870
3171
  self.app.server_add_delete.append((self.web_path("DELETE", path), handler, kwargs))
2871
3172
 
2872
- def add_patch(self, path, handler, **kwargs):
3173
+ def add_patch(self, path: str, handler: Callable, **kwargs: Any):
2873
3174
  self.app.server_add_patch.append((self.web_path("PATCH", path), handler, kwargs))
2874
3175
 
2875
- def add_importmaps(self, dict):
3176
+ def add_importmaps(self, dict: Dict[str, str]):
2876
3177
  self.app.import_maps.update(dict)
2877
3178
 
2878
- def add_index_header(self, html):
3179
+ def add_index_header(self, html: str):
2879
3180
  self.app.index_headers.append(html)
2880
3181
 
2881
- def add_index_footer(self, html):
3182
+ def add_index_footer(self, html: str):
2882
3183
  self.app.index_footers.append(html)
2883
3184
 
2884
- def get_config(self):
3185
+ def get_home_path(self, name: str = "") -> str:
3186
+ return home_llms_path(name)
3187
+
3188
+ def get_config(self) -> Optional[Dict[str, Any]]:
2885
3189
  return g_config
2886
3190
 
2887
- def get_cache_path(self, path=""):
3191
+ def get_cache_path(self, path: str = "") -> str:
2888
3192
  return get_cache_path(path)
2889
3193
 
2890
- def get_file_mime_type(self, filename):
3194
+ def get_file_mime_type(self, filename: str) -> str:
2891
3195
  return get_file_mime_type(filename)
2892
3196
 
2893
- def chat_request(self, template=None, text=None, model=None, system_prompt=None):
3197
+ def chat_request(
3198
+ self,
3199
+ template: Optional[str] = None,
3200
+ text: Optional[str] = None,
3201
+ model: Optional[str] = None,
3202
+ system_prompt: Optional[str] = None,
3203
+ ) -> Dict[str, Any]:
2894
3204
  return self.app.chat_request(template=template, text=text, model=model, system_prompt=system_prompt)
2895
3205
 
2896
- def chat_completion(self, chat, context=None):
2897
- return self.app.chat_completion(chat, context=context)
3206
+ async def chat_completion(self, chat: Dict[str, Any], context: Optional[Dict[str, Any]] = None) -> Any:
3207
+ return await self.app.chat_completion(chat, context=context)
2898
3208
 
2899
- def get_providers(self):
3209
+ def get_providers(self) -> Dict[str, Any]:
2900
3210
  return g_handlers
2901
3211
 
2902
- def get_provider(self, name):
3212
+ def get_provider(self, name: str) -> Optional[Any]:
2903
3213
  return g_handlers.get(name)
2904
3214
 
2905
- def sanitize_tool_def(self, tool_def):
3215
+ def sanitize_tool_def(self, tool_def: Dict[str, Any]) -> Dict[str, Any]:
2906
3216
  """
2907
3217
  Merge $defs parameter into tool_def property to reduce client/server complexity
2908
3218
  """
@@ -2942,7 +3252,7 @@ class ExtensionContext:
2942
3252
  parameters = func_def.get("parameters", {})
2943
3253
  defs = parameters.get("$defs", {})
2944
3254
  properties = parameters.get("properties", {})
2945
- for prop_name, prop_def in properties.items():
3255
+ for _, prop_def in properties.items():
2946
3256
  if "$ref" in prop_def:
2947
3257
  ref = prop_def["$ref"]
2948
3258
  if ref.startswith("#/$defs/"):
@@ -2954,7 +3264,7 @@ class ExtensionContext:
2954
3264
  del parameters["$defs"]
2955
3265
  return tool_def
2956
3266
 
2957
- def register_tool(self, func, tool_def=None, group=None):
3267
+ def register_tool(self, func: Callable, tool_def: Optional[Dict[str, Any]] = None, group: Optional[str] = None):
2958
3268
  if tool_def is None:
2959
3269
  tool_def = function_to_tool_definition(func)
2960
3270
 
@@ -2976,54 +3286,58 @@ class ExtensionContext:
2976
3286
  self.app.tool_groups[group] = []
2977
3287
  self.app.tool_groups[group].append(name)
2978
3288
 
2979
- def get_tool_definition(self, name):
2980
- for tool_def in self.app.tool_definitions:
2981
- if tool_def["function"]["name"] == name:
2982
- return tool_def
2983
- return None
3289
+ def get_tool_definition(self, name: str) -> Optional[Dict[str, Any]]:
3290
+ return self.app.get_tool_definition(name)
2984
3291
 
2985
- def group_resources(self, resources: list):
3292
+ def group_resources(self, resources: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
2986
3293
  return group_resources(resources)
2987
3294
 
2988
- def check_auth(self, request):
3295
+ def check_auth(self, request: web.Request) -> Tuple[bool, Optional[Dict[str, Any]]]:
2989
3296
  return self.app.check_auth(request)
2990
3297
 
2991
- def get_session(self, request):
3298
+ def get_session(self, request: web.Request) -> Optional[Dict[str, Any]]:
2992
3299
  return self.app.get_session(request)
2993
3300
 
2994
- def get_username(self, request):
3301
+ def get_username(self, request: web.Request) -> Optional[str]:
2995
3302
  return self.app.get_username(request)
2996
3303
 
2997
- def get_user_path(self, username=None):
3304
+ def get_user_path(self, username: Optional[str] = None) -> str:
2998
3305
  return self.app.get_user_path(username)
2999
3306
 
3000
- def context_to_username(self, context):
3307
+ def context_to_username(self, context: Optional[Dict[str, Any]]) -> Optional[str]:
3001
3308
  if context and "request" in context:
3002
3309
  return self.get_username(context["request"])
3003
3310
  return None
3004
3311
 
3005
- def should_cancel_thread(self, context):
3312
+ def should_cancel_thread(self, context: Dict[str, Any]) -> bool:
3006
3313
  return should_cancel_thread(context)
3007
3314
 
3008
- def cache_message_inline_data(self, message):
3315
+ def cache_message_inline_data(self, message: Dict[str, Any]):
3009
3316
  return cache_message_inline_data(message)
3010
3317
 
3011
- async def exec_tool(self, name, args):
3318
+ async def exec_tool(self, name: str, args: Dict[str, Any]) -> Tuple[Optional[str], List[Dict[str, Any]]]:
3012
3319
  return await g_exec_tool(name, args)
3013
3320
 
3014
- def tool_result(self, result, function_name: Optional[str] = None, function_args: Optional[dict] = None):
3321
+ def tool_result(
3322
+ self, result: Any, function_name: Optional[str] = None, function_args: Optional[Dict[str, Any]] = None
3323
+ ) -> Dict[str, Any]:
3015
3324
  return g_tool_result(result, function_name, function_args)
3016
3325
 
3017
- def tool_result_part(self, result: dict, function_name: Optional[str] = None, function_args: Optional[dict] = None):
3326
+ def tool_result_part(
3327
+ self,
3328
+ result: Dict[str, Any],
3329
+ function_name: Optional[str] = None,
3330
+ function_args: Optional[Dict[str, Any]] = None,
3331
+ ) -> Dict[str, Any]:
3018
3332
  return tool_result_part(result, function_name, function_args)
3019
3333
 
3020
- def to_content(self, result):
3334
+ def to_content(self, result: Any) -> str:
3021
3335
  return to_content(result)
3022
3336
 
3023
- def create_chat_with_tools(self, chat, use_tools="all"):
3337
+ def create_chat_with_tools(self, chat: Dict[str, Any], use_tools: str = "all") -> Dict[str, Any]:
3024
3338
  return self.app.create_chat_with_tools(chat, use_tools)
3025
3339
 
3026
- def chat_to_aspect_ratio(self, chat):
3340
+ def chat_to_aspect_ratio(self, chat: Dict[str, Any]) -> str:
3027
3341
  return chat_to_aspect_ratio(chat)
3028
3342
 
3029
3343
 
@@ -3080,7 +3394,21 @@ def get_extensions_dirs():
3080
3394
  return ret
3081
3395
 
3082
3396
 
3397
+ def verify_root_path():
3398
+ global _ROOT
3399
+ _ROOT = os.getenv("LLMS_ROOT", resolve_root())
3400
+ if not _ROOT:
3401
+ print("Resource root not found")
3402
+ exit(1)
3403
+
3404
+
3083
3405
  def init_extensions(parser):
3406
+ """
3407
+ Programmatic entry point for the CLI.
3408
+ Example: cli("ls minimax")
3409
+ """
3410
+ verify_root_path()
3411
+
3084
3412
  """
3085
3413
  Initializes extensions by loading their __init__.py files and calling the __parser__ function if it exists.
3086
3414
  """
@@ -3235,14 +3563,7 @@ def run_extension_cli():
3235
3563
  return False
3236
3564
 
3237
3565
 
3238
- def main():
3239
- global _ROOT, g_verbose, g_default_model, g_logprefix, g_providers, g_config, g_config_path, g_app
3240
-
3241
- _ROOT = os.getenv("LLMS_ROOT", resolve_root())
3242
- if not _ROOT:
3243
- print("Resource root not found")
3244
- exit(1)
3245
-
3566
+ def create_arg_parser():
3246
3567
  parser = argparse.ArgumentParser(description=f"llms v{VERSION}")
3247
3568
  parser.add_argument("--config", default=None, help="Path to config file", metavar="FILE")
3248
3569
  parser.add_argument("--providers", default=None, help="Path to models.dev providers file", metavar="FILE")
@@ -3311,11 +3632,13 @@ def main():
3311
3632
  help="Update an extension (use 'all' to update all extensions)",
3312
3633
  metavar="EXTENSION",
3313
3634
  )
3635
+ return parser
3314
3636
 
3315
- # Load parser extensions, go through all extensions and load their parser arguments
3316
- init_extensions(parser)
3317
3637
 
3318
- cli_args, extra_args = parser.parse_known_args()
3638
+ def cli_exec(cli_args, extra_args):
3639
+ global _ROOT, g_verbose, g_default_model, g_logprefix, g_providers, g_config, g_config_path, g_app
3640
+
3641
+ verify_root_path()
3319
3642
 
3320
3643
  g_app = AppExtensions(cli_args, extra_args)
3321
3644
 
@@ -3351,12 +3674,12 @@ def main():
3351
3674
  else:
3352
3675
  asyncio.run(save_text_url(github_url("providers-extra.json"), home_providers_extra_path))
3353
3676
  print(f"Created default extra providers config at {home_providers_extra_path}")
3354
- exit(0)
3677
+ return ExitCode.SUCCESS
3355
3678
 
3356
3679
  if cli_args.providers:
3357
3680
  if not os.path.exists(cli_args.providers):
3358
3681
  print(f"providers.json not found at {cli_args.providers}")
3359
- exit(1)
3682
+ return ExitCode.FAILED
3360
3683
  g_providers = json.loads(text_from_file(cli_args.providers))
3361
3684
 
3362
3685
  if cli_args.config:
@@ -3385,7 +3708,7 @@ def main():
3385
3708
  if cli_args.update_providers:
3386
3709
  asyncio.run(update_providers(home_providers_path))
3387
3710
  print(f"Updated {home_providers_path}")
3388
- exit(0)
3711
+ return ExitCode.SUCCESS
3389
3712
 
3390
3713
  # if home_providers_path is older than 1 day, update providers list
3391
3714
  if (
@@ -3418,7 +3741,7 @@ def main():
3418
3741
  print(" llms --add <github-user>/<repo>")
3419
3742
 
3420
3743
  asyncio.run(list_extensions())
3421
- exit(0)
3744
+ return ExitCode.SUCCESS
3422
3745
 
3423
3746
  async def install_extension(name):
3424
3747
  # Determine git URL and target directory name
@@ -3481,7 +3804,7 @@ def main():
3481
3804
  os.rmdir(target_path)
3482
3805
 
3483
3806
  asyncio.run(install_extension(cli_args.add))
3484
- exit(0)
3807
+ return ExitCode.SUCCESS
3485
3808
 
3486
3809
  if cli_args.remove is not None:
3487
3810
  if cli_args.remove == "ls":
@@ -3490,11 +3813,11 @@ def main():
3490
3813
  extensions = os.listdir(extensions_path)
3491
3814
  if len(extensions) == 0:
3492
3815
  print("No extensions installed.")
3493
- exit(0)
3816
+ return ExitCode.SUCCESS
3494
3817
  print("Installed extensions:")
3495
3818
  for extension in extensions:
3496
3819
  print(f" {extension}")
3497
- exit(0)
3820
+ return ExitCode.SUCCESS
3498
3821
  # Remove an extension
3499
3822
  extension_name = cli_args.remove
3500
3823
  extensions_path = get_extensions_path()
@@ -3502,7 +3825,7 @@ def main():
3502
3825
 
3503
3826
  if not os.path.exists(target_path):
3504
3827
  print(f"Extension {extension_name} not found at {target_path}")
3505
- exit(1)
3828
+ return ExitCode.FAILED
3506
3829
 
3507
3830
  print(f"Removing extension: {extension_name}...")
3508
3831
  try:
@@ -3510,9 +3833,9 @@ def main():
3510
3833
  print(f"Extension {extension_name} removed successfully.")
3511
3834
  except Exception as e:
3512
3835
  print(f"Failed to remove extension: {e}")
3513
- exit(1)
3836
+ return ExitCode.FAILED
3514
3837
 
3515
- exit(0)
3838
+ return ExitCode.SUCCESS
3516
3839
 
3517
3840
  if cli_args.update:
3518
3841
  if cli_args.update == "ls":
@@ -3521,7 +3844,7 @@ def main():
3521
3844
  extensions = os.listdir(extensions_path)
3522
3845
  if len(extensions) == 0:
3523
3846
  print("No extensions installed.")
3524
- exit(0)
3847
+ return ExitCode.SUCCESS
3525
3848
  print("Installed extensions:")
3526
3849
  for extension in extensions:
3527
3850
  print(f" {extension}")
@@ -3529,7 +3852,7 @@ def main():
3529
3852
  print("\nUsage:")
3530
3853
  print(" llms --update <extension>")
3531
3854
  print(" llms --update all")
3532
- exit(0)
3855
+ return ExitCode.SUCCESS
3533
3856
 
3534
3857
  async def update_extensions(extension_name):
3535
3858
  extensions_path = get_extensions_path()
@@ -3546,7 +3869,11 @@ def main():
3546
3869
  _log(result.stdout.decode("utf-8"))
3547
3870
 
3548
3871
  asyncio.run(update_extensions(cli_args.update))
3549
- exit(0)
3872
+ return ExitCode.SUCCESS
3873
+
3874
+ g_app.add_allowed_directory(home_llms_path(".agent")) # info for agents, e.g: skills
3875
+ g_app.add_allowed_directory(os.getcwd()) # add current directory
3876
+ g_app.add_allowed_directory(tempfile.gettempdir()) # add temp directory
3550
3877
 
3551
3878
  g_app.extensions = install_extensions()
3552
3879
 
@@ -3604,7 +3931,7 @@ def main():
3604
3931
  print(f"\n{model_count} models available from {provider_count} providers")
3605
3932
 
3606
3933
  print_status()
3607
- g_app.exit(0)
3934
+ return ExitCode.SUCCESS
3608
3935
 
3609
3936
  if cli_args.check is not None:
3610
3937
  # Check validity of models for a provider
@@ -3613,7 +3940,7 @@ def main():
3613
3940
  provider_name = cli_args.check
3614
3941
  model_names = extra_args if len(extra_args) > 0 else None
3615
3942
  loop.run_until_complete(check_models(provider_name, model_names))
3616
- g_app.exit(0)
3943
+ return ExitCode.SUCCESS
3617
3944
 
3618
3945
  if cli_args.serve is not None:
3619
3946
  # Disable inactive providers and save to config before starting server
@@ -3658,7 +3985,7 @@ def main():
3658
3985
  print("ERROR: Authentication is enabled but GitHub OAuth is not properly configured.")
3659
3986
  print("Please set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET environment variables,")
3660
3987
  print("or disable authentication by setting 'auth.enabled' to false in llms.json")
3661
- exit(1)
3988
+ return ExitCode.FAILED
3662
3989
 
3663
3990
  _log("Authentication enabled - GitHub OAuth configured")
3664
3991
 
@@ -4252,7 +4579,7 @@ def main():
4252
4579
 
4253
4580
  print(f"Starting server on port {port}...")
4254
4581
  web.run_app(app, host="0.0.0.0", port=port, print=_log)
4255
- g_app.exit(0)
4582
+ return ExitCode.SUCCESS
4256
4583
 
4257
4584
  if cli_args.enable is not None:
4258
4585
  if cli_args.enable.endswith(","):
@@ -4271,7 +4598,7 @@ def main():
4271
4598
  if provider not in g_config["providers"]:
4272
4599
  print(f"Provider '{provider}' not found")
4273
4600
  print(f"Available providers: {', '.join(g_config['providers'].keys())}")
4274
- exit(1)
4601
+ return ExitCode.FAILED
4275
4602
  if provider in g_config["providers"]:
4276
4603
  provider_config, msg = enable_provider(provider)
4277
4604
  print(f"\nEnabled provider {provider}:")
@@ -4282,7 +4609,7 @@ def main():
4282
4609
  print_status()
4283
4610
  if len(msgs) > 0:
4284
4611
  print("\n" + "\n".join(msgs))
4285
- g_app.exit(0)
4612
+ return ExitCode.SUCCESS
4286
4613
 
4287
4614
  if cli_args.disable is not None:
4288
4615
  if cli_args.disable.endswith(","):
@@ -4300,24 +4627,24 @@ def main():
4300
4627
  if provider not in g_config["providers"]:
4301
4628
  print(f"Provider {provider} not found")
4302
4629
  print(f"Available providers: {', '.join(g_config['providers'].keys())}")
4303
- exit(1)
4630
+ return ExitCode.FAILED
4304
4631
  disable_provider(provider)
4305
4632
  print(f"\nDisabled provider {provider}")
4306
4633
 
4307
4634
  print_status()
4308
- g_app.exit(0)
4635
+ return ExitCode.SUCCESS
4309
4636
 
4310
4637
  if cli_args.default is not None:
4311
4638
  default_model = cli_args.default
4312
4639
  provider_model = get_provider_model(default_model)
4313
4640
  if provider_model is None:
4314
4641
  print(f"Model {default_model} not found")
4315
- exit(1)
4642
+ return ExitCode.FAILED
4316
4643
  default_text = g_config["defaults"]["text"]
4317
4644
  default_text["model"] = default_model
4318
4645
  save_config(g_config)
4319
4646
  print(f"\nDefault model set to: {default_model}")
4320
- g_app.exit(0)
4647
+ return ExitCode.SUCCESS
4321
4648
 
4322
4649
  if (
4323
4650
  cli_args.chat is not None
@@ -4339,13 +4666,13 @@ def main():
4339
4666
  template = f"out:{cli_args.out}"
4340
4667
  if template not in g_config["defaults"]:
4341
4668
  print(f"Template for output modality '{cli_args.out}' not found")
4342
- exit(1)
4669
+ return ExitCode.FAILED
4343
4670
  chat = g_config["defaults"][template]
4344
4671
  if cli_args.chat is not None:
4345
4672
  chat_path = os.path.join(os.path.dirname(__file__), cli_args.chat)
4346
4673
  if not os.path.exists(chat_path):
4347
4674
  print(f"Chat request template not found: {chat_path}")
4348
- exit(1)
4675
+ return ExitCode.FAILED
4349
4676
  _log(f"Using chat: {chat_path}")
4350
4677
 
4351
4678
  with open(chat_path) as f:
@@ -4386,19 +4713,48 @@ def main():
4386
4713
  raw=cli_args.raw,
4387
4714
  )
4388
4715
  )
4389
- g_app.exit(0)
4716
+ return ExitCode.SUCCESS
4390
4717
  except Exception as e:
4391
4718
  print(f"{cli_args.logprefix}Error: {e}")
4392
4719
  if cli_args.verbose:
4393
4720
  traceback.print_exc()
4394
- g_app.exit(1)
4721
+ return ExitCode.FAILED
4395
4722
 
4396
4723
  handled = run_extension_cli()
4724
+ return ExitCode.SUCCESS if handled else ExitCode.UNHANDLED
4725
+
4726
+
4727
+ def get_app():
4728
+ return g_app
4397
4729
 
4398
- if not handled:
4730
+
4731
+ def cli(command_line: str):
4732
+ parser = create_arg_parser()
4733
+
4734
+ # Load parser extensions, go through all extensions and load their parser arguments
4735
+ if load_extensions:
4736
+ init_extensions(parser)
4737
+
4738
+ args = shlex.split(command_line)
4739
+ cli_args, extra_args = parser.parse_known_args(args)
4740
+ return cli_exec(cli_args, extra_args)
4741
+
4742
+
4743
+ def main():
4744
+ parser = create_arg_parser()
4745
+
4746
+ # Load parser extensions, go through all extensions and load their parser arguments
4747
+ init_extensions(parser)
4748
+
4749
+ cli_args, extra_args = parser.parse_known_args()
4750
+ exit_code = cli_exec(cli_args, extra_args)
4751
+
4752
+ if exit_code == ExitCode.UNHANDLED:
4399
4753
  # show usage from ArgumentParser
4400
4754
  parser.print_help()
4401
- g_app.exit(0)
4755
+ g_app.exit(0) if g_app else exit(0)
4756
+
4757
+ g_app.exit(exit_code) if g_app else exit(exit_code)
4402
4758
 
4403
4759
 
4404
4760
  if __name__ == "__main__":