llms-py 3.0.6__py3-none-any.whl → 3.0.7__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.
@@ -370,7 +370,7 @@ export const Analytics = {
370
370
  </div>
371
371
  <div>
372
372
  <div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Duration</div>
373
- <div v-if="request.duration" class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $fmt.humanifyMs(request.duration) }}</div>
373
+ <div v-if="request.duration" class="text-sm font-semibold text-gray-900 dark:text-gray-100">{{ $fmt.humanifyMs(request.duration * 1000) }}</div>
374
374
  </div>
375
375
  <div>
376
376
  <div class="text-xs text-gray-500 dark:text-gray-400 font-medium">Speed</div>
@@ -444,7 +444,9 @@ def install(ctx):
444
444
  input_tokens = usage.get("prompt_tokens", 0)
445
445
  output_tokens = usage.get("completion_tokens", 0)
446
446
  total_tokens = usage.get("total_tokens", input_tokens + output_tokens)
447
- cost = o.get("cost", ((input_price * input_tokens) + (output_price * output_tokens)) / 1000000)
447
+ cost = usage.get("cost") or o.get(
448
+ "cost", ((input_price * input_tokens) + (output_price * output_tokens)) / 1000000
449
+ )
448
450
 
449
451
  request = {
450
452
  "user": user,
@@ -5,6 +5,7 @@ from .google import install_google
5
5
  from .nvidia import install_nvidia
6
6
  from .openai import install_openai
7
7
  from .openrouter import install_openrouter
8
+ from .zai import install_zai
8
9
 
9
10
 
10
11
  def install(ctx):
@@ -15,6 +16,7 @@ def install(ctx):
15
16
  install_nvidia(ctx)
16
17
  install_openai(ctx)
17
18
  install_openrouter(ctx)
19
+ install_zai(ctx)
18
20
 
19
21
 
20
22
  __install__ = install
@@ -221,7 +221,7 @@ def install_anthropic(ctx):
221
221
  # Add metadata
222
222
  if "metadata" not in ret:
223
223
  ret["metadata"] = {}
224
- ret["metadata"]["duration"] = int((time.time() - started_at) * 1000)
224
+ ret["metadata"]["duration"] = int(time.time() - started_at)
225
225
 
226
226
  if chat is not None and "model" in chat:
227
227
  cost = self.model_cost(chat["model"])
@@ -66,15 +66,13 @@ def install_chutes(ctx):
66
66
  if chat["model"] in self.model_negative_prompt:
67
67
  payload["negative_prompt"] = self.negative_prompt
68
68
 
69
- image_config = chat.get("image_config", {})
70
- aspect_ratio = image_config.get("aspect_ratio")
71
- if aspect_ratio:
72
- dimension = ctx.app.aspect_ratios.get(aspect_ratio)
73
- if dimension:
74
- w, h = dimension.split("×")
75
- width, height = int(w), int(h)
76
- payload["width"] = width
77
- payload["height"] = height
69
+ aspect_ratio = ctx.chat_to_aspect_ratio(chat) or "1:1"
70
+ dimension = ctx.app.aspect_ratios.get(aspect_ratio)
71
+ if dimension:
72
+ w, h = dimension.split("×")
73
+ width, height = int(w), int(h)
74
+ payload["width"] = width
75
+ payload["height"] = height
78
76
 
79
77
  if chat["model"] in self.model_resolutions:
80
78
  # if models use resolution, remove width and height
@@ -66,17 +66,15 @@ def install_nvidia(ctx):
66
66
  }
67
67
  modalities = chat.get("modalities", ["text"])
68
68
  if "image" in modalities:
69
- image_config = chat.get("image_config", {})
70
- aspect_ratio = image_config.get("aspect_ratio")
71
- if aspect_ratio:
72
- dimension = ctx.app.aspect_ratios.get(aspect_ratio)
73
- if dimension:
74
- width, height = dimension.split("×")
75
- gen_request["width"] = int(width)
76
- gen_request["height"] = int(height)
77
- else:
78
- gen_request["width"] = self.width
79
- gen_request["height"] = self.height
69
+ aspect_ratio = ctx.chat_to_aspect_ratio(chat) or "1:1"
70
+ dimension = ctx.app.aspect_ratios.get(aspect_ratio)
71
+ if dimension:
72
+ width, height = dimension.split("×")
73
+ gen_request["width"] = int(width)
74
+ gen_request["height"] = int(height)
75
+ else:
76
+ gen_request["width"] = self.width
77
+ gen_request["height"] = self.height
80
78
 
81
79
  gen_request["mode"] = self.mode
82
80
  gen_request["cfg_scale"] = self.cfg_scale
@@ -119,9 +119,7 @@ def install_openai(ctx):
119
119
  if chat["model"] in self.map_image_models:
120
120
  chat["model"] = self.map_image_models[chat["model"]]
121
121
 
122
- aspect_ratio = "1:1"
123
- if "image_config" in chat and "aspect_ratio" in chat["image_config"]:
124
- aspect_ratio = chat["image_config"].get("aspect_ratio", "1:1")
122
+ aspect_ratio = ctx.chat_to_aspect_ratio(chat) or "1:1"
125
123
  payload = {
126
124
  "model": chat["model"],
127
125
  "prompt": ctx.last_user_prompt(chat),
@@ -0,0 +1,182 @@
1
+ import json
2
+ import time
3
+ from typing import Optional
4
+
5
+ import aiohttp
6
+
7
+
8
+ def install_zai(ctx):
9
+ from llms.main import GeneratorBase
10
+
11
+ # https://docs.z.ai/guides/image/glm-image
12
+ class ZaiGenerator(GeneratorBase):
13
+ sdk = "zai/image"
14
+
15
+ def __init__(self, **kwargs):
16
+ super().__init__(**kwargs)
17
+ self.aspect_ratios = {
18
+ "1:1": "1280×1280",
19
+ "2:3": "1056×1568",
20
+ "3:2": "1568×1056",
21
+ "3:4": "1088×1472",
22
+ "4:3": "1472×1088",
23
+ "4:5": "1088×1472",
24
+ "5:4": "1472×1088",
25
+ "9:16": "960×1728",
26
+ "16:9": "1728×960",
27
+ "21:9": "1728×960",
28
+ }
29
+ self.model: str = kwargs.get("model", "glm-image")
30
+ self.n: Optional[int] = kwargs.get("n")
31
+ self.quality: Optional[str] = kwargs.get("quality")
32
+ self.response_format: Optional[str] = kwargs.get("response_format")
33
+ self.size: Optional[str] = kwargs.get("size")
34
+ self.style: Optional[str] = kwargs.get("style")
35
+ self.sensitive_word_check: Optional[str] = kwargs.get("sensitive_word_check")
36
+ self.user: Optional[str] = kwargs.get("user")
37
+ self.request_id: Optional[str] = kwargs.get("request_id")
38
+ self.user_id: Optional[str] = kwargs.get("user_id")
39
+ self.extra_headers: Optional[dict] = kwargs.get("extra_headers")
40
+ self.extra_body: Optional[dict] = kwargs.get("extra_body")
41
+ self.disable_strict_validation: Optional[bool] = kwargs.get("disable_strict_validation")
42
+ self.timeout: Optional[float] = float(kwargs.get("timeout") or 300)
43
+ self.watermark_enabled: Optional[bool] = kwargs.get("watermark_enabled")
44
+
45
+ async def chat(self, chat, provider=None, context=None):
46
+ headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
47
+ if self.extra_headers:
48
+ headers.update(self.extra_headers)
49
+
50
+ chat_url = "https://api.z.ai/api/paas/v4/images/generations"
51
+ if provider is not None:
52
+ headers["Authorization"] = f"Bearer {provider.api_key}"
53
+ chat["model"] = provider.provider_model(chat["model"]) or chat["model"]
54
+ chat_url = provider.api + "/images/generations"
55
+
56
+ body = {}
57
+ attrs = [
58
+ "model",
59
+ "n",
60
+ "quality",
61
+ "response_format",
62
+ "size",
63
+ "style",
64
+ "sensitive_word_check",
65
+ "user",
66
+ "request_id",
67
+ "user_id",
68
+ "disable_strict_validation",
69
+ "watermark_enabled",
70
+ ]
71
+ for attr in attrs:
72
+ if hasattr(self, attr) and getattr(self, attr) is not None:
73
+ body[attr] = getattr(self, attr)
74
+
75
+ if self.extra_body:
76
+ body.update(self.extra_body)
77
+
78
+ if "model" in chat:
79
+ body["model"] = chat["model"]
80
+
81
+ body["prompt"] = ctx.last_user_prompt(chat)
82
+
83
+ aspect_ratio = ctx.chat_to_aspect_ratio(chat) or "1:1"
84
+ size = self.aspect_ratios.get(aspect_ratio, "1280x1280").replace("×", "x")
85
+ body["size"] = size
86
+
87
+ username = ctx.context_to_username(context)
88
+ if username:
89
+ body["user"] = username
90
+
91
+ ctx.dbg(f"ZaiProvider.chat: {chat_url}")
92
+ ctx.dbg(json.dumps(body, indent=2))
93
+ started_at = time.time()
94
+ async with aiohttp.ClientSession() as session, session.post(
95
+ chat_url,
96
+ headers=headers,
97
+ data=json.dumps(body),
98
+ timeout=aiohttp.ClientTimeout(total=self.timeout),
99
+ ) as response:
100
+ # Example Response
101
+ # {
102
+ # "created": 1768451303,
103
+ # "data": [
104
+ # {
105
+ # "url": "https://mfile.z.ai/1768451374203-b334959408a643a8a6c74eb104746dcb.png?ufileattname=202601151228236805d575507d4570_watermark.png"
106
+ # }
107
+ # ],
108
+ # "id": "202601151228236805d575507d4570",
109
+ # "request_id": "202601151228236805d575507d4570",
110
+ # "usage": {
111
+ # "tokens": 0,
112
+ # "price": 0,
113
+ # "cost": 0.0,
114
+ # "duration": 71
115
+ # },
116
+ # "timestamp": 1768451374519,
117
+ # "model": "GLM-Image"
118
+ # }
119
+
120
+ response_json = await self.response_json(response)
121
+ duration = int(time.time() - started_at)
122
+ usage = response_json.get("usage", {})
123
+ if context is not None:
124
+ context["providerResponse"] = response_json
125
+ if "cost" in usage:
126
+ context["cost"] = usage.get("cost")
127
+
128
+ images = []
129
+ for image in response_json.get("data", []):
130
+ url = image.get("url")
131
+ if not url:
132
+ continue
133
+ # download url with aiohttp
134
+ async with session.get(url) as image_response:
135
+ headers = image_response.headers
136
+ # get filename from Content-Disposition
137
+ # attachment; filename="202601151228236805d575507d4570_watermark.png"
138
+ mime_type = headers.get("Content-Type") or "image/png"
139
+ disposition = headers.get("Content-Disposition")
140
+ if disposition:
141
+ start = disposition.index('filename="') + len('filename="')
142
+ end = disposition.index('"', start)
143
+ filename = disposition[start:end]
144
+ else:
145
+ ext = mime_type.split("/")[1]
146
+ filename = f"{body['model'].lower()}-{response_json.get('id', int(started_at))}.{ext}"
147
+ image_bytes = await image_response.read()
148
+
149
+ info = {
150
+ "prompt": body["prompt"],
151
+ "type": mime_type,
152
+ "width": int(size.split("x")[0]),
153
+ "height": int(size.split("x")[1]),
154
+ "duration": duration,
155
+ }
156
+ info.update(usage)
157
+ cache_url, info = ctx.save_image_to_cache(
158
+ image_bytes, filename, image_info=info, ignore_info=True
159
+ )
160
+
161
+ images.append(
162
+ {
163
+ "type": "image_url",
164
+ "image_url": {
165
+ "url": cache_url,
166
+ },
167
+ }
168
+ )
169
+
170
+ chat_response = {
171
+ "choices": [{"message": {"role": "assistant", "content": self.default_content, "images": images}}],
172
+ "created": int(time.time()),
173
+ "usage": {
174
+ "prompt_tokens": 0,
175
+ "completion_tokens": 1_000_000, # Price per image is 0.015, so 1M token is 0.015
176
+ },
177
+ }
178
+ if "cost" in usage:
179
+ chat_response["cost"] = usage["cost"]
180
+ return ctx.log_json(chat_response)
181
+
182
+ ctx.add_provider(ZaiGenerator)
llms/llms.json CHANGED
@@ -223,7 +223,13 @@
223
223
  },
224
224
  "zai-coding-plan": {
225
225
  "enabled": true,
226
- "temperature": 0.7
226
+ "temperature": 0.7,
227
+ "modalities": {
228
+ "image": {
229
+ "name": "Z.ai Image",
230
+ "npm": "zai/image"
231
+ }
232
+ }
227
233
  },
228
234
  "minimax": {
229
235
  "enabled": true,
@@ -314,7 +320,13 @@
314
320
  },
315
321
  "zai": {
316
322
  "enabled": true,
317
- "temperature": 0.7
323
+ "temperature": 0.7,
324
+ "modalities": {
325
+ "image": {
326
+ "name": "Z.ai Image",
327
+ "npm": "zai/image"
328
+ }
329
+ }
318
330
  },
319
331
  "mistral": {
320
332
  "enabled": true,
llms/main.py CHANGED
@@ -41,7 +41,7 @@ try:
41
41
  except ImportError:
42
42
  HAS_PIL = False
43
43
 
44
- VERSION = "3.0.6"
44
+ VERSION = "3.0.7"
45
45
  _ROOT = None
46
46
  DEBUG = os.getenv("DEBUG") == "1"
47
47
  MOCK = os.getenv("MOCK") == "1"
@@ -757,6 +757,12 @@ def chat_to_username(chat):
757
757
  return None
758
758
 
759
759
 
760
+ def chat_to_aspect_ratio(chat):
761
+ if "image_config" in chat and "aspect_ratio" in chat["image_config"]:
762
+ return chat["image_config"]["aspect_ratio"]
763
+ return None
764
+
765
+
760
766
  def last_user_prompt(chat):
761
767
  prompt = ""
762
768
  if "messages" in chat:
@@ -2539,8 +2545,8 @@ class ExtensionContext:
2539
2545
  def to_file_info(self, chat, info=None, response=None):
2540
2546
  return to_file_info(chat, info=info, response=response)
2541
2547
 
2542
- def save_image_to_cache(self, base64_data, filename, image_info):
2543
- return save_image_to_cache(base64_data, filename, image_info)
2548
+ def save_image_to_cache(self, base64_data, filename, image_info, ignore_info=False):
2549
+ return save_image_to_cache(base64_data, filename, image_info, ignore_info=ignore_info)
2544
2550
 
2545
2551
  def save_bytes_to_cache(self, bytes_data, filename, file_info):
2546
2552
  return save_bytes_to_cache(bytes_data, filename, file_info)
@@ -2692,6 +2698,11 @@ class ExtensionContext:
2692
2698
  def get_user_path(self, username=None):
2693
2699
  return self.app.get_user_path(username)
2694
2700
 
2701
+ def context_to_username(self, context):
2702
+ if context and "request" in context:
2703
+ return self.get_username(context["request"])
2704
+ return None
2705
+
2695
2706
  def should_cancel_thread(self, context):
2696
2707
  return should_cancel_thread(context)
2697
2708
 
@@ -2704,6 +2715,9 @@ class ExtensionContext:
2704
2715
  def create_chat_with_tools(self, chat, use_tools="all"):
2705
2716
  return self.app.create_chat_with_tools(chat, use_tools)
2706
2717
 
2718
+ def chat_to_aspect_ratio(self, chat):
2719
+ return chat_to_aspect_ratio(chat)
2720
+
2707
2721
 
2708
2722
  def get_extensions_path():
2709
2723
  return os.getenv("LLMS_EXTENSIONS_DIR", home_llms_path("extensions"))
llms/providers-extra.json CHANGED
@@ -352,5 +352,43 @@
352
352
  }
353
353
  }
354
354
  }
355
+ },
356
+ "zai": {
357
+ "models": {
358
+ "glm-image": {
359
+ "name": "GLM-Image",
360
+ "modalities": {
361
+ "input": [
362
+ "text"
363
+ ],
364
+ "output": [
365
+ "image"
366
+ ]
367
+ },
368
+ "cost": {
369
+ "input": 0,
370
+ "output": 0.015
371
+ }
372
+ }
373
+ }
374
+ },
375
+ "zai-coding-plan": {
376
+ "models": {
377
+ "glm-image": {
378
+ "name": "GLM-Image",
379
+ "modalities": {
380
+ "input": [
381
+ "text"
382
+ ],
383
+ "output": [
384
+ "image"
385
+ ]
386
+ },
387
+ "cost": {
388
+ "input": 0,
389
+ "output": 0.015
390
+ }
391
+ }
392
+ }
355
393
  }
356
394
  }