llms-py 3.0.0b2__py3-none-any.whl → 3.0.0b4__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.
- llms/__pycache__/main.cpython-314.pyc +0 -0
- llms/index.html +2 -1
- llms/llms.json +50 -17
- llms/main.py +484 -544
- llms/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
- llms/providers/__pycache__/chutes.cpython-314.pyc +0 -0
- llms/providers/__pycache__/google.cpython-314.pyc +0 -0
- llms/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
- llms/providers/__pycache__/openai.cpython-314.pyc +0 -0
- llms/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
- llms/providers/anthropic.py +189 -0
- llms/providers/chutes.py +152 -0
- llms/providers/google.py +306 -0
- llms/providers/nvidia.py +107 -0
- llms/providers/openai.py +159 -0
- llms/providers/openrouter.py +70 -0
- llms/providers-extra.json +356 -0
- llms/providers.json +1 -1
- llms/ui/App.mjs +132 -60
- llms/ui/ai.mjs +76 -10
- llms/ui/app.css +65 -28
- llms/ui/ctx.mjs +196 -0
- llms/ui/index.mjs +75 -171
- llms/ui/lib/charts.mjs +9 -13
- llms/ui/markdown.mjs +6 -0
- llms/ui/{Analytics.mjs → modules/analytics.mjs} +76 -64
- llms/ui/{Main.mjs → modules/chat/ChatBody.mjs} +59 -135
- llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +8 -8
- llms/ui/{ChatPrompt.mjs → modules/chat/index.mjs} +242 -46
- llms/ui/modules/layout.mjs +267 -0
- llms/ui/modules/model-selector.mjs +851 -0
- llms/ui/{Recents.mjs → modules/threads/Recents.mjs} +0 -2
- llms/ui/{Sidebar.mjs → modules/threads/index.mjs} +46 -44
- llms/ui/{threadStore.mjs → modules/threads/threadStore.mjs} +10 -7
- llms/ui/utils.mjs +82 -123
- {llms_py-3.0.0b2.dist-info → llms_py-3.0.0b4.dist-info}/METADATA +1 -1
- llms_py-3.0.0b4.dist-info/RECORD +65 -0
- llms/ui/Avatar.mjs +0 -86
- llms/ui/Brand.mjs +0 -52
- llms/ui/OAuthSignIn.mjs +0 -61
- llms/ui/ProviderIcon.mjs +0 -36
- llms/ui/ProviderStatus.mjs +0 -104
- llms/ui/SignIn.mjs +0 -65
- llms/ui/Welcome.mjs +0 -8
- llms/ui/model-selector.mjs +0 -686
- llms/ui.json +0 -1069
- llms_py-3.0.0b2.dist-info/RECORD +0 -58
- {llms_py-3.0.0b2.dist-info → llms_py-3.0.0b4.dist-info}/WHEEL +0 -0
- {llms_py-3.0.0b2.dist-info → llms_py-3.0.0b4.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.0b2.dist-info → llms_py-3.0.0b4.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.0b2.dist-info → llms_py-3.0.0b4.dist-info}/top_level.txt +0 -0
llms/main.py
CHANGED
|
@@ -38,11 +38,13 @@ try:
|
|
|
38
38
|
except ImportError:
|
|
39
39
|
HAS_PIL = False
|
|
40
40
|
|
|
41
|
-
VERSION = "3.0.
|
|
41
|
+
VERSION = "3.0.0b4"
|
|
42
42
|
_ROOT = None
|
|
43
43
|
DEBUG = True # os.getenv("PYPI_SERVICESTACK") is not None
|
|
44
|
+
MOCK = False
|
|
45
|
+
MOCK_DIR = os.getenv("MOCK_DIR")
|
|
46
|
+
MOCK = os.getenv("MOCK") == "1"
|
|
44
47
|
g_config_path = None
|
|
45
|
-
g_ui_path = None
|
|
46
48
|
g_config = None
|
|
47
49
|
g_providers = None
|
|
48
50
|
g_handlers = {}
|
|
@@ -102,17 +104,6 @@ def chat_summary(chat):
|
|
|
102
104
|
return json.dumps(clone, indent=2)
|
|
103
105
|
|
|
104
106
|
|
|
105
|
-
def gemini_chat_summary(gemini_chat):
|
|
106
|
-
"""Summarize Gemini chat completion request for logging. Replace inline_data with size of content only"""
|
|
107
|
-
clone = json.loads(json.dumps(gemini_chat))
|
|
108
|
-
for content in clone["contents"]:
|
|
109
|
-
for part in content["parts"]:
|
|
110
|
-
if "inline_data" in part:
|
|
111
|
-
data = part["inline_data"]["data"]
|
|
112
|
-
part["inline_data"]["data"] = f"({len(data)})"
|
|
113
|
-
return json.dumps(clone, indent=2)
|
|
114
|
-
|
|
115
|
-
|
|
116
107
|
image_exts = ["png", "webp", "jpg", "jpeg", "gif", "bmp", "svg", "tiff", "ico"]
|
|
117
108
|
audio_exts = ["mp3", "wav", "ogg", "flac", "m4a", "opus", "webm"]
|
|
118
109
|
|
|
@@ -206,6 +197,10 @@ def is_base_64(data):
|
|
|
206
197
|
return False
|
|
207
198
|
|
|
208
199
|
|
|
200
|
+
def id_to_name(id):
|
|
201
|
+
return id.replace("-", " ").title()
|
|
202
|
+
|
|
203
|
+
|
|
209
204
|
def get_file_mime_type(filename):
|
|
210
205
|
mime_type, _ = mimetypes.guess_type(filename)
|
|
211
206
|
return mime_type or "application/octet-stream"
|
|
@@ -467,6 +462,61 @@ class HTTPError(Exception):
|
|
|
467
462
|
super().__init__(f"HTTP {status} {reason}")
|
|
468
463
|
|
|
469
464
|
|
|
465
|
+
def save_image_to_cache(base64_data, filename, image_info):
|
|
466
|
+
ext = filename.split(".")[-1]
|
|
467
|
+
mimetype = get_file_mime_type(filename)
|
|
468
|
+
content = base64.b64decode(base64_data) if isinstance(base64_data, str) else base64_data
|
|
469
|
+
sha256_hash = hashlib.sha256(content).hexdigest()
|
|
470
|
+
|
|
471
|
+
save_filename = f"{sha256_hash}.{ext}" if ext else sha256_hash
|
|
472
|
+
|
|
473
|
+
# Use first 2 chars for subdir to avoid too many files in one dir
|
|
474
|
+
subdir = sha256_hash[:2]
|
|
475
|
+
relative_path = f"{subdir}/{save_filename}"
|
|
476
|
+
full_path = get_cache_path(relative_path)
|
|
477
|
+
|
|
478
|
+
url = f"~cache/{relative_path}"
|
|
479
|
+
|
|
480
|
+
# if file and its .info.json already exists, return it
|
|
481
|
+
info_path = os.path.splitext(full_path)[0] + ".info.json"
|
|
482
|
+
if os.path.exists(full_path) and os.path.exists(info_path):
|
|
483
|
+
return url, json.load(open(info_path))
|
|
484
|
+
|
|
485
|
+
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
486
|
+
|
|
487
|
+
with open(full_path, "wb") as f:
|
|
488
|
+
f.write(content)
|
|
489
|
+
info = {
|
|
490
|
+
"date": int(time.time()),
|
|
491
|
+
"url": url,
|
|
492
|
+
"size": len(content),
|
|
493
|
+
"type": mimetype,
|
|
494
|
+
"name": filename,
|
|
495
|
+
}
|
|
496
|
+
info.update(image_info)
|
|
497
|
+
|
|
498
|
+
# If image, get dimensions
|
|
499
|
+
if HAS_PIL and mimetype.startswith("image/"):
|
|
500
|
+
try:
|
|
501
|
+
with Image.open(BytesIO(content)) as img:
|
|
502
|
+
info["width"] = img.width
|
|
503
|
+
info["height"] = img.height
|
|
504
|
+
except Exception:
|
|
505
|
+
pass
|
|
506
|
+
|
|
507
|
+
if "width" in info and "height" in info:
|
|
508
|
+
_log(f"Saved image to cache: {full_path} ({len(content)} bytes) {info['width']}x{info['height']}")
|
|
509
|
+
else:
|
|
510
|
+
_log(f"Saved image to cache: {full_path} ({len(content)} bytes)")
|
|
511
|
+
|
|
512
|
+
# Save metadata
|
|
513
|
+
info_path = os.path.splitext(full_path)[0] + ".info.json"
|
|
514
|
+
with open(info_path, "w") as f:
|
|
515
|
+
json.dump(info, f)
|
|
516
|
+
|
|
517
|
+
return url, info
|
|
518
|
+
|
|
519
|
+
|
|
470
520
|
async def response_json(response):
|
|
471
521
|
text = await response.text()
|
|
472
522
|
if response.status >= 400:
|
|
@@ -476,6 +526,120 @@ async def response_json(response):
|
|
|
476
526
|
return body
|
|
477
527
|
|
|
478
528
|
|
|
529
|
+
def chat_to_prompt(chat):
|
|
530
|
+
prompt = ""
|
|
531
|
+
if "messages" in chat:
|
|
532
|
+
for message in chat["messages"]:
|
|
533
|
+
if message["role"] == "user":
|
|
534
|
+
# if content is string
|
|
535
|
+
if isinstance(message["content"], str):
|
|
536
|
+
if prompt:
|
|
537
|
+
prompt += "\n"
|
|
538
|
+
prompt += message["content"]
|
|
539
|
+
elif isinstance(message["content"], list):
|
|
540
|
+
# if content is array of objects
|
|
541
|
+
for part in message["content"]:
|
|
542
|
+
if part["type"] == "text":
|
|
543
|
+
if prompt:
|
|
544
|
+
prompt += "\n"
|
|
545
|
+
prompt += part["text"]
|
|
546
|
+
return prompt
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def last_user_prompt(chat):
|
|
550
|
+
prompt = ""
|
|
551
|
+
if "messages" in chat:
|
|
552
|
+
for message in chat["messages"]:
|
|
553
|
+
if message["role"] == "user":
|
|
554
|
+
# if content is string
|
|
555
|
+
if isinstance(message["content"], str):
|
|
556
|
+
prompt = message["content"]
|
|
557
|
+
elif isinstance(message["content"], list):
|
|
558
|
+
# if content is array of objects
|
|
559
|
+
for part in message["content"]:
|
|
560
|
+
if part["type"] == "text":
|
|
561
|
+
prompt = part["text"]
|
|
562
|
+
return prompt
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
# Image Generator Providers
|
|
566
|
+
class GeneratorBase:
|
|
567
|
+
def __init__(self, **kwargs):
|
|
568
|
+
self.id = kwargs.get("id")
|
|
569
|
+
self.api = kwargs.get("api")
|
|
570
|
+
self.api_key = kwargs.get("api_key")
|
|
571
|
+
self.headers = {
|
|
572
|
+
"Accept": "application/json",
|
|
573
|
+
"Content-Type": "application/json",
|
|
574
|
+
}
|
|
575
|
+
self.chat_url = f"{self.api}/chat/completions"
|
|
576
|
+
self.default_content = "I've generated the image for you."
|
|
577
|
+
|
|
578
|
+
def validate(self, **kwargs):
|
|
579
|
+
if not self.api_key:
|
|
580
|
+
api_keys = ", ".join(self.env)
|
|
581
|
+
return f"Provider '{self.name}' requires API Key {api_keys}"
|
|
582
|
+
return None
|
|
583
|
+
|
|
584
|
+
def test(self, **kwargs):
|
|
585
|
+
error_msg = self.validate(**kwargs)
|
|
586
|
+
if error_msg:
|
|
587
|
+
_log(error_msg)
|
|
588
|
+
return False
|
|
589
|
+
return True
|
|
590
|
+
|
|
591
|
+
async def load(self):
|
|
592
|
+
pass
|
|
593
|
+
|
|
594
|
+
def gen_summary(self, gen):
|
|
595
|
+
"""Summarize gen response for logging."""
|
|
596
|
+
clone = json.loads(json.dumps(gen))
|
|
597
|
+
return json.dumps(clone, indent=2)
|
|
598
|
+
|
|
599
|
+
def chat_summary(self, chat):
|
|
600
|
+
return chat_summary(chat)
|
|
601
|
+
|
|
602
|
+
def process_chat(self, chat, provider_id=None):
|
|
603
|
+
return process_chat(chat, provider_id)
|
|
604
|
+
|
|
605
|
+
async def response_json(self, response):
|
|
606
|
+
return await response_json(response)
|
|
607
|
+
|
|
608
|
+
def get_headers(self, provider, chat):
|
|
609
|
+
headers = self.headers.copy()
|
|
610
|
+
if provider is not None:
|
|
611
|
+
headers["Authorization"] = f"Bearer {provider.api_key}"
|
|
612
|
+
elif self.api_key:
|
|
613
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
614
|
+
return headers
|
|
615
|
+
|
|
616
|
+
def to_response(self, response, chat, started_at):
|
|
617
|
+
raise NotImplementedError
|
|
618
|
+
|
|
619
|
+
async def chat(self, chat, provider=None):
|
|
620
|
+
return {
|
|
621
|
+
"choices": [
|
|
622
|
+
{
|
|
623
|
+
"message": {
|
|
624
|
+
"role": "assistant",
|
|
625
|
+
"content": "Not Implemented",
|
|
626
|
+
"images": [
|
|
627
|
+
{
|
|
628
|
+
"type": "image_url",
|
|
629
|
+
"image_url": {
|
|
630
|
+
"url": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik0xMiAyMGE4IDggMCAxIDAgMC0xNmE4IDggMCAwIDAgMCAxNm0wIDJDNi40NzcgMjIgMiAxNy41MjMgMiAxMlM2LjQ3NyAyIDEyIDJzMTAgNC40NzcgMTAgMTBzLTQuNDc3IDEwLTEwIDEwbS0xLTZoMnYyaC0yem0wLTEwaDJ2OGgtMnoiLz48L3N2Zz4=",
|
|
631
|
+
},
|
|
632
|
+
}
|
|
633
|
+
],
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
]
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
# OpenAI Providers
|
|
641
|
+
|
|
642
|
+
|
|
479
643
|
class OpenAiCompatible:
|
|
480
644
|
sdk = "@ai-sdk/openai-compatible"
|
|
481
645
|
|
|
@@ -487,8 +651,9 @@ class OpenAiCompatible:
|
|
|
487
651
|
|
|
488
652
|
self.id = kwargs.get("id")
|
|
489
653
|
self.api = kwargs.get("api").strip("/")
|
|
654
|
+
self.env = kwargs.get("env", [])
|
|
490
655
|
self.api_key = kwargs.get("api_key")
|
|
491
|
-
self.name = kwargs.get("name", self.id
|
|
656
|
+
self.name = kwargs.get("name", id_to_name(self.id))
|
|
492
657
|
self.set_models(**kwargs)
|
|
493
658
|
|
|
494
659
|
self.chat_url = f"{self.api}/chat/completions"
|
|
@@ -516,6 +681,7 @@ class OpenAiCompatible:
|
|
|
516
681
|
self.stream = bool(kwargs["stream"]) if "stream" in kwargs else None
|
|
517
682
|
self.enable_thinking = bool(kwargs["enable_thinking"]) if "enable_thinking" in kwargs else None
|
|
518
683
|
self.check = kwargs.get("check")
|
|
684
|
+
self.modalities = kwargs.get("modalities", {})
|
|
519
685
|
|
|
520
686
|
def set_models(self, **kwargs):
|
|
521
687
|
models = kwargs.get("models", {})
|
|
@@ -541,11 +707,18 @@ class OpenAiCompatible:
|
|
|
541
707
|
_log(f"Filtering {len(self.models)} models, excluding models that match regex: {exclude_models}")
|
|
542
708
|
self.models = {k: v for k, v in self.models.items() if not re.search(exclude_models, k)}
|
|
543
709
|
|
|
710
|
+
def validate(self, **kwargs):
|
|
711
|
+
if not self.api_key:
|
|
712
|
+
api_keys = ", ".join(self.env)
|
|
713
|
+
return f"Provider '{self.name}' requires API Key {api_keys}"
|
|
714
|
+
return None
|
|
715
|
+
|
|
544
716
|
def test(self, **kwargs):
|
|
545
|
-
|
|
546
|
-
if
|
|
547
|
-
_log(
|
|
548
|
-
|
|
717
|
+
error_msg = self.validate(**kwargs)
|
|
718
|
+
if error_msg:
|
|
719
|
+
_log(error_msg)
|
|
720
|
+
return False
|
|
721
|
+
return True
|
|
549
722
|
|
|
550
723
|
async def load(self):
|
|
551
724
|
if not self.models:
|
|
@@ -593,8 +766,12 @@ class OpenAiCompatible:
|
|
|
593
766
|
if "/" in model:
|
|
594
767
|
last_part = model.split("/")[-1]
|
|
595
768
|
return self.provider_model(last_part)
|
|
769
|
+
|
|
596
770
|
return None
|
|
597
771
|
|
|
772
|
+
def response_json(self, response):
|
|
773
|
+
return response_json(response)
|
|
774
|
+
|
|
598
775
|
def to_response(self, response, chat, started_at):
|
|
599
776
|
if "metadata" not in response:
|
|
600
777
|
response["metadata"] = {}
|
|
@@ -603,12 +780,28 @@ class OpenAiCompatible:
|
|
|
603
780
|
pricing = self.model_cost(chat["model"])
|
|
604
781
|
if pricing and "input" in pricing and "output" in pricing:
|
|
605
782
|
response["metadata"]["pricing"] = f"{pricing['input']}/{pricing['output']}"
|
|
606
|
-
_log(json.dumps(response, indent=2))
|
|
607
783
|
return response
|
|
608
784
|
|
|
785
|
+
def chat_summary(self, chat):
|
|
786
|
+
return chat_summary(chat)
|
|
787
|
+
|
|
788
|
+
def process_chat(self, chat, provider_id=None):
|
|
789
|
+
return process_chat(chat, provider_id)
|
|
790
|
+
|
|
609
791
|
async def chat(self, chat):
|
|
610
792
|
chat["model"] = self.provider_model(chat["model"]) or chat["model"]
|
|
611
793
|
|
|
794
|
+
if "modalities" in chat:
|
|
795
|
+
for modality in chat["modalities"]:
|
|
796
|
+
# use default implementation for text modalities
|
|
797
|
+
if modality == "text":
|
|
798
|
+
continue
|
|
799
|
+
modality_provider = self.modalities.get(modality)
|
|
800
|
+
if modality_provider:
|
|
801
|
+
return await modality_provider.chat(chat, self)
|
|
802
|
+
else:
|
|
803
|
+
raise Exception(f"Provider {self.name} does not support '{modality}' modality")
|
|
804
|
+
|
|
612
805
|
# with open(os.path.join(os.path.dirname(__file__), 'chat.wip.json'), "w") as f:
|
|
613
806
|
# f.write(json.dumps(chat, indent=2))
|
|
614
807
|
|
|
@@ -661,193 +854,6 @@ class OpenAiCompatible:
|
|
|
661
854
|
return self.to_response(await response_json(response), chat, started_at)
|
|
662
855
|
|
|
663
856
|
|
|
664
|
-
class OpenAiProvider(OpenAiCompatible):
|
|
665
|
-
sdk = "@ai-sdk/openai"
|
|
666
|
-
|
|
667
|
-
def __init__(self, **kwargs):
|
|
668
|
-
if "api" not in kwargs:
|
|
669
|
-
kwargs["api"] = "https://api.openai.com/v1"
|
|
670
|
-
super().__init__(**kwargs)
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
class AnthropicProvider(OpenAiCompatible):
|
|
674
|
-
sdk = "@ai-sdk/anthropic"
|
|
675
|
-
|
|
676
|
-
def __init__(self, **kwargs):
|
|
677
|
-
if "api" not in kwargs:
|
|
678
|
-
kwargs["api"] = "https://api.anthropic.com/v1"
|
|
679
|
-
super().__init__(**kwargs)
|
|
680
|
-
|
|
681
|
-
# Anthropic uses x-api-key header instead of Authorization
|
|
682
|
-
if self.api_key:
|
|
683
|
-
self.headers = self.headers.copy()
|
|
684
|
-
if "Authorization" in self.headers:
|
|
685
|
-
del self.headers["Authorization"]
|
|
686
|
-
self.headers["x-api-key"] = self.api_key
|
|
687
|
-
|
|
688
|
-
if "anthropic-version" not in self.headers:
|
|
689
|
-
self.headers = self.headers.copy()
|
|
690
|
-
self.headers["anthropic-version"] = "2023-06-01"
|
|
691
|
-
self.chat_url = f"{self.api}/messages"
|
|
692
|
-
|
|
693
|
-
async def chat(self, chat):
|
|
694
|
-
chat["model"] = self.provider_model(chat["model"]) or chat["model"]
|
|
695
|
-
|
|
696
|
-
chat = await process_chat(chat, provider_id=self.id)
|
|
697
|
-
|
|
698
|
-
# Transform OpenAI format to Anthropic format
|
|
699
|
-
anthropic_request = {
|
|
700
|
-
"model": chat["model"],
|
|
701
|
-
"messages": [],
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
# Extract system message (Anthropic uses top-level 'system' parameter)
|
|
705
|
-
system_messages = []
|
|
706
|
-
for message in chat.get("messages", []):
|
|
707
|
-
if message.get("role") == "system":
|
|
708
|
-
content = message.get("content", "")
|
|
709
|
-
if isinstance(content, str):
|
|
710
|
-
system_messages.append(content)
|
|
711
|
-
elif isinstance(content, list):
|
|
712
|
-
for item in content:
|
|
713
|
-
if item.get("type") == "text":
|
|
714
|
-
system_messages.append(item.get("text", ""))
|
|
715
|
-
|
|
716
|
-
if system_messages:
|
|
717
|
-
anthropic_request["system"] = "\n".join(system_messages)
|
|
718
|
-
|
|
719
|
-
# Transform messages (exclude system messages)
|
|
720
|
-
for message in chat.get("messages", []):
|
|
721
|
-
if message.get("role") == "system":
|
|
722
|
-
continue
|
|
723
|
-
|
|
724
|
-
anthropic_message = {"role": message.get("role"), "content": []}
|
|
725
|
-
|
|
726
|
-
content = message.get("content", "")
|
|
727
|
-
if isinstance(content, str):
|
|
728
|
-
anthropic_message["content"] = content
|
|
729
|
-
elif isinstance(content, list):
|
|
730
|
-
for item in content:
|
|
731
|
-
if item.get("type") == "text":
|
|
732
|
-
anthropic_message["content"].append({"type": "text", "text": item.get("text", "")})
|
|
733
|
-
elif item.get("type") == "image_url" and "image_url" in item:
|
|
734
|
-
# Transform OpenAI image_url format to Anthropic format
|
|
735
|
-
image_url = item["image_url"].get("url", "")
|
|
736
|
-
if image_url.startswith("data:"):
|
|
737
|
-
# Extract media type and base64 data
|
|
738
|
-
parts = image_url.split(";base64,", 1)
|
|
739
|
-
if len(parts) == 2:
|
|
740
|
-
media_type = parts[0].replace("data:", "")
|
|
741
|
-
base64_data = parts[1]
|
|
742
|
-
anthropic_message["content"].append(
|
|
743
|
-
{
|
|
744
|
-
"type": "image",
|
|
745
|
-
"source": {"type": "base64", "media_type": media_type, "data": base64_data},
|
|
746
|
-
}
|
|
747
|
-
)
|
|
748
|
-
|
|
749
|
-
anthropic_request["messages"].append(anthropic_message)
|
|
750
|
-
|
|
751
|
-
# Handle max_tokens (required by Anthropic, uses max_tokens not max_completion_tokens)
|
|
752
|
-
if "max_completion_tokens" in chat:
|
|
753
|
-
anthropic_request["max_tokens"] = chat["max_completion_tokens"]
|
|
754
|
-
elif "max_tokens" in chat:
|
|
755
|
-
anthropic_request["max_tokens"] = chat["max_tokens"]
|
|
756
|
-
else:
|
|
757
|
-
# Anthropic requires max_tokens, set a default
|
|
758
|
-
anthropic_request["max_tokens"] = 4096
|
|
759
|
-
|
|
760
|
-
# Copy other supported parameters
|
|
761
|
-
if "temperature" in chat:
|
|
762
|
-
anthropic_request["temperature"] = chat["temperature"]
|
|
763
|
-
if "top_p" in chat:
|
|
764
|
-
anthropic_request["top_p"] = chat["top_p"]
|
|
765
|
-
if "top_k" in chat:
|
|
766
|
-
anthropic_request["top_k"] = chat["top_k"]
|
|
767
|
-
if "stop" in chat:
|
|
768
|
-
anthropic_request["stop_sequences"] = chat["stop"] if isinstance(chat["stop"], list) else [chat["stop"]]
|
|
769
|
-
if "stream" in chat:
|
|
770
|
-
anthropic_request["stream"] = chat["stream"]
|
|
771
|
-
if "tools" in chat:
|
|
772
|
-
anthropic_request["tools"] = chat["tools"]
|
|
773
|
-
if "tool_choice" in chat:
|
|
774
|
-
anthropic_request["tool_choice"] = chat["tool_choice"]
|
|
775
|
-
|
|
776
|
-
_log(f"POST {self.chat_url}")
|
|
777
|
-
_log(f"Anthropic Request: {json.dumps(anthropic_request, indent=2)}")
|
|
778
|
-
|
|
779
|
-
async with aiohttp.ClientSession() as session:
|
|
780
|
-
started_at = time.time()
|
|
781
|
-
async with session.post(
|
|
782
|
-
self.chat_url,
|
|
783
|
-
headers=self.headers,
|
|
784
|
-
data=json.dumps(anthropic_request),
|
|
785
|
-
timeout=aiohttp.ClientTimeout(total=120),
|
|
786
|
-
) as response:
|
|
787
|
-
return self.to_response(await response_json(response), chat, started_at)
|
|
788
|
-
|
|
789
|
-
def to_response(self, response, chat, started_at):
|
|
790
|
-
"""Convert Anthropic response format to OpenAI-compatible format."""
|
|
791
|
-
# Transform Anthropic response to OpenAI format
|
|
792
|
-
openai_response = {
|
|
793
|
-
"id": response.get("id", ""),
|
|
794
|
-
"object": "chat.completion",
|
|
795
|
-
"created": int(started_at),
|
|
796
|
-
"model": response.get("model", ""),
|
|
797
|
-
"choices": [],
|
|
798
|
-
"usage": {},
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
# Transform content blocks to message content
|
|
802
|
-
content_parts = []
|
|
803
|
-
thinking_parts = []
|
|
804
|
-
|
|
805
|
-
for block in response.get("content", []):
|
|
806
|
-
if block.get("type") == "text":
|
|
807
|
-
content_parts.append(block.get("text", ""))
|
|
808
|
-
elif block.get("type") == "thinking":
|
|
809
|
-
# Store thinking blocks separately (some models include reasoning)
|
|
810
|
-
thinking_parts.append(block.get("thinking", ""))
|
|
811
|
-
|
|
812
|
-
# Combine all text content
|
|
813
|
-
message_content = "\n".join(content_parts) if content_parts else ""
|
|
814
|
-
|
|
815
|
-
# Create the choice object
|
|
816
|
-
choice = {
|
|
817
|
-
"index": 0,
|
|
818
|
-
"message": {"role": "assistant", "content": message_content},
|
|
819
|
-
"finish_reason": response.get("stop_reason", "stop"),
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
# Add thinking as metadata if present
|
|
823
|
-
if thinking_parts:
|
|
824
|
-
choice["message"]["thinking"] = "\n".join(thinking_parts)
|
|
825
|
-
|
|
826
|
-
openai_response["choices"].append(choice)
|
|
827
|
-
|
|
828
|
-
# Transform usage
|
|
829
|
-
if "usage" in response:
|
|
830
|
-
usage = response["usage"]
|
|
831
|
-
openai_response["usage"] = {
|
|
832
|
-
"prompt_tokens": usage.get("input_tokens", 0),
|
|
833
|
-
"completion_tokens": usage.get("output_tokens", 0),
|
|
834
|
-
"total_tokens": usage.get("input_tokens", 0) + usage.get("output_tokens", 0),
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
# Add metadata
|
|
838
|
-
if "metadata" not in openai_response:
|
|
839
|
-
openai_response["metadata"] = {}
|
|
840
|
-
openai_response["metadata"]["duration"] = int((time.time() - started_at) * 1000)
|
|
841
|
-
|
|
842
|
-
if chat is not None and "model" in chat:
|
|
843
|
-
cost = self.model_cost(chat["model"])
|
|
844
|
-
if cost and "input" in cost and "output" in cost:
|
|
845
|
-
openai_response["metadata"]["pricing"] = f"{cost['input']}/{cost['output']}"
|
|
846
|
-
|
|
847
|
-
_log(json.dumps(openai_response, indent=2))
|
|
848
|
-
return openai_response
|
|
849
|
-
|
|
850
|
-
|
|
851
857
|
class MistralProvider(OpenAiCompatible):
|
|
852
858
|
sdk = "@ai-sdk/mistral"
|
|
853
859
|
|
|
@@ -943,8 +949,8 @@ class OllamaProvider(OpenAiCompatible):
|
|
|
943
949
|
}
|
|
944
950
|
self.models = models
|
|
945
951
|
|
|
946
|
-
def
|
|
947
|
-
return
|
|
952
|
+
def validate(self, **kwargs):
|
|
953
|
+
return None
|
|
948
954
|
|
|
949
955
|
|
|
950
956
|
class LMStudioProvider(OllamaProvider):
|
|
@@ -973,223 +979,6 @@ class LMStudioProvider(OllamaProvider):
|
|
|
973
979
|
return ret
|
|
974
980
|
|
|
975
981
|
|
|
976
|
-
# class GoogleOpenAiProvider(OpenAiCompatible):
|
|
977
|
-
# sdk = "google-openai-compatible"
|
|
978
|
-
|
|
979
|
-
# def __init__(self, api_key, **kwargs):
|
|
980
|
-
# super().__init__(api="https://generativelanguage.googleapis.com", api_key=api_key, **kwargs)
|
|
981
|
-
# self.chat_url = "https://generativelanguage.googleapis.com/v1beta/chat/completions"
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
class GoogleProvider(OpenAiCompatible):
|
|
985
|
-
sdk = "@ai-sdk/google"
|
|
986
|
-
|
|
987
|
-
def __init__(self, **kwargs):
|
|
988
|
-
new_kwargs = {"api": "https://generativelanguage.googleapis.com", **kwargs}
|
|
989
|
-
super().__init__(**new_kwargs)
|
|
990
|
-
self.safety_settings = kwargs.get("safety_settings")
|
|
991
|
-
self.thinking_config = kwargs.get("thinking_config")
|
|
992
|
-
self.curl = kwargs.get("curl")
|
|
993
|
-
self.headers = kwargs.get("headers", {"Content-Type": "application/json"})
|
|
994
|
-
# Google fails when using Authorization header, use query string param instead
|
|
995
|
-
if "Authorization" in self.headers:
|
|
996
|
-
del self.headers["Authorization"]
|
|
997
|
-
|
|
998
|
-
async def chat(self, chat):
|
|
999
|
-
chat["model"] = self.provider_model(chat["model"]) or chat["model"]
|
|
1000
|
-
|
|
1001
|
-
chat = await process_chat(chat)
|
|
1002
|
-
generation_config = {}
|
|
1003
|
-
|
|
1004
|
-
# Filter out system messages and convert to proper Gemini format
|
|
1005
|
-
contents = []
|
|
1006
|
-
system_prompt = None
|
|
1007
|
-
|
|
1008
|
-
async with aiohttp.ClientSession() as session:
|
|
1009
|
-
for message in chat["messages"]:
|
|
1010
|
-
if message["role"] == "system":
|
|
1011
|
-
content = message["content"]
|
|
1012
|
-
if isinstance(content, list):
|
|
1013
|
-
for item in content:
|
|
1014
|
-
if "text" in item:
|
|
1015
|
-
system_prompt = item["text"]
|
|
1016
|
-
break
|
|
1017
|
-
elif isinstance(content, str):
|
|
1018
|
-
system_prompt = content
|
|
1019
|
-
elif "content" in message:
|
|
1020
|
-
if isinstance(message["content"], list):
|
|
1021
|
-
parts = []
|
|
1022
|
-
for item in message["content"]:
|
|
1023
|
-
if "type" in item:
|
|
1024
|
-
if item["type"] == "image_url" and "image_url" in item:
|
|
1025
|
-
image_url = item["image_url"]
|
|
1026
|
-
if "url" not in image_url:
|
|
1027
|
-
continue
|
|
1028
|
-
url = image_url["url"]
|
|
1029
|
-
if not url.startswith("data:"):
|
|
1030
|
-
raise (Exception("Image was not downloaded: " + url))
|
|
1031
|
-
# Extract mime type from data uri
|
|
1032
|
-
mimetype = url.split(";", 1)[0].split(":", 1)[1] if ";" in url else "image/png"
|
|
1033
|
-
base64_data = url.split(",", 1)[1]
|
|
1034
|
-
parts.append({"inline_data": {"mime_type": mimetype, "data": base64_data}})
|
|
1035
|
-
elif item["type"] == "input_audio" and "input_audio" in item:
|
|
1036
|
-
input_audio = item["input_audio"]
|
|
1037
|
-
if "data" not in input_audio:
|
|
1038
|
-
continue
|
|
1039
|
-
data = input_audio["data"]
|
|
1040
|
-
format = input_audio["format"]
|
|
1041
|
-
mimetype = f"audio/{format}"
|
|
1042
|
-
parts.append({"inline_data": {"mime_type": mimetype, "data": data}})
|
|
1043
|
-
elif item["type"] == "file" and "file" in item:
|
|
1044
|
-
file = item["file"]
|
|
1045
|
-
if "file_data" not in file:
|
|
1046
|
-
continue
|
|
1047
|
-
data = file["file_data"]
|
|
1048
|
-
if not data.startswith("data:"):
|
|
1049
|
-
raise (Exception("File was not downloaded: " + data))
|
|
1050
|
-
# Extract mime type from data uri
|
|
1051
|
-
mimetype = (
|
|
1052
|
-
data.split(";", 1)[0].split(":", 1)[1]
|
|
1053
|
-
if ";" in data
|
|
1054
|
-
else "application/octet-stream"
|
|
1055
|
-
)
|
|
1056
|
-
base64_data = data.split(",", 1)[1]
|
|
1057
|
-
parts.append({"inline_data": {"mime_type": mimetype, "data": base64_data}})
|
|
1058
|
-
if "text" in item:
|
|
1059
|
-
text = item["text"]
|
|
1060
|
-
parts.append({"text": text})
|
|
1061
|
-
if len(parts) > 0:
|
|
1062
|
-
contents.append(
|
|
1063
|
-
{
|
|
1064
|
-
"role": message["role"]
|
|
1065
|
-
if "role" in message and message["role"] == "user"
|
|
1066
|
-
else "model",
|
|
1067
|
-
"parts": parts,
|
|
1068
|
-
}
|
|
1069
|
-
)
|
|
1070
|
-
else:
|
|
1071
|
-
content = message["content"]
|
|
1072
|
-
contents.append(
|
|
1073
|
-
{
|
|
1074
|
-
"role": message["role"] if "role" in message and message["role"] == "user" else "model",
|
|
1075
|
-
"parts": [{"text": content}],
|
|
1076
|
-
}
|
|
1077
|
-
)
|
|
1078
|
-
|
|
1079
|
-
gemini_chat = {
|
|
1080
|
-
"contents": contents,
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
if self.safety_settings:
|
|
1084
|
-
gemini_chat["safetySettings"] = self.safety_settings
|
|
1085
|
-
|
|
1086
|
-
# Add system instruction if present
|
|
1087
|
-
if system_prompt is not None:
|
|
1088
|
-
gemini_chat["systemInstruction"] = {"parts": [{"text": system_prompt}]}
|
|
1089
|
-
|
|
1090
|
-
if "max_completion_tokens" in chat:
|
|
1091
|
-
generation_config["maxOutputTokens"] = chat["max_completion_tokens"]
|
|
1092
|
-
if "stop" in chat:
|
|
1093
|
-
generation_config["stopSequences"] = [chat["stop"]]
|
|
1094
|
-
if "temperature" in chat:
|
|
1095
|
-
generation_config["temperature"] = chat["temperature"]
|
|
1096
|
-
if "top_p" in chat:
|
|
1097
|
-
generation_config["topP"] = chat["top_p"]
|
|
1098
|
-
if "top_logprobs" in chat:
|
|
1099
|
-
generation_config["topK"] = chat["top_logprobs"]
|
|
1100
|
-
|
|
1101
|
-
if "thinkingConfig" in chat:
|
|
1102
|
-
generation_config["thinkingConfig"] = chat["thinkingConfig"]
|
|
1103
|
-
elif self.thinking_config:
|
|
1104
|
-
generation_config["thinkingConfig"] = self.thinking_config
|
|
1105
|
-
|
|
1106
|
-
if len(generation_config) > 0:
|
|
1107
|
-
gemini_chat["generationConfig"] = generation_config
|
|
1108
|
-
|
|
1109
|
-
started_at = int(time.time() * 1000)
|
|
1110
|
-
gemini_chat_url = f"https://generativelanguage.googleapis.com/v1beta/models/{chat['model']}:generateContent?key={self.api_key}"
|
|
1111
|
-
|
|
1112
|
-
_log(f"POST {gemini_chat_url}")
|
|
1113
|
-
_log(gemini_chat_summary(gemini_chat))
|
|
1114
|
-
started_at = time.time()
|
|
1115
|
-
|
|
1116
|
-
if self.curl:
|
|
1117
|
-
curl_args = [
|
|
1118
|
-
"curl",
|
|
1119
|
-
"-X",
|
|
1120
|
-
"POST",
|
|
1121
|
-
"-H",
|
|
1122
|
-
"Content-Type: application/json",
|
|
1123
|
-
"-d",
|
|
1124
|
-
json.dumps(gemini_chat),
|
|
1125
|
-
gemini_chat_url,
|
|
1126
|
-
]
|
|
1127
|
-
try:
|
|
1128
|
-
o = subprocess.run(curl_args, check=True, capture_output=True, text=True, timeout=120)
|
|
1129
|
-
obj = json.loads(o.stdout)
|
|
1130
|
-
except Exception as e:
|
|
1131
|
-
raise Exception(f"Error executing curl: {e}") from e
|
|
1132
|
-
else:
|
|
1133
|
-
async with session.post(
|
|
1134
|
-
gemini_chat_url,
|
|
1135
|
-
headers=self.headers,
|
|
1136
|
-
data=json.dumps(gemini_chat),
|
|
1137
|
-
timeout=aiohttp.ClientTimeout(total=120),
|
|
1138
|
-
) as res:
|
|
1139
|
-
obj = await response_json(res)
|
|
1140
|
-
_log(f"google response:\n{json.dumps(obj, indent=2)}")
|
|
1141
|
-
|
|
1142
|
-
response = {
|
|
1143
|
-
"id": f"chatcmpl-{started_at}",
|
|
1144
|
-
"created": started_at,
|
|
1145
|
-
"model": obj.get("modelVersion", chat["model"]),
|
|
1146
|
-
}
|
|
1147
|
-
choices = []
|
|
1148
|
-
if "error" in obj:
|
|
1149
|
-
_log(f"Error: {obj['error']}")
|
|
1150
|
-
raise Exception(obj["error"]["message"])
|
|
1151
|
-
for i, candidate in enumerate(obj["candidates"]):
|
|
1152
|
-
role = "assistant"
|
|
1153
|
-
if "content" in candidate and "role" in candidate["content"]:
|
|
1154
|
-
role = "assistant" if candidate["content"]["role"] == "model" else candidate["content"]["role"]
|
|
1155
|
-
|
|
1156
|
-
# Safely extract content from all text parts
|
|
1157
|
-
content = ""
|
|
1158
|
-
reasoning = ""
|
|
1159
|
-
if "content" in candidate and "parts" in candidate["content"]:
|
|
1160
|
-
text_parts = []
|
|
1161
|
-
reasoning_parts = []
|
|
1162
|
-
for part in candidate["content"]["parts"]:
|
|
1163
|
-
if "text" in part:
|
|
1164
|
-
if "thought" in part and part["thought"]:
|
|
1165
|
-
reasoning_parts.append(part["text"])
|
|
1166
|
-
else:
|
|
1167
|
-
text_parts.append(part["text"])
|
|
1168
|
-
content = " ".join(text_parts)
|
|
1169
|
-
reasoning = " ".join(reasoning_parts)
|
|
1170
|
-
|
|
1171
|
-
choice = {
|
|
1172
|
-
"index": i,
|
|
1173
|
-
"finish_reason": candidate.get("finishReason", "stop"),
|
|
1174
|
-
"message": {
|
|
1175
|
-
"role": role,
|
|
1176
|
-
"content": content,
|
|
1177
|
-
},
|
|
1178
|
-
}
|
|
1179
|
-
if reasoning:
|
|
1180
|
-
choice["message"]["reasoning"] = reasoning
|
|
1181
|
-
choices.append(choice)
|
|
1182
|
-
response["choices"] = choices
|
|
1183
|
-
if "usageMetadata" in obj:
|
|
1184
|
-
usage = obj["usageMetadata"]
|
|
1185
|
-
response["usage"] = {
|
|
1186
|
-
"completion_tokens": usage["candidatesTokenCount"],
|
|
1187
|
-
"total_tokens": usage["totalTokenCount"],
|
|
1188
|
-
"prompt_tokens": usage["promptTokenCount"],
|
|
1189
|
-
}
|
|
1190
|
-
return self.to_response(response, chat, started_at)
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
982
|
def get_provider_model(model_name):
|
|
1194
983
|
for provider in g_handlers.values():
|
|
1195
984
|
provider_model = provider.provider_model(model_name)
|
|
@@ -1337,8 +1126,29 @@ async def cli_chat(chat, image=None, audio=None, file=None, args=None, raw=False
|
|
|
1337
1126
|
print(json.dumps(response, indent=2))
|
|
1338
1127
|
exit(0)
|
|
1339
1128
|
else:
|
|
1340
|
-
|
|
1341
|
-
|
|
1129
|
+
msg = response["choices"][0]["message"]
|
|
1130
|
+
if "answer" in msg:
|
|
1131
|
+
answer = msg["content"]
|
|
1132
|
+
print(answer)
|
|
1133
|
+
|
|
1134
|
+
generated_files = []
|
|
1135
|
+
for choice in response["choices"]:
|
|
1136
|
+
if "message" in choice:
|
|
1137
|
+
msg = choice["message"]
|
|
1138
|
+
if "images" in msg:
|
|
1139
|
+
for image in msg["images"]:
|
|
1140
|
+
image_url = image["image_url"]["url"]
|
|
1141
|
+
generated_files.append(image_url)
|
|
1142
|
+
|
|
1143
|
+
if len(generated_files) > 0:
|
|
1144
|
+
print("\nSaved files:")
|
|
1145
|
+
for file in generated_files:
|
|
1146
|
+
if file.startswith("~cache"):
|
|
1147
|
+
print(get_cache_path(file[7:]))
|
|
1148
|
+
_log(f"http://localhost:8000/{file}")
|
|
1149
|
+
else:
|
|
1150
|
+
print(file)
|
|
1151
|
+
|
|
1342
1152
|
except HTTPError as e:
|
|
1343
1153
|
# HTTP error (4xx, 5xx)
|
|
1344
1154
|
print(f"{e}:\n{e.body}")
|
|
@@ -1380,22 +1190,26 @@ def init_llms(config, providers):
|
|
|
1380
1190
|
providers = g_config["providers"]
|
|
1381
1191
|
|
|
1382
1192
|
for id, orig in providers.items():
|
|
1383
|
-
|
|
1384
|
-
if "enabled" in definition and not definition["enabled"]:
|
|
1193
|
+
if "enabled" in orig and not orig["enabled"]:
|
|
1385
1194
|
continue
|
|
1386
1195
|
|
|
1387
|
-
|
|
1388
|
-
if "id" not in definition:
|
|
1389
|
-
definition["id"] = provider_id
|
|
1390
|
-
provider = g_providers.get(provider_id)
|
|
1391
|
-
constructor_kwargs = create_provider_kwargs(definition, provider)
|
|
1392
|
-
provider = create_provider(constructor_kwargs)
|
|
1393
|
-
|
|
1196
|
+
provider, constructor_kwargs = create_provider_from_definition(id, orig)
|
|
1394
1197
|
if provider and provider.test(**constructor_kwargs):
|
|
1395
1198
|
g_handlers[id] = provider
|
|
1396
1199
|
return g_handlers
|
|
1397
1200
|
|
|
1398
1201
|
|
|
1202
|
+
def create_provider_from_definition(id, orig):
|
|
1203
|
+
definition = orig.copy()
|
|
1204
|
+
provider_id = definition.get("id", id)
|
|
1205
|
+
if "id" not in definition:
|
|
1206
|
+
definition["id"] = provider_id
|
|
1207
|
+
provider = g_providers.get(provider_id)
|
|
1208
|
+
constructor_kwargs = create_provider_kwargs(definition, provider)
|
|
1209
|
+
provider = create_provider(constructor_kwargs)
|
|
1210
|
+
return provider, constructor_kwargs
|
|
1211
|
+
|
|
1212
|
+
|
|
1399
1213
|
def create_provider_kwargs(definition, provider=None):
|
|
1400
1214
|
if provider:
|
|
1401
1215
|
provider = provider.copy()
|
|
@@ -1423,6 +1237,15 @@ def create_provider_kwargs(definition, provider=None):
|
|
|
1423
1237
|
if isinstance(value, (list, dict)):
|
|
1424
1238
|
constructor_kwargs[key] = value.copy()
|
|
1425
1239
|
constructor_kwargs["headers"] = g_config["defaults"]["headers"].copy()
|
|
1240
|
+
|
|
1241
|
+
if "modalities" in definition:
|
|
1242
|
+
constructor_kwargs["modalities"] = {}
|
|
1243
|
+
for modality, modality_definition in definition["modalities"].items():
|
|
1244
|
+
modality_provider = create_provider(modality_definition)
|
|
1245
|
+
if not modality_provider:
|
|
1246
|
+
return None
|
|
1247
|
+
constructor_kwargs["modalities"][modality] = modality_provider
|
|
1248
|
+
|
|
1426
1249
|
return constructor_kwargs
|
|
1427
1250
|
|
|
1428
1251
|
|
|
@@ -1438,6 +1261,8 @@ def create_provider(provider):
|
|
|
1438
1261
|
for provider_type in g_app.all_providers:
|
|
1439
1262
|
if provider_type.sdk == npm_sdk:
|
|
1440
1263
|
kwargs = create_provider_kwargs(provider)
|
|
1264
|
+
if kwargs is None:
|
|
1265
|
+
kwargs = provider
|
|
1441
1266
|
return provider_type(**kwargs)
|
|
1442
1267
|
|
|
1443
1268
|
_log(f"Could not find provider {provider_label} with npm sdk {npm_sdk}")
|
|
@@ -1491,11 +1316,23 @@ async def update_providers(home_providers_path):
|
|
|
1491
1316
|
global g_providers
|
|
1492
1317
|
text = await get_text("https://models.dev/api.json")
|
|
1493
1318
|
all_providers = json.loads(text)
|
|
1319
|
+
extra_providers = {}
|
|
1320
|
+
extra_providers_path = home_providers_path.replace("providers.json", "providers-extra.json")
|
|
1321
|
+
if os.path.exists(extra_providers_path):
|
|
1322
|
+
with open(extra_providers_path) as f:
|
|
1323
|
+
extra_providers = json.load(f)
|
|
1494
1324
|
|
|
1495
1325
|
filtered_providers = {}
|
|
1496
1326
|
for id, provider in all_providers.items():
|
|
1497
1327
|
if id in g_config["providers"]:
|
|
1498
1328
|
filtered_providers[id] = provider
|
|
1329
|
+
if id in extra_providers and "models" in extra_providers[id]:
|
|
1330
|
+
for model_id, model in extra_providers[id]["models"].items():
|
|
1331
|
+
if "id" not in model:
|
|
1332
|
+
model["id"] = model_id
|
|
1333
|
+
if "name" not in model:
|
|
1334
|
+
model["name"] = id_to_name(model["id"])
|
|
1335
|
+
filtered_providers[id]["models"][model_id] = model
|
|
1499
1336
|
|
|
1500
1337
|
os.makedirs(os.path.dirname(home_providers_path), exist_ok=True)
|
|
1501
1338
|
with open(home_providers_path, "w", encoding="utf-8") as f:
|
|
@@ -1548,26 +1385,18 @@ def get_config_path():
|
|
|
1548
1385
|
return None
|
|
1549
1386
|
|
|
1550
1387
|
|
|
1551
|
-
def get_ui_path():
|
|
1552
|
-
ui_paths = [home_llms_path("ui.json"), "ui.json"]
|
|
1553
|
-
for ui_path in ui_paths:
|
|
1554
|
-
if os.path.exists(ui_path):
|
|
1555
|
-
return ui_path
|
|
1556
|
-
return None
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
1388
|
def enable_provider(provider):
|
|
1560
1389
|
msg = None
|
|
1561
1390
|
provider_config = g_config["providers"][provider]
|
|
1391
|
+
if not provider_config:
|
|
1392
|
+
return None, f"Provider {provider} not found"
|
|
1393
|
+
|
|
1394
|
+
provider, constructor_kwargs = create_provider_from_definition(provider, provider_config)
|
|
1395
|
+
msg = provider.validate(**constructor_kwargs)
|
|
1396
|
+
if msg:
|
|
1397
|
+
return None, msg
|
|
1398
|
+
|
|
1562
1399
|
provider_config["enabled"] = True
|
|
1563
|
-
if "api_key" in provider_config:
|
|
1564
|
-
api_key = provider_config["api_key"]
|
|
1565
|
-
if isinstance(api_key, str):
|
|
1566
|
-
if api_key.startswith("$"):
|
|
1567
|
-
if not os.environ.get(api_key[1:], ""):
|
|
1568
|
-
msg = f"WARNING: {provider} requires missing API Key in Environment Variable {api_key}"
|
|
1569
|
-
else:
|
|
1570
|
-
msg = f"WARNING: {provider} is not configured with an API Key"
|
|
1571
1400
|
save_config(g_config)
|
|
1572
1401
|
init_llms(g_config, g_providers)
|
|
1573
1402
|
return provider_config, msg
|
|
@@ -1892,9 +1721,14 @@ async def text_from_resource_or_url(filename):
|
|
|
1892
1721
|
|
|
1893
1722
|
async def save_home_configs():
|
|
1894
1723
|
home_config_path = home_llms_path("llms.json")
|
|
1895
|
-
home_ui_path = home_llms_path("ui.json")
|
|
1896
1724
|
home_providers_path = home_llms_path("providers.json")
|
|
1897
|
-
|
|
1725
|
+
home_providers_extra_path = home_llms_path("providers-extra.json")
|
|
1726
|
+
|
|
1727
|
+
if (
|
|
1728
|
+
os.path.exists(home_config_path)
|
|
1729
|
+
and os.path.exists(home_providers_path)
|
|
1730
|
+
and os.path.exists(home_providers_extra_path)
|
|
1731
|
+
):
|
|
1898
1732
|
return
|
|
1899
1733
|
|
|
1900
1734
|
llms_home = os.path.dirname(home_config_path)
|
|
@@ -1906,17 +1740,17 @@ async def save_home_configs():
|
|
|
1906
1740
|
f.write(config_json)
|
|
1907
1741
|
_log(f"Created default config at {home_config_path}")
|
|
1908
1742
|
|
|
1909
|
-
if not os.path.exists(home_ui_path):
|
|
1910
|
-
ui_json = await text_from_resource_or_url("ui.json")
|
|
1911
|
-
with open(home_ui_path, "w", encoding="utf-8") as f:
|
|
1912
|
-
f.write(ui_json)
|
|
1913
|
-
_log(f"Created default ui config at {home_ui_path}")
|
|
1914
|
-
|
|
1915
1743
|
if not os.path.exists(home_providers_path):
|
|
1916
1744
|
providers_json = await text_from_resource_or_url("providers.json")
|
|
1917
1745
|
with open(home_providers_path, "w", encoding="utf-8") as f:
|
|
1918
1746
|
f.write(providers_json)
|
|
1919
1747
|
_log(f"Created default providers config at {home_providers_path}")
|
|
1748
|
+
|
|
1749
|
+
if not os.path.exists(home_providers_extra_path):
|
|
1750
|
+
extra_json = await text_from_resource_or_url("providers-extra.json")
|
|
1751
|
+
with open(home_providers_extra_path, "w", encoding="utf-8") as f:
|
|
1752
|
+
f.write(extra_json)
|
|
1753
|
+
_log(f"Created default extra providers config at {home_providers_extra_path}")
|
|
1920
1754
|
except Exception:
|
|
1921
1755
|
print("Could not create llms.json. Create one with --init or use --config <path>")
|
|
1922
1756
|
exit(1)
|
|
@@ -1953,59 +1787,52 @@ async def reload_providers():
|
|
|
1953
1787
|
return g_handlers
|
|
1954
1788
|
|
|
1955
1789
|
|
|
1956
|
-
async def watch_config_files(config_path,
|
|
1790
|
+
async def watch_config_files(config_path, providers_path, interval=1):
|
|
1957
1791
|
"""Watch config files and reload providers when they change"""
|
|
1958
1792
|
global g_config
|
|
1959
1793
|
|
|
1960
1794
|
config_path = Path(config_path)
|
|
1961
|
-
|
|
1795
|
+
providers_path = Path(providers_path)
|
|
1796
|
+
|
|
1797
|
+
_log(f"Watching config file: {config_path}")
|
|
1798
|
+
_log(f"Watching providers file: {providers_path}")
|
|
1962
1799
|
|
|
1963
|
-
|
|
1800
|
+
def get_latest_mtime():
|
|
1801
|
+
ret = 0
|
|
1802
|
+
name = "llms.json"
|
|
1803
|
+
if config_path.is_file():
|
|
1804
|
+
ret = config_path.stat().st_mtime
|
|
1805
|
+
name = config_path.name
|
|
1806
|
+
if providers_path.is_file() and providers_path.stat().st_mtime > ret:
|
|
1807
|
+
ret = providers_path.stat().st_mtime
|
|
1808
|
+
name = providers_path.name
|
|
1809
|
+
return ret, name
|
|
1964
1810
|
|
|
1965
|
-
|
|
1811
|
+
latest_mtime, name = get_latest_mtime()
|
|
1966
1812
|
|
|
1967
1813
|
while True:
|
|
1968
1814
|
await asyncio.sleep(interval)
|
|
1969
1815
|
|
|
1970
1816
|
# Check llms.json
|
|
1971
1817
|
try:
|
|
1972
|
-
|
|
1973
|
-
|
|
1818
|
+
new_mtime, name = get_latest_mtime()
|
|
1819
|
+
if new_mtime > latest_mtime:
|
|
1820
|
+
_log(f"Config file changed: {name}")
|
|
1821
|
+
latest_mtime = new_mtime
|
|
1974
1822
|
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
file_mtimes[str(config_path)] = mtime
|
|
1823
|
+
try:
|
|
1824
|
+
# Reload llms.json
|
|
1825
|
+
with open(config_path) as f:
|
|
1826
|
+
g_config = json.load(f)
|
|
1980
1827
|
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
# Reload providers
|
|
1987
|
-
await reload_providers()
|
|
1988
|
-
_log("Providers reloaded successfully")
|
|
1989
|
-
except Exception as e:
|
|
1990
|
-
_log(f"Error reloading config: {e}")
|
|
1828
|
+
# Reload providers
|
|
1829
|
+
await reload_providers()
|
|
1830
|
+
_log("Providers reloaded successfully")
|
|
1831
|
+
except Exception as e:
|
|
1832
|
+
_log(f"Error reloading config: {e}")
|
|
1991
1833
|
except FileNotFoundError:
|
|
1992
1834
|
pass
|
|
1993
1835
|
|
|
1994
|
-
# Check ui.json
|
|
1995
|
-
if ui_path:
|
|
1996
|
-
try:
|
|
1997
|
-
if ui_path.is_file():
|
|
1998
|
-
mtime = ui_path.stat().st_mtime
|
|
1999
|
-
|
|
2000
|
-
if str(ui_path) not in file_mtimes:
|
|
2001
|
-
file_mtimes[str(ui_path)] = mtime
|
|
2002
|
-
elif file_mtimes[str(ui_path)] != mtime:
|
|
2003
|
-
_log(f"Config file changed: {ui_path.name}")
|
|
2004
|
-
file_mtimes[str(ui_path)] = mtime
|
|
2005
|
-
_log("ui.json reloaded - reload page to update")
|
|
2006
|
-
except FileNotFoundError:
|
|
2007
|
-
pass
|
|
2008
|
-
|
|
2009
1836
|
|
|
2010
1837
|
def get_session_token(request):
|
|
2011
1838
|
return request.query.get("session") or request.headers.get("X-Session-Token") or request.cookies.get("llms-token")
|
|
@@ -2026,16 +1853,25 @@ class AppExtensions:
|
|
|
2026
1853
|
self.server_add_post = []
|
|
2027
1854
|
self.all_providers = [
|
|
2028
1855
|
OpenAiCompatible,
|
|
2029
|
-
OpenAiProvider,
|
|
2030
|
-
AnthropicProvider,
|
|
2031
1856
|
MistralProvider,
|
|
2032
1857
|
GroqProvider,
|
|
2033
1858
|
XaiProvider,
|
|
2034
1859
|
CodestralProvider,
|
|
2035
|
-
GoogleProvider,
|
|
2036
1860
|
OllamaProvider,
|
|
2037
1861
|
LMStudioProvider,
|
|
2038
1862
|
]
|
|
1863
|
+
self.aspect_ratios = {
|
|
1864
|
+
"1:1": "1024×1024",
|
|
1865
|
+
"2:3": "832×1248",
|
|
1866
|
+
"3:2": "1248×832",
|
|
1867
|
+
"3:4": "864×1184",
|
|
1868
|
+
"4:3": "1184×864",
|
|
1869
|
+
"4:5": "896×1152",
|
|
1870
|
+
"5:4": "1152×896",
|
|
1871
|
+
"9:16": "768×1344",
|
|
1872
|
+
"16:9": "1344×768",
|
|
1873
|
+
"21:9": "1536×672",
|
|
1874
|
+
}
|
|
2039
1875
|
|
|
2040
1876
|
|
|
2041
1877
|
class ExtensionContext:
|
|
@@ -2043,18 +1879,43 @@ class ExtensionContext:
|
|
|
2043
1879
|
self.app = app
|
|
2044
1880
|
self.path = path
|
|
2045
1881
|
self.name = os.path.basename(path)
|
|
1882
|
+
if self.name.endswith(".py"):
|
|
1883
|
+
self.name = self.name[:-3]
|
|
2046
1884
|
self.ext_prefix = f"/ext/{self.name}"
|
|
1885
|
+
self.MOCK = MOCK
|
|
1886
|
+
self.MOCK_DIR = MOCK_DIR
|
|
1887
|
+
self.debug = DEBUG
|
|
1888
|
+
self.verbose = g_verbose
|
|
1889
|
+
|
|
1890
|
+
def chat_to_prompt(self, chat):
|
|
1891
|
+
return chat_to_prompt(chat)
|
|
1892
|
+
|
|
1893
|
+
def last_user_prompt(self, chat):
|
|
1894
|
+
return last_user_prompt(chat)
|
|
1895
|
+
|
|
1896
|
+
def save_image_to_cache(self, base64_data, filename, image_info):
|
|
1897
|
+
return save_image_to_cache(base64_data, filename, image_info)
|
|
1898
|
+
|
|
1899
|
+
def text_from_file(self, path):
|
|
1900
|
+
return text_from_file(path)
|
|
2047
1901
|
|
|
2048
1902
|
def log(self, message):
|
|
2049
|
-
|
|
1903
|
+
if self.verbose:
|
|
1904
|
+
print(f"[{self.name}] {message}", flush=True)
|
|
1905
|
+
return message
|
|
1906
|
+
|
|
1907
|
+
def log_json(self, obj):
|
|
1908
|
+
if self.verbose:
|
|
1909
|
+
print(f"[{self.name}] {json.dumps(obj, indent=2)}", flush=True)
|
|
1910
|
+
return obj
|
|
2050
1911
|
|
|
2051
1912
|
def dbg(self, message):
|
|
2052
|
-
if
|
|
1913
|
+
if self.debug:
|
|
2053
1914
|
print(f"DEBUG [{self.name}]: {message}", flush=True)
|
|
2054
1915
|
|
|
2055
1916
|
def err(self, message, e):
|
|
2056
1917
|
print(f"ERROR [{self.name}]: {message}", e)
|
|
2057
|
-
if
|
|
1918
|
+
if self.verbose:
|
|
2058
1919
|
print(traceback.format_exc(), flush=True)
|
|
2059
1920
|
|
|
2060
1921
|
def add_provider(self, provider):
|
|
@@ -2122,6 +1983,33 @@ class ExtensionContext:
|
|
|
2122
1983
|
return None
|
|
2123
1984
|
|
|
2124
1985
|
|
|
1986
|
+
def load_builtin_extensions():
|
|
1987
|
+
providers_path = _ROOT / "providers"
|
|
1988
|
+
if not providers_path.exists():
|
|
1989
|
+
return
|
|
1990
|
+
|
|
1991
|
+
for item in os.listdir(providers_path):
|
|
1992
|
+
if not item.endswith(".py") or item == "__init__.py":
|
|
1993
|
+
continue
|
|
1994
|
+
|
|
1995
|
+
item_path = providers_path / item
|
|
1996
|
+
module_name = item[:-3]
|
|
1997
|
+
|
|
1998
|
+
try:
|
|
1999
|
+
spec = importlib.util.spec_from_file_location(module_name, item_path)
|
|
2000
|
+
if spec and spec.loader:
|
|
2001
|
+
module = importlib.util.module_from_spec(spec)
|
|
2002
|
+
sys.modules[f"llms.providers.{module_name}"] = module
|
|
2003
|
+
spec.loader.exec_module(module)
|
|
2004
|
+
|
|
2005
|
+
install_func = getattr(module, "__install__", None)
|
|
2006
|
+
if callable(install_func):
|
|
2007
|
+
install_func(ExtensionContext(g_app, item_path))
|
|
2008
|
+
_log(f"Loaded builtin extension: {module_name}")
|
|
2009
|
+
except Exception as e:
|
|
2010
|
+
_err(f"Failed to load builtin extension {module_name}", e)
|
|
2011
|
+
|
|
2012
|
+
|
|
2125
2013
|
def get_extensions_path():
|
|
2126
2014
|
return os.path.join(Path.home(), ".llms", "extensions")
|
|
2127
2015
|
|
|
@@ -2240,7 +2128,7 @@ def run_extension_cli():
|
|
|
2240
2128
|
|
|
2241
2129
|
|
|
2242
2130
|
def main():
|
|
2243
|
-
global _ROOT, g_verbose, g_default_model, g_logprefix, g_providers, g_config, g_config_path,
|
|
2131
|
+
global _ROOT, g_verbose, g_default_model, g_logprefix, g_providers, g_config, g_config_path, g_app
|
|
2244
2132
|
|
|
2245
2133
|
parser = argparse.ArgumentParser(description=f"llms v{VERSION}")
|
|
2246
2134
|
parser.add_argument("--config", default=None, help="Path to config file", metavar="FILE")
|
|
@@ -2254,6 +2142,7 @@ def main():
|
|
|
2254
2142
|
parser.add_argument("--image", default=None, help="Image input to use in chat completion")
|
|
2255
2143
|
parser.add_argument("--audio", default=None, help="Audio input to use in chat completion")
|
|
2256
2144
|
parser.add_argument("--file", default=None, help="File input to use in chat completion")
|
|
2145
|
+
parser.add_argument("--out", default=None, help="Image or Video Generation Request", metavar="MODALITY")
|
|
2257
2146
|
parser.add_argument(
|
|
2258
2147
|
"--args",
|
|
2259
2148
|
default=None,
|
|
@@ -2276,7 +2165,8 @@ def main():
|
|
|
2276
2165
|
parser.add_argument("--default", default=None, help="Configure the default model to use", metavar="MODEL")
|
|
2277
2166
|
|
|
2278
2167
|
parser.add_argument("--init", action="store_true", help="Create a default llms.json")
|
|
2279
|
-
parser.add_argument("--update", action="store_true", help="Update local models.dev providers.json")
|
|
2168
|
+
parser.add_argument("--update-providers", action="store_true", help="Update local models.dev providers.json")
|
|
2169
|
+
parser.add_argument("--update-extensions", action="store_true", help="Update installed extensions")
|
|
2280
2170
|
|
|
2281
2171
|
parser.add_argument("--root", default=None, help="Change root directory for UI files", metavar="PATH")
|
|
2282
2172
|
parser.add_argument("--logprefix", default="", help="Prefix used in log messages", metavar="PREFIX")
|
|
@@ -2299,6 +2189,15 @@ def main():
|
|
|
2299
2189
|
metavar="EXTENSION",
|
|
2300
2190
|
)
|
|
2301
2191
|
|
|
2192
|
+
parser.add_argument(
|
|
2193
|
+
"--update",
|
|
2194
|
+
nargs="?",
|
|
2195
|
+
const="ls",
|
|
2196
|
+
default=None,
|
|
2197
|
+
help="Update an extension (use 'all' to update all extensions)",
|
|
2198
|
+
metavar="EXTENSION",
|
|
2199
|
+
)
|
|
2200
|
+
|
|
2302
2201
|
# Load parser extensions, go through all extensions and load their parser arguments
|
|
2303
2202
|
init_extensions(parser)
|
|
2304
2203
|
|
|
@@ -2322,8 +2221,8 @@ def main():
|
|
|
2322
2221
|
exit(1)
|
|
2323
2222
|
|
|
2324
2223
|
home_config_path = home_llms_path("llms.json")
|
|
2325
|
-
home_ui_path = home_llms_path("ui.json")
|
|
2326
2224
|
home_providers_path = home_llms_path("providers.json")
|
|
2225
|
+
home_providers_extra_path = home_llms_path("providers-extra.json")
|
|
2327
2226
|
|
|
2328
2227
|
if cli_args.init:
|
|
2329
2228
|
if os.path.exists(home_config_path):
|
|
@@ -2332,17 +2231,17 @@ def main():
|
|
|
2332
2231
|
asyncio.run(save_default_config(home_config_path))
|
|
2333
2232
|
print(f"Created default config at {home_config_path}")
|
|
2334
2233
|
|
|
2335
|
-
if os.path.exists(home_ui_path):
|
|
2336
|
-
print(f"ui.json already exists at {home_ui_path}")
|
|
2337
|
-
else:
|
|
2338
|
-
asyncio.run(save_text_url(github_url("ui.json"), home_ui_path))
|
|
2339
|
-
print(f"Created default ui config at {home_ui_path}")
|
|
2340
|
-
|
|
2341
2234
|
if os.path.exists(home_providers_path):
|
|
2342
2235
|
print(f"providers.json already exists at {home_providers_path}")
|
|
2343
2236
|
else:
|
|
2344
2237
|
asyncio.run(save_text_url(github_url("providers.json"), home_providers_path))
|
|
2345
2238
|
print(f"Created default providers config at {home_providers_path}")
|
|
2239
|
+
|
|
2240
|
+
if os.path.exists(home_providers_extra_path):
|
|
2241
|
+
print(f"providers-extra.json already exists at {home_providers_extra_path}")
|
|
2242
|
+
else:
|
|
2243
|
+
asyncio.run(save_text_url(github_url("providers-extra.json"), home_providers_extra_path))
|
|
2244
|
+
print(f"Created default extra providers config at {home_providers_extra_path}")
|
|
2346
2245
|
exit(0)
|
|
2347
2246
|
|
|
2348
2247
|
if cli_args.providers:
|
|
@@ -2359,36 +2258,36 @@ def main():
|
|
|
2359
2258
|
g_config = load_config_json(config_json)
|
|
2360
2259
|
|
|
2361
2260
|
config_dir = os.path.dirname(g_config_path)
|
|
2362
|
-
# look for ui.json in same directory as config
|
|
2363
|
-
ui_path = os.path.join(config_dir, "ui.json")
|
|
2364
|
-
if os.path.exists(ui_path):
|
|
2365
|
-
g_ui_path = ui_path
|
|
2366
|
-
else:
|
|
2367
|
-
if not os.path.exists(home_ui_path):
|
|
2368
|
-
ui_json = text_from_resource("ui.json")
|
|
2369
|
-
with open(home_ui_path, "w", encoding="utf-8") as f:
|
|
2370
|
-
f.write(ui_json)
|
|
2371
|
-
_log(f"Created default ui config at {home_ui_path}")
|
|
2372
|
-
g_ui_path = home_ui_path
|
|
2373
2261
|
|
|
2374
2262
|
if not g_providers and os.path.exists(os.path.join(config_dir, "providers.json")):
|
|
2375
2263
|
g_providers = json.loads(text_from_file(os.path.join(config_dir, "providers.json")))
|
|
2376
2264
|
|
|
2377
2265
|
else:
|
|
2378
|
-
# ensure llms.json and
|
|
2266
|
+
# ensure llms.json and providers.json exist in home directory
|
|
2379
2267
|
asyncio.run(save_home_configs())
|
|
2380
2268
|
g_config_path = home_config_path
|
|
2381
|
-
g_ui_path = home_ui_path
|
|
2382
2269
|
g_config = load_config_json(text_from_file(g_config_path))
|
|
2383
2270
|
|
|
2384
2271
|
if not g_providers:
|
|
2385
2272
|
g_providers = json.loads(text_from_file(home_providers_path))
|
|
2386
2273
|
|
|
2387
|
-
if cli_args.
|
|
2274
|
+
if cli_args.update_providers:
|
|
2388
2275
|
asyncio.run(update_providers(home_providers_path))
|
|
2389
2276
|
print(f"Updated {home_providers_path}")
|
|
2390
2277
|
exit(0)
|
|
2391
2278
|
|
|
2279
|
+
# if home_providers_path is older than 1 day, update providers list
|
|
2280
|
+
if (
|
|
2281
|
+
os.path.exists(home_providers_path)
|
|
2282
|
+
and (time.time() - os.path.getmtime(home_providers_path)) > 86400
|
|
2283
|
+
and os.environ.get("LLMS_DISABLE_UPDATE", "") != "1"
|
|
2284
|
+
):
|
|
2285
|
+
try:
|
|
2286
|
+
asyncio.run(update_providers(home_providers_path))
|
|
2287
|
+
_log(f"Updated {home_providers_path}")
|
|
2288
|
+
except Exception as e:
|
|
2289
|
+
_err("Failed to update providers", e)
|
|
2290
|
+
|
|
2392
2291
|
if cli_args.add is not None:
|
|
2393
2292
|
if cli_args.add == "ls":
|
|
2394
2293
|
|
|
@@ -2484,6 +2383,42 @@ def main():
|
|
|
2484
2383
|
|
|
2485
2384
|
exit(0)
|
|
2486
2385
|
|
|
2386
|
+
if cli_args.update:
|
|
2387
|
+
if cli_args.update == "ls":
|
|
2388
|
+
# List installed extensions
|
|
2389
|
+
extensions_path = get_extensions_path()
|
|
2390
|
+
extensions = os.listdir(extensions_path)
|
|
2391
|
+
if len(extensions) == 0:
|
|
2392
|
+
print("No extensions installed.")
|
|
2393
|
+
exit(0)
|
|
2394
|
+
print("Installed extensions:")
|
|
2395
|
+
for extension in extensions:
|
|
2396
|
+
print(f" {extension}")
|
|
2397
|
+
|
|
2398
|
+
print("\nUsage:")
|
|
2399
|
+
print(" llms --update <extension>")
|
|
2400
|
+
print(" llms --update all")
|
|
2401
|
+
exit(0)
|
|
2402
|
+
|
|
2403
|
+
async def update_extensions(extension_name):
|
|
2404
|
+
extensions_path = get_extensions_path()
|
|
2405
|
+
for extension in os.listdir(extensions_path):
|
|
2406
|
+
extension_path = os.path.join(extensions_path, extension)
|
|
2407
|
+
if os.path.isdir(extension_path):
|
|
2408
|
+
if extension_name != "all" and extension != extension_name:
|
|
2409
|
+
continue
|
|
2410
|
+
result = subprocess.run(["git", "pull"], cwd=extension_path, capture_output=True)
|
|
2411
|
+
if result.returncode != 0:
|
|
2412
|
+
print(f"Failed to update extension {extension}: {result.stderr.decode('utf-8')}")
|
|
2413
|
+
continue
|
|
2414
|
+
print(f"Updated extension {extension}")
|
|
2415
|
+
_log(result.stdout.decode("utf-8"))
|
|
2416
|
+
|
|
2417
|
+
asyncio.run(update_extensions(cli_args.update))
|
|
2418
|
+
exit(0)
|
|
2419
|
+
|
|
2420
|
+
load_builtin_extensions()
|
|
2421
|
+
|
|
2487
2422
|
asyncio.run(reload_providers())
|
|
2488
2423
|
|
|
2489
2424
|
install_extensions()
|
|
@@ -2560,10 +2495,6 @@ def main():
|
|
|
2560
2495
|
# Start server
|
|
2561
2496
|
port = int(cli_args.serve)
|
|
2562
2497
|
|
|
2563
|
-
if not os.path.exists(g_ui_path):
|
|
2564
|
-
print(f"UI not found at {g_ui_path}")
|
|
2565
|
-
exit(1)
|
|
2566
|
-
|
|
2567
2498
|
# Validate auth configuration if enabled
|
|
2568
2499
|
auth_enabled = g_config.get("auth", {}).get("enabled", False)
|
|
2569
2500
|
if auth_enabled:
|
|
@@ -2696,8 +2627,9 @@ def main():
|
|
|
2696
2627
|
if provider:
|
|
2697
2628
|
if data.get("enable", False):
|
|
2698
2629
|
provider_config, msg = enable_provider(provider)
|
|
2699
|
-
_log(f"Enabled provider {provider}")
|
|
2700
|
-
|
|
2630
|
+
_log(f"Enabled provider {provider} {msg}")
|
|
2631
|
+
if not msg:
|
|
2632
|
+
await load_llms()
|
|
2701
2633
|
elif data.get("disable", False):
|
|
2702
2634
|
disable_provider(provider)
|
|
2703
2635
|
_log(f"Disabled provider {provider}")
|
|
@@ -3104,19 +3036,18 @@ def main():
|
|
|
3104
3036
|
|
|
3105
3037
|
app.router.add_get("/ui/{path:.*}", ui_static, name="ui_static")
|
|
3106
3038
|
|
|
3107
|
-
async def
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
return web.json_response(ui)
|
|
3039
|
+
async def config_handler(request):
|
|
3040
|
+
ret = {}
|
|
3041
|
+
if "defaults" not in ret:
|
|
3042
|
+
ret["defaults"] = g_config["defaults"]
|
|
3043
|
+
enabled, disabled = provider_status()
|
|
3044
|
+
ret["status"] = {"all": list(g_config["providers"].keys()), "enabled": enabled, "disabled": disabled}
|
|
3045
|
+
# Add auth configuration
|
|
3046
|
+
ret["requiresAuth"] = auth_enabled
|
|
3047
|
+
ret["authType"] = "oauth" if auth_enabled else "apikey"
|
|
3048
|
+
return web.json_response(ret)
|
|
3118
3049
|
|
|
3119
|
-
app.router.add_get("/config",
|
|
3050
|
+
app.router.add_get("/config", config_handler)
|
|
3120
3051
|
|
|
3121
3052
|
async def not_found_handler(request):
|
|
3122
3053
|
return web.Response(text="404: Not Found", status=404)
|
|
@@ -3145,7 +3076,7 @@ def main():
|
|
|
3145
3076
|
async def start_background_tasks(app):
|
|
3146
3077
|
"""Start background tasks when the app starts"""
|
|
3147
3078
|
# Start watching config files in the background
|
|
3148
|
-
asyncio.create_task(watch_config_files(g_config_path,
|
|
3079
|
+
asyncio.create_task(watch_config_files(g_config_path, home_providers_path))
|
|
3149
3080
|
|
|
3150
3081
|
app.on_startup.append(start_background_tasks)
|
|
3151
3082
|
|
|
@@ -3225,6 +3156,7 @@ def main():
|
|
|
3225
3156
|
or cli_args.image is not None
|
|
3226
3157
|
or cli_args.audio is not None
|
|
3227
3158
|
or cli_args.file is not None
|
|
3159
|
+
or cli_args.out is not None
|
|
3228
3160
|
or len(extra_args) > 0
|
|
3229
3161
|
):
|
|
3230
3162
|
try:
|
|
@@ -3235,6 +3167,12 @@ def main():
|
|
|
3235
3167
|
chat = g_config["defaults"]["audio"]
|
|
3236
3168
|
elif cli_args.file is not None:
|
|
3237
3169
|
chat = g_config["defaults"]["file"]
|
|
3170
|
+
elif cli_args.out is not None:
|
|
3171
|
+
template = f"out:{cli_args.out}"
|
|
3172
|
+
if template not in g_config["defaults"]:
|
|
3173
|
+
print(f"Template for output modality '{cli_args.out}' not found")
|
|
3174
|
+
exit(1)
|
|
3175
|
+
chat = g_config["defaults"][template]
|
|
3238
3176
|
if cli_args.chat is not None:
|
|
3239
3177
|
chat_path = os.path.join(os.path.dirname(__file__), cli_args.chat)
|
|
3240
3178
|
if not os.path.exists(chat_path):
|
|
@@ -3286,4 +3224,6 @@ def main():
|
|
|
3286
3224
|
|
|
3287
3225
|
|
|
3288
3226
|
if __name__ == "__main__":
|
|
3227
|
+
if MOCK or DEBUG:
|
|
3228
|
+
print(f"MOCK={MOCK} or DEBUG={DEBUG}")
|
|
3289
3229
|
main()
|