llms-py 2.0.6__tar.gz → 2.0.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {llms_py-2.0.6/llms_py.egg-info → llms_py-2.0.7}/PKG-INFO +9 -1
  2. {llms_py-2.0.6 → llms_py-2.0.7}/README.md +8 -0
  3. {llms_py-2.0.6 → llms_py-2.0.7}/llms.py +12 -2
  4. {llms_py-2.0.6 → llms_py-2.0.7/llms_py.egg-info}/PKG-INFO +9 -1
  5. {llms_py-2.0.6 → llms_py-2.0.7}/pyproject.toml +1 -1
  6. {llms_py-2.0.6 → llms_py-2.0.7}/setup.py +1 -1
  7. {llms_py-2.0.6 → llms_py-2.0.7}/ui/ChatPrompt.mjs +8 -6
  8. {llms_py-2.0.6 → llms_py-2.0.7}/ui/Main.mjs +33 -1
  9. {llms_py-2.0.6 → llms_py-2.0.7}/ui/app.css +36 -58
  10. {llms_py-2.0.6 → llms_py-2.0.7}/LICENSE +0 -0
  11. {llms_py-2.0.6 → llms_py-2.0.7}/MANIFEST.in +0 -0
  12. {llms_py-2.0.6 → llms_py-2.0.7}/index.html +0 -0
  13. {llms_py-2.0.6 → llms_py-2.0.7}/llms.json +0 -0
  14. {llms_py-2.0.6 → llms_py-2.0.7}/llms_py.egg-info/SOURCES.txt +0 -0
  15. {llms_py-2.0.6 → llms_py-2.0.7}/llms_py.egg-info/dependency_links.txt +0 -0
  16. {llms_py-2.0.6 → llms_py-2.0.7}/llms_py.egg-info/entry_points.txt +0 -0
  17. {llms_py-2.0.6 → llms_py-2.0.7}/llms_py.egg-info/not-zip-safe +0 -0
  18. {llms_py-2.0.6 → llms_py-2.0.7}/llms_py.egg-info/requires.txt +0 -0
  19. {llms_py-2.0.6 → llms_py-2.0.7}/llms_py.egg-info/top_level.txt +0 -0
  20. {llms_py-2.0.6 → llms_py-2.0.7}/requirements.txt +0 -0
  21. {llms_py-2.0.6 → llms_py-2.0.7}/setup.cfg +0 -0
  22. {llms_py-2.0.6 → llms_py-2.0.7}/ui/App.mjs +0 -0
  23. {llms_py-2.0.6 → llms_py-2.0.7}/ui/Recents.mjs +0 -0
  24. {llms_py-2.0.6 → llms_py-2.0.7}/ui/Sidebar.mjs +0 -0
  25. {llms_py-2.0.6 → llms_py-2.0.7}/ui/fav.svg +0 -0
  26. {llms_py-2.0.6 → llms_py-2.0.7}/ui/lib/highlight.min.mjs +0 -0
  27. {llms_py-2.0.6 → llms_py-2.0.7}/ui/lib/idb.min.mjs +0 -0
  28. {llms_py-2.0.6 → llms_py-2.0.7}/ui/lib/marked.min.mjs +0 -0
  29. {llms_py-2.0.6 → llms_py-2.0.7}/ui/lib/servicestack-client.min.mjs +0 -0
  30. {llms_py-2.0.6 → llms_py-2.0.7}/ui/lib/servicestack-vue.min.mjs +0 -0
  31. {llms_py-2.0.6 → llms_py-2.0.7}/ui/lib/vue-router.min.mjs +0 -0
  32. {llms_py-2.0.6 → llms_py-2.0.7}/ui/lib/vue.min.mjs +0 -0
  33. {llms_py-2.0.6 → llms_py-2.0.7}/ui/lib/vue.mjs +0 -0
  34. {llms_py-2.0.6 → llms_py-2.0.7}/ui/markdown.mjs +0 -0
  35. {llms_py-2.0.6 → llms_py-2.0.7}/ui/tailwind.input.css +0 -0
  36. {llms_py-2.0.6 → llms_py-2.0.7}/ui/threadStore.mjs +0 -0
  37. {llms_py-2.0.6 → llms_py-2.0.7}/ui/typography.css +0 -0
  38. {llms_py-2.0.6 → llms_py-2.0.7}/ui/utils.mjs +0 -0
  39. {llms_py-2.0.6 → llms_py-2.0.7}/ui.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llms-py
3
- Version: 2.0.6
3
+ Version: 2.0.7
4
4
  Summary: A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers
5
5
  Home-page: https://github.com/ServiceStack/llms
6
6
  Author: ServiceStack
@@ -63,6 +63,14 @@ Configure additional providers and models in [llms.json](llms.json)
63
63
  - **Unified Models**: Define custom model names that map to different provider-specific names
64
64
  - **Multi-Model Support**: Support for over 160+ different LLMs
65
65
 
66
+ ## llms.py UI
67
+
68
+ Simple ChatGPT-like UI to access ALL Your LLMs, Locally or Remotely!
69
+
70
+ [![llms.py UI](https://servicestack.net/img/posts/llms-py-ui/bg.webp)](https://servicestack.net/posts/llms-py-ui)
71
+
72
+ Read the [Introductory Blog Post](https://servicestack.net/posts/llms-py-ui).
73
+
66
74
  ## Installation
67
75
 
68
76
  ### Option 1: Install from PyPI
@@ -23,6 +23,14 @@ Configure additional providers and models in [llms.json](llms.json)
23
23
  - **Unified Models**: Define custom model names that map to different provider-specific names
24
24
  - **Multi-Model Support**: Support for over 160+ different LLMs
25
25
 
26
+ ## llms.py UI
27
+
28
+ Simple ChatGPT-like UI to access ALL Your LLMs, Locally or Remotely!
29
+
30
+ [![llms.py UI](https://servicestack.net/img/posts/llms-py-ui/bg.webp)](https://servicestack.net/posts/llms-py-ui)
31
+
32
+ Read the [Introductory Blog Post](https://servicestack.net/posts/llms-py-ui).
33
+
26
34
  ## Installation
27
35
 
28
36
  ### Option 1: Install from PyPI
@@ -21,7 +21,7 @@ from aiohttp import web
21
21
  from pathlib import Path
22
22
  from importlib import resources # Py≥3.9 (pip install importlib_resources for 3.7/3.8)
23
23
 
24
- VERSION = "2.0.6"
24
+ VERSION = "2.0.7"
25
25
  _ROOT = None
26
26
  g_config_path = None
27
27
  g_ui_path = None
@@ -66,6 +66,16 @@ def chat_summary(chat):
66
66
  item['file']['file_data'] = f"({len(data)})"
67
67
  return json.dumps(clone, indent=2)
68
68
 
69
+ def gemini_chat_summary(gemini_chat):
70
+ """Summarize Gemini chat completion request for logging. Replace inline_data with size of content only"""
71
+ clone = json.loads(json.dumps(gemini_chat))
72
+ for content in clone['contents']:
73
+ for part in content['parts']:
74
+ if 'inline_data' in part:
75
+ data = part['inline_data']['data']
76
+ part['inline_data']['data'] = f"({len(data)})"
77
+ return json.dumps(clone, indent=2)
78
+
69
79
  image_exts = 'png,webp,jpg,jpeg,gif,bmp,svg,tiff,ico'.split(',')
70
80
  audio_exts = 'mp3,wav,ogg,flac,m4a,opus,webm'.split(',')
71
81
 
@@ -417,7 +427,7 @@ class GoogleProvider(OpenAiProvider):
417
427
  gemini_chat_url = f"https://generativelanguage.googleapis.com/v1beta/models/{chat['model']}:generateContent?key={self.api_key}"
418
428
 
419
429
  _log(f"POST {gemini_chat_url}")
420
- _log(json.dumps(gemini_chat, indent=2))
430
+ _log(gemini_chat_summary(gemini_chat))
421
431
 
422
432
  if self.curl:
423
433
  curl_args = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llms-py
3
- Version: 2.0.6
3
+ Version: 2.0.7
4
4
  Summary: A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers
5
5
  Home-page: https://github.com/ServiceStack/llms
6
6
  Author: ServiceStack
@@ -63,6 +63,14 @@ Configure additional providers and models in [llms.json](llms.json)
63
63
  - **Unified Models**: Define custom model names that map to different provider-specific names
64
64
  - **Multi-Model Support**: Support for over 160+ different LLMs
65
65
 
66
+ ## llms.py UI
67
+
68
+ Simple ChatGPT-like UI to access ALL Your LLMs, Locally or Remotely!
69
+
70
+ [![llms.py UI](https://servicestack.net/img/posts/llms-py-ui/bg.webp)](https://servicestack.net/posts/llms-py-ui)
71
+
72
+ Read the [Introductory Blog Post](https://servicestack.net/posts/llms-py-ui).
73
+
66
74
  ## Installation
67
75
 
68
76
  ### Option 1: Install from PyPI
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "llms-py"
7
- version = "2.0.6"
7
+ version = "2.0.7"
8
8
  description = "A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -16,7 +16,7 @@ with open(os.path.join(this_directory, "requirements.txt"), encoding="utf-8") as
16
16
 
17
17
  setup(
18
18
  name="llms-py",
19
- version="2.0.6",
19
+ version="2.0.7",
20
20
  author="ServiceStack",
21
21
  author_email="team@servicestack.net",
22
22
  description="A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers",
@@ -146,12 +146,14 @@ export default {
146
146
  // allow re-selecting the same file
147
147
  if (fileInput.value) fileInput.value.value = ''
148
148
 
149
- if (hasImage()) {
150
- messageText.value = getTextContent(config.defaults.image)
151
- } else if (hasAudio()) {
152
- messageText.value = getTextContent(config.defaults.audio)
153
- } else {
154
- messageText.value = getTextContent(config.defaults.file)
149
+ if (!messageText.value.trim()) {
150
+ if (hasImage()) {
151
+ messageText.value = getTextContent(config.defaults.image)
152
+ } else if (hasAudio()) {
153
+ messageText.value = getTextContent(config.defaults.audio)
154
+ } else {
155
+ messageText.value = getTextContent(config.defaults.file)
156
+ }
155
157
  }
156
158
  }
157
159
  const removeAttachment = (i) => {
@@ -274,11 +274,25 @@ export default {
274
274
 
275
275
  <!-- Message bubble -->
276
276
  <div
277
- class="message rounded-lg px-4 py-3"
277
+ class="message rounded-lg px-4 py-3 relative group"
278
278
  :class="message.role === 'user'
279
279
  ? 'bg-blue-600 text-white'
280
280
  : 'bg-gray-100 text-gray-900 border border-gray-200'"
281
281
  >
282
+ <!-- Copy button in top right corner -->
283
+ <button
284
+ type="button"
285
+ @click="copyMessageContent(message)"
286
+ class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 p-1 rounded hover:bg-black/10 focus:outline-none focus:ring-2 focus:ring-blue-500"
287
+ :class="message.role === 'user' ? 'text-white/70 hover:text-white hover:bg-white/20' : 'text-gray-500 hover:text-gray-700'"
288
+ title="Copy message content"
289
+ >
290
+ <svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
291
+ <rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
292
+ <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
293
+ </svg>
294
+ </button>
295
+
282
296
  <div
283
297
  v-if="message.role === 'assistant'"
284
298
  v-html="renderMarkdown(message.content)"
@@ -612,6 +626,23 @@ export default {
612
626
  }
613
627
  const formatReasoning = (r) => typeof r === 'string' ? r : JSON.stringify(r, null, 2)
614
628
 
629
+ // Copy message content to clipboard
630
+ const copyMessageContent = async (message) => {
631
+ try {
632
+ await navigator.clipboard.writeText(message.content)
633
+ // Could add a toast notification here if desired
634
+ } catch (err) {
635
+ console.error('Failed to copy message content:', err)
636
+ // Fallback for older browsers
637
+ const textArea = document.createElement('textarea')
638
+ textArea.value = message.content
639
+ document.body.appendChild(textArea)
640
+ textArea.select()
641
+ document.execCommand('copy')
642
+ document.body.removeChild(textArea)
643
+ }
644
+ }
645
+
615
646
  onMounted(() => {
616
647
  setTimeout(addCopyButtons, 1)
617
648
  })
@@ -636,6 +667,7 @@ export default {
636
667
  isReasoningExpanded,
637
668
  toggleReasoning,
638
669
  formatReasoning,
670
+ copyMessageContent,
639
671
  configUpdated,
640
672
  exportThreads,
641
673
  isExporting,
@@ -135,7 +135,6 @@
135
135
  --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
136
136
  --default-font-family: var(--font-sans);
137
137
  --default-mono-font-family: var(--font-mono);
138
- --default-ring-color: hsl(var(--ring));
139
138
  }
140
139
  }
141
140
  @layer base {
@@ -331,6 +330,9 @@
331
330
  .top-0 {
332
331
  top: calc(var(--spacing) * 0);
333
332
  }
333
+ .top-2 {
334
+ top: calc(var(--spacing) * 2);
335
+ }
334
336
  .right-0 {
335
337
  right: calc(var(--spacing) * 0);
336
338
  }
@@ -376,15 +378,9 @@
376
378
  max-width: 96rem;
377
379
  }
378
380
  }
379
- .-m-2 {
380
- margin: calc(var(--spacing) * -2);
381
- }
382
381
  .-m-2\.5 {
383
382
  margin: calc(var(--spacing) * -2.5);
384
383
  }
385
- .-mx-1 {
386
- margin-inline: calc(var(--spacing) * -1);
387
- }
388
384
  .-mx-1\.5 {
389
385
  margin-inline: calc(var(--spacing) * -1.5);
390
386
  }
@@ -394,9 +390,6 @@
394
390
  .mx-auto {
395
391
  margin-inline: auto;
396
392
  }
397
- .-my-1 {
398
- margin-block: calc(var(--spacing) * -1);
399
- }
400
393
  .-my-1\.5 {
401
394
  margin-block: calc(var(--spacing) * -1.5);
402
395
  }
@@ -457,15 +450,9 @@
457
450
  .mb-4 {
458
451
  margin-bottom: calc(var(--spacing) * 4);
459
452
  }
460
- .mb-6 {
461
- margin-bottom: calc(var(--spacing) * 6);
462
- }
463
453
  .-ml-px {
464
454
  margin-left: -1px;
465
455
  }
466
- .ml-0 {
467
- margin-left: calc(var(--spacing) * 0);
468
- }
469
456
  .ml-0\.5 {
470
457
  margin-left: calc(var(--spacing) * 0.5);
471
458
  }
@@ -636,9 +623,6 @@
636
623
  .w-80 {
637
624
  width: calc(var(--spacing) * 80);
638
625
  }
639
- .w-84 {
640
- width: calc(var(--spacing) * 84);
641
- }
642
626
  .w-full {
643
627
  width: 100%;
644
628
  }
@@ -1034,9 +1018,6 @@
1034
1018
  .bg-gray-400 {
1035
1019
  background-color: var(--color-gray-400);
1036
1020
  }
1037
- .bg-gray-500 {
1038
- background-color: var(--color-gray-500);
1039
- }
1040
1021
  .bg-gray-500\/75 {
1041
1022
  background-color: color-mix(in srgb, oklch(55.1% 0.027 264.364) 75%, transparent);
1042
1023
  @supports (color: color-mix(in lab, red, red)) {
@@ -1049,9 +1030,6 @@
1049
1030
  .bg-gray-700 {
1050
1031
  background-color: var(--color-gray-700);
1051
1032
  }
1052
- .bg-gray-900 {
1053
- background-color: var(--color-gray-900);
1054
- }
1055
1033
  .bg-gray-900\/80 {
1056
1034
  background-color: color-mix(in srgb, oklch(21% 0.034 264.665) 80%, transparent);
1057
1035
  @supports (color: color-mix(in lab, red, red)) {
@@ -1145,9 +1123,6 @@
1145
1123
  .px-6 {
1146
1124
  padding-inline: calc(var(--spacing) * 6);
1147
1125
  }
1148
- .py-0 {
1149
- padding-block: calc(var(--spacing) * 0);
1150
- }
1151
1126
  .py-0\.5 {
1152
1127
  padding-block: calc(var(--spacing) * 0.5);
1153
1128
  }
@@ -1178,9 +1153,6 @@
1178
1153
  .py-12 {
1179
1154
  padding-block: calc(var(--spacing) * 12);
1180
1155
  }
1181
- .pt-0 {
1182
- padding-top: calc(var(--spacing) * 0);
1183
- }
1184
1156
  .pt-0\.5 {
1185
1157
  padding-top: calc(var(--spacing) * 0.5);
1186
1158
  }
@@ -1447,15 +1419,18 @@
1447
1419
  .text-sky-600 {
1448
1420
  color: var(--color-sky-600);
1449
1421
  }
1450
- .text-slate-300 {
1451
- color: var(--color-slate-300);
1452
- }
1453
1422
  .text-slate-500 {
1454
1423
  color: var(--color-slate-500);
1455
1424
  }
1456
1425
  .text-white {
1457
1426
  color: var(--color-white);
1458
1427
  }
1428
+ .text-white\/70 {
1429
+ color: color-mix(in srgb, #fff 70%, transparent);
1430
+ @supports (color: color-mix(in lab, red, red)) {
1431
+ color: color-mix(in oklab, var(--color-white) 70%, transparent);
1432
+ }
1433
+ }
1459
1434
  .text-yellow-400 {
1460
1435
  color: var(--color-yellow-400);
1461
1436
  }
@@ -1474,9 +1449,6 @@
1474
1449
  .uppercase {
1475
1450
  text-transform: uppercase;
1476
1451
  }
1477
- .underline {
1478
- text-decoration-line: underline;
1479
- }
1480
1452
  .placeholder-gray-500 {
1481
1453
  &::placeholder {
1482
1454
  color: var(--color-gray-500);
@@ -1534,9 +1506,6 @@
1534
1506
  --tw-inset-ring-shadow: inset 0 0 0 1px var(--tw-inset-ring-color, currentcolor);
1535
1507
  box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
1536
1508
  }
1537
- .ring-black {
1538
- --tw-ring-color: var(--color-black);
1539
- }
1540
1509
  .ring-black\/5 {
1541
1510
  --tw-ring-color: color-mix(in srgb, #000 5%, transparent);
1542
1511
  @supports (color: color-mix(in lab, red, red)) {
@@ -1546,9 +1515,6 @@
1546
1515
  .ring-indigo-500 {
1547
1516
  --tw-ring-color: var(--color-indigo-500);
1548
1517
  }
1549
- .inset-ring-gray-900 {
1550
- --tw-inset-ring-color: var(--color-gray-900);
1551
- }
1552
1518
  .inset-ring-gray-900\/5 {
1553
1519
  --tw-inset-ring-color: color-mix(in srgb, oklch(21% 0.034 264.665) 5%, transparent);
1554
1520
  @supports (color: color-mix(in lab, red, red)) {
@@ -1791,6 +1757,16 @@
1791
1757
  }
1792
1758
  }
1793
1759
  }
1760
+ .hover\:bg-black\/10 {
1761
+ &:hover {
1762
+ @media (hover: hover) {
1763
+ background-color: color-mix(in srgb, #000 10%, transparent);
1764
+ @supports (color: color-mix(in lab, red, red)) {
1765
+ background-color: color-mix(in oklab, var(--color-black) 10%, transparent);
1766
+ }
1767
+ }
1768
+ }
1769
+ }
1794
1770
  .hover\:bg-blue-100 {
1795
1771
  &:hover {
1796
1772
  @media (hover: hover) {
@@ -1889,6 +1865,16 @@
1889
1865
  }
1890
1866
  }
1891
1867
  }
1868
+ .hover\:bg-white\/20 {
1869
+ &:hover {
1870
+ @media (hover: hover) {
1871
+ background-color: color-mix(in srgb, #fff 20%, transparent);
1872
+ @supports (color: color-mix(in lab, red, red)) {
1873
+ background-color: color-mix(in oklab, var(--color-white) 20%, transparent);
1874
+ }
1875
+ }
1876
+ }
1877
+ }
1892
1878
  .hover\:bg-yellow-50 {
1893
1879
  &:hover {
1894
1880
  @media (hover: hover) {
@@ -2022,6 +2008,13 @@
2022
2008
  }
2023
2009
  }
2024
2010
  }
2011
+ .hover\:text-white {
2012
+ &:hover {
2013
+ @media (hover: hover) {
2014
+ color: var(--color-white);
2015
+ }
2016
+ }
2017
+ }
2025
2018
  .hover\:opacity-70 {
2026
2019
  &:hover {
2027
2020
  @media (hover: hover) {
@@ -2515,11 +2508,6 @@
2515
2508
  display: table-cell;
2516
2509
  }
2517
2510
  }
2518
- .md\:w-84 {
2519
- @media (width >= 48rem) {
2520
- width: calc(var(--spacing) * 84);
2521
- }
2522
- }
2523
2511
  .md\:max-w-xl {
2524
2512
  @media (width >= 48rem) {
2525
2513
  max-width: var(--container-xl);
@@ -2570,16 +2558,6 @@
2570
2558
  width: calc(var(--spacing) * 72);
2571
2559
  }
2572
2560
  }
2573
- .lg\:w-80 {
2574
- @media (width >= 64rem) {
2575
- width: calc(var(--spacing) * 80);
2576
- }
2577
- }
2578
- .lg\:w-84 {
2579
- @media (width >= 64rem) {
2580
- width: calc(var(--spacing) * 84);
2581
- }
2582
- }
2583
2561
  .lg\:max-w-screen-md {
2584
2562
  @media (width >= 64rem) {
2585
2563
  max-width: var(--breakpoint-md);
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes