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.
Files changed (51) hide show
  1. llms/__pycache__/main.cpython-314.pyc +0 -0
  2. llms/index.html +2 -1
  3. llms/llms.json +50 -17
  4. llms/main.py +484 -544
  5. llms/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  6. llms/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  7. llms/providers/__pycache__/google.cpython-314.pyc +0 -0
  8. llms/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
  9. llms/providers/__pycache__/openai.cpython-314.pyc +0 -0
  10. llms/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  11. llms/providers/anthropic.py +189 -0
  12. llms/providers/chutes.py +152 -0
  13. llms/providers/google.py +306 -0
  14. llms/providers/nvidia.py +107 -0
  15. llms/providers/openai.py +159 -0
  16. llms/providers/openrouter.py +70 -0
  17. llms/providers-extra.json +356 -0
  18. llms/providers.json +1 -1
  19. llms/ui/App.mjs +132 -60
  20. llms/ui/ai.mjs +76 -10
  21. llms/ui/app.css +65 -28
  22. llms/ui/ctx.mjs +196 -0
  23. llms/ui/index.mjs +75 -171
  24. llms/ui/lib/charts.mjs +9 -13
  25. llms/ui/markdown.mjs +6 -0
  26. llms/ui/{Analytics.mjs → modules/analytics.mjs} +76 -64
  27. llms/ui/{Main.mjs → modules/chat/ChatBody.mjs} +59 -135
  28. llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +8 -8
  29. llms/ui/{ChatPrompt.mjs → modules/chat/index.mjs} +242 -46
  30. llms/ui/modules/layout.mjs +267 -0
  31. llms/ui/modules/model-selector.mjs +851 -0
  32. llms/ui/{Recents.mjs → modules/threads/Recents.mjs} +0 -2
  33. llms/ui/{Sidebar.mjs → modules/threads/index.mjs} +46 -44
  34. llms/ui/{threadStore.mjs → modules/threads/threadStore.mjs} +10 -7
  35. llms/ui/utils.mjs +82 -123
  36. {llms_py-3.0.0b2.dist-info → llms_py-3.0.0b4.dist-info}/METADATA +1 -1
  37. llms_py-3.0.0b4.dist-info/RECORD +65 -0
  38. llms/ui/Avatar.mjs +0 -86
  39. llms/ui/Brand.mjs +0 -52
  40. llms/ui/OAuthSignIn.mjs +0 -61
  41. llms/ui/ProviderIcon.mjs +0 -36
  42. llms/ui/ProviderStatus.mjs +0 -104
  43. llms/ui/SignIn.mjs +0 -65
  44. llms/ui/Welcome.mjs +0 -8
  45. llms/ui/model-selector.mjs +0 -686
  46. llms/ui.json +0 -1069
  47. llms_py-3.0.0b2.dist-info/RECORD +0 -58
  48. {llms_py-3.0.0b2.dist-info → llms_py-3.0.0b4.dist-info}/WHEEL +0 -0
  49. {llms_py-3.0.0b2.dist-info → llms_py-3.0.0b4.dist-info}/entry_points.txt +0 -0
  50. {llms_py-3.0.0b2.dist-info → llms_py-3.0.0b4.dist-info}/licenses/LICENSE +0 -0
  51. {llms_py-3.0.0b2.dist-info → llms_py-3.0.0b4.dist-info}/top_level.txt +0 -0
@@ -1,88 +1,9 @@
1
- import { ref, computed, nextTick, watch, onMounted, provide, inject } from 'vue'
1
+ import { ref, computed, nextTick, watch, onMounted, onUnmounted, inject } from 'vue'
2
2
  import { useRouter, useRoute } from 'vue-router'
3
- import { useFormatters } from '@servicestack/vue'
4
- import { useThreadStore } from './threadStore.mjs'
5
- import { addCopyButtons, formatCost, statsTitle, fetchCacheInfos } from './utils.mjs'
6
- import { renderMarkdown } from './markdown.mjs'
7
- import ChatPrompt, { useChatPrompt } from './ChatPrompt.mjs'
8
- import SignIn from './SignIn.mjs'
9
- import OAuthSignIn from './OAuthSignIn.mjs'
10
- import Avatar from './Avatar.mjs'
11
- import { useSettings } from "./SettingsDialog.mjs"
12
- import Welcome from './Welcome.mjs'
13
-
14
- const { humanifyMs, humanifyNumber } = useFormatters()
15
-
16
- const TopBar = {
17
- template: `
18
- <div class="flex space-x-2">
19
- <div v-for="(ext, index) in extensions" :key="ext.id" class="relative flex items-center justify-center">
20
- <component :is="ext.topBarIcon"
21
- class="size-7 p-1 cursor-pointer text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 block"
22
- :class="{ 'bg-gray-100 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded' : ext.isActive($layout.top) }"
23
- @mouseenter="tooltip = ext.name"
24
- @mouseleave="tooltip = ''"
25
- />
26
- <div v-if="tooltip === ext.name"
27
- class="absolute top-full mt-2 px-2 py-1 text-xs text-white bg-gray-900 dark:bg-gray-800 rounded shadow-md z-50 whitespace-nowrap pointer-events-none"
28
- :class="index <= extensions.length - 1 ? 'right-0' : 'left-1/2 -translate-x-1/2'">
29
- {{ext.name}}
30
- </div>
31
- </div>
32
- </div>
33
- `,
34
- setup() {
35
- const ctx = inject('ctx')
36
- const tooltip = ref('')
37
- const extensions = computed(() => ctx.extensions.filter(x => x.topBarIcon))
38
- return {
39
- extensions,
40
- tooltip,
41
- }
42
- }
43
- }
44
-
45
- const TopPanel = {
46
- template: `
47
- <component v-if="component" :is="component" />
48
- `,
49
- setup() {
50
- const ctx = inject('ctx')
51
- const component = computed(() => ctx.component(ctx.layout.top))
52
- return {
53
- component,
54
- }
55
- }
56
- }
57
3
 
58
4
  export default {
59
- components: {
60
- TopBar,
61
- TopPanel,
62
- ChatPrompt,
63
- SignIn,
64
- OAuthSignIn,
65
- Avatar,
66
- Welcome,
67
- },
68
5
  template: `
69
- <div class="flex flex-col h-full w-full">
70
- <!-- Header with model selectors -->
71
- <div v-if="$ai.hasAccess"
72
- :class="!$ai.isSidebarOpen ? 'pl-6' : ''"
73
- class="flex items-center border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-2 w-full min-h-16">
74
- <div class="flex flex-wrap items-center justify-between w-full">
75
- <ModelSelector :models="models" v-model="selectedModel" @updated="configUpdated" />
76
-
77
- <div class="flex items-center space-x-2 pl-4">
78
- <TopBar />
79
- <Avatar />
80
- </div>
81
- </div>
82
- </div>
83
-
84
- <TopPanel />
85
-
6
+ <div class="flex flex-col h-full">
86
7
  <!-- Messages Area -->
87
8
  <div class="flex-1 overflow-y-auto" ref="messagesContainer">
88
9
  <div class="mx-auto max-w-6xl px-4 py-6">
@@ -148,7 +69,14 @@ export default {
148
69
  </div>
149
70
 
150
71
  <!-- Messages -->
151
- <div v-else class="space-y-6">
72
+ <div v-else class="space-y-2">
73
+ <div v-if="currentThread?.messages.length && currentThread?.model" class="flex items-center justify-center select-none">
74
+ <span @click="$chat.setSelectedModel({ name: currentThread.model})"
75
+ class="flex items-center cursor-pointer px-1.5 py-0.5 text-xs rounded text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-gray-100 transition-colors border hover:border-gray-300 dark:hover:border-gray-700">
76
+ <ProviderIcon class="size-4 mr-1" :provider="$chat.getProviderForModel(currentThread.model)" />
77
+ {{currentThread.model}}
78
+ </span>
79
+ </div>
152
80
  <div
153
81
  v-for="message in currentThread.messages"
154
82
  :key="message.id"
@@ -199,7 +127,7 @@ export default {
199
127
 
200
128
  <div
201
129
  v-if="message.role === 'assistant'"
202
- v-html="renderMarkdown(message.content)"
130
+ v-html="$fmt.markdown(message.content)"
203
131
  class="prose prose-sm max-w-none dark:prose-invert"
204
132
  ></div>
205
133
 
@@ -210,14 +138,23 @@ export default {
210
138
  <span>{{ isReasoningExpanded(message.id) ? 'Hide reasoning' : 'Show reasoning' }}</span>
211
139
  </button>
212
140
  <div v-if="isReasoningExpanded(message.id)" class="mt-2 rounded border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 p-2">
213
- <div v-if="typeof message.reasoning === 'string'" v-html="renderMarkdown(message.reasoning)" class="prose prose-xs max-w-none dark:prose-invert"></div>
141
+ <div v-if="typeof message.reasoning === 'string'" v-html="$fmt.markdown(message.reasoning)" class="prose prose-xs max-w-none dark:prose-invert"></div>
214
142
  <pre v-else class="text-xs whitespace-pre-wrap overflow-x-auto text-gray-900 dark:text-gray-100">{{ formatReasoning(message.reasoning) }}</pre>
215
143
  </div>
216
144
  </div>
217
145
 
146
+ <!-- Assistant Images -->
147
+ <div v-if="message.images && message.images.length > 0" class="mt-2 flex flex-wrap gap-2">
148
+ <template v-for="(img, i) in message.images" :key="i">
149
+ <div v-if="img.type === 'image_url'" class="group relative cursor-pointer" @click="openLightbox(resolveUrl(img.image_url.url))">
150
+ <img :src="resolveUrl(img.image_url.url)" class="max-w-[400px] max-h-96 rounded-lg border border-gray-200 dark:border-gray-700 object-contain bg-gray-50 dark:bg-gray-900 shadow-sm transition-transform hover:scale-[1.02]" />
151
+ </div>
152
+ </template>
153
+ </div>
154
+
218
155
  <!-- User Message with separate attachments -->
219
156
  <div v-if="message.role !== 'assistant'">
220
- <div v-html="renderMarkdown(message.content)" class="prose prose-sm max-w-none dark:prose-invert break-words"></div>
157
+ <div v-html="$fmt.markdown(message.content)" class="prose prose-sm max-w-none dark:prose-invert break-words"></div>
221
158
 
222
159
  <!-- Attachments Grid -->
223
160
  <div v-if="hasAttachments(message)" class="mt-2 flex flex-wrap gap-2">
@@ -241,13 +178,14 @@ export default {
241
178
  </div>
242
179
  </div>
243
180
 
244
- <div class="mt-2 text-xs opacity-70">
245
- <span>{{ formatTime(message.timestamp) }}</span>
181
+ <div class="mt-2 text-xs opacity-70">
182
+ <span v-if="message.model" @click="$chat.setSelectedModel({ name: message.model })" title="Select model"><span class="cursor-pointer hover:underline">{{ message.model }}</span> &#8226; </span>
183
+ <span>{{ $fmt.time(message.timestamp) }}</span>
246
184
  <span v-if="message.usage" :title="tokensTitle(message.usage)">
247
185
  &#8226;
248
- {{ humanifyNumber(message.usage.tokens) }} tokens
186
+ {{ $fmt.humanifyNumber(message.usage.tokens) }} tokens
249
187
  <span v-if="message.usage.cost">&#183; {{ message.usage.cost }}</span>
250
- <span v-if="message.usage.duration"> in {{ humanifyMs(message.usage.duration) }}</span>
188
+ <span v-if="message.usage.duration"> in {{ $fmt.humanifyMs(message.usage.duration) }}</span>
251
189
  </span>
252
190
  </div>
253
191
  </div>
@@ -274,8 +212,8 @@ export default {
274
212
  </div>
275
213
 
276
214
  <div v-if="currentThread.stats && currentThread.stats.outputTokens" class="text-center text-gray-500 dark:text-gray-400 text-sm">
277
- <span :title="statsTitle(currentThread.stats)">
278
- {{ currentThread.stats.cost ? formatCost(currentThread.stats.cost) + ' for ' : '' }} {{ humanifyNumber(currentThread.stats.inputTokens) }} → {{ humanifyNumber(currentThread.stats.outputTokens) }} tokens over {{ currentThread.stats.requests }} request{{currentThread.stats.requests===1?'':'s'}} in {{ humanifyMs(currentThread.stats.duration) }}
215
+ <span :title="$fmt.statsTitle(currentThread.stats)">
216
+ {{ currentThread.stats.cost ? $fmt.costLong(currentThread.stats.cost) + ' for ' : '' }} {{ $fmt.humanifyNumber(currentThread.stats.inputTokens) }} → {{ $fmt.humanifyNumber(currentThread.stats.outputTokens) }} tokens over {{ currentThread.stats.requests }} request{{currentThread.stats.requests===1?'':'s'}} in {{ $fmt.humanifyMs(currentThread.stats.duration) }}
279
217
  </span>
280
218
  </div>
281
219
 
@@ -342,20 +280,21 @@ export default {
342
280
 
343
281
  <!-- Input Area -->
344
282
  <div v-if="$ai.hasAccess" class="flex-shrink-0 border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 px-6 py-4">
345
- <ChatPrompt :model="selectedModelObj" />
283
+ <ChatPrompt :model="$chat.getSelectedModel()" />
346
284
  </div>
347
285
 
348
286
  <!-- Lightbox -->
349
- <div v-if="lightboxUrl" class="fixed inset-0 z-[100] bg-black/90 flex items-center justify-center p-4 cursor-pointer" @click="closeLightbox">
287
+ <div v-if="lightboxUrl" class="fixed inset-0 z-[100] bg-black/90 flex items-center justify-center p-4 cursor-pointer"
288
+ @click="closeLightbox">
289
+ <button type="button" @click="closeLightbox"
290
+ class="absolute top-4 right-4 text-white/70 hover:text-white p-2 rounded-full hover:bg-white/10 transition-colors z-[101]"
291
+ title="Close">
292
+ <svg class="size-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
293
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
294
+ </svg>
295
+ </button>
350
296
  <div class="relative max-w-full max-h-full">
351
297
  <img :src="lightboxUrl" class="max-w-full max-h-[90vh] object-contain rounded-sm shadow-2xl" @click.stop />
352
- <button type="button" @click="closeLightbox"
353
- class="absolute -top-12 right-0 text-white/70 hover:text-white p-2 rounded-full bg-white/10 hover:bg-white/20 transition-colors"
354
- title="Close">
355
- <svg class="size-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
356
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
357
- </svg>
358
- </button>
359
298
  </div>
360
299
  </div>
361
300
  </div>
@@ -364,19 +303,13 @@ export default {
364
303
  const ctx = inject('ctx')
365
304
  const models = ctx.state.models
366
305
  const config = ctx.state.config
306
+ const threads = ctx.threads
307
+ const chatPrompt = ctx.chat
308
+ const { currentThread } = threads
309
+ const { errorStatus, isGenerating } = ctx.chat
310
+
367
311
  const router = useRouter()
368
312
  const route = useRoute()
369
- const threads = useThreadStore()
370
- const { currentThread } = threads
371
- const chatPrompt = useChatPrompt()
372
- const chatSettings = useSettings()
373
- const {
374
- errorStatus,
375
- isGenerating,
376
- } = chatPrompt
377
- provide('threads', threads)
378
- provide('chatPrompt', chatPrompt)
379
- provide('chatSettings', chatSettings)
380
313
 
381
314
  const prefs = ctx.getPrefs()
382
315
 
@@ -399,6 +332,13 @@ export default {
399
332
  lightboxUrl.value = null
400
333
  }
401
334
 
335
+ const resolveUrl = (url) => {
336
+ if (url && url.startsWith('~')) {
337
+ return '/' + url
338
+ }
339
+ return ctx.ai.resolveUrl(url)
340
+ }
341
+
402
342
  // Auto-scroll to bottom when new messages arrive
403
343
  const scrollToBottom = async () => {
404
344
  await nextTick()
@@ -422,7 +362,7 @@ export default {
422
362
  if (!newId) {
423
363
  chatPrompt.reset()
424
364
  }
425
- nextTick(addCopyButtons)
365
+ nextTick(ctx.chat.addCopyButtons)
426
366
  }, { immediate: true })
427
367
 
428
368
  watch(() => [selectedModel.value], () => {
@@ -622,14 +562,6 @@ export default {
622
562
  }
623
563
  }
624
564
 
625
- // Format timestamp
626
- const formatTime = (timestamp) => {
627
- return new Date(timestamp).toLocaleTimeString([], {
628
- hour: '2-digit',
629
- minute: '2-digit'
630
- })
631
- }
632
-
633
565
  // Reasoning collapse state and helpers
634
566
  const expandedReasoning = ref(new Set())
635
567
  const isReasoningExpanded = (id) => expandedReasoning.value.has(id)
@@ -721,7 +653,7 @@ export default {
721
653
  text = message.content
722
654
  }
723
655
 
724
- const infos = await fetchCacheInfos(getCacheInfos)
656
+ const infos = await ctx.ai.fetchCacheInfos(getCacheInfos)
725
657
  // replace name with info.name
726
658
  for (let i = 0; i < files.length; i++) {
727
659
  const url = files[i]?.url
@@ -801,22 +733,20 @@ export default {
801
733
  let title = []
802
734
  if (usage.tokens && usage.price) {
803
735
  const msg = parseFloat(usage.price) > 0
804
- ? `${usage.tokens} tokens @ ${usage.price} = ${tokenCost(usage.price, usage.tokens)}`
736
+ ? `${usage.tokens} tokens @ ${usage.price} = ${ctx.fmt.tokenCostLong(usage.price, usage.tokens)}`
805
737
  : `${usage.tokens} tokens`
806
738
  const duration = usage.duration ? ` in ${usage.duration}ms` : ''
807
739
  title.push(msg + duration)
808
740
  }
809
741
  return title.join('\n')
810
742
  }
811
- const numFmt = new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD', minimumFractionDigits: 6 })
812
- function tokenCost(price, tokens) {
813
- if (!price || !tokens) return ''
814
- return numFmt.format(parseFloat(price) * tokens)
815
- }
816
743
 
744
+ let sub
817
745
  onMounted(() => {
818
- setTimeout(addCopyButtons, 1)
746
+ sub = ctx.events.subscribe(`keydown:Escape`, closeLightbox)
747
+ setTimeout(ctx.chat.addCopyButtons, 1)
819
748
  })
749
+ onUnmounted(() => sub?.unsubscribe())
820
750
 
821
751
  return {
822
752
  config,
@@ -829,8 +759,6 @@ export default {
829
759
  messagesContainer,
830
760
  errorStatus,
831
761
  copying,
832
- formatTime,
833
- renderMarkdown,
834
762
  isReasoningExpanded,
835
763
  toggleReasoning,
836
764
  formatReasoning,
@@ -847,16 +775,12 @@ export default {
847
775
  isImporting,
848
776
  fileInput,
849
777
  tokensTitle,
850
- humanifyMs,
851
- humanifyNumber,
852
- formatCost,
853
- formatCost,
854
- statsTitle,
855
778
  getAttachments,
856
779
  hasAttachments,
857
780
  lightboxUrl,
858
781
  openLightbox,
859
782
  closeLightbox,
783
+ resolveUrl,
860
784
  }
861
785
  }
862
786
  }
@@ -1,5 +1,5 @@
1
1
  import { ref, computed, watch, inject } from 'vue'
2
- import { storageObject } from './utils.mjs'
2
+ import { storageObject } from '../../utils.mjs'
3
3
 
4
4
  const settingsKey = 'llms.settings'
5
5
 
@@ -40,7 +40,7 @@ export function useSettings() {
40
40
  ]
41
41
 
42
42
  let settings = ref(storageObject(settingsKey))
43
-
43
+
44
44
  function validSettings(localSettings) {
45
45
  const to = {}
46
46
  intFields.forEach(f => {
@@ -65,9 +65,9 @@ export function useSettings() {
65
65
  })
66
66
  listFields.forEach(f => {
67
67
  if (localSettings[f] != null && localSettings[f] !== '') {
68
- to[f] = Array.isArray(localSettings[f])
68
+ to[f] = Array.isArray(localSettings[f])
69
69
  ? localSettings[f]
70
- : typeof localSettings[f] == 'string'
70
+ : typeof localSettings[f] == 'string'
71
71
  ? localSettings[f].split(',').map(x => x.trim())
72
72
  : []
73
73
  }
@@ -88,7 +88,7 @@ export function useSettings() {
88
88
  function resetSettings() {
89
89
  return saveSettings({})
90
90
  }
91
-
91
+
92
92
  function saveSettings(localSettings) {
93
93
  // console.log('saveSettings', JSON.stringify(localSettings, undefined, 2))
94
94
  settings.value = validSettings(localSettings)
@@ -337,9 +337,9 @@ export default {
337
337
  },
338
338
  emits: ['close'],
339
339
  setup(props, { emit }) {
340
- const chatSettings = inject('chatSettings')
341
- const { settings, saveSettings, resetSettings } = chatSettings
342
-
340
+ const ctx = inject('ctx')
341
+ const { settings, saveSettings, resetSettings } = ctx.chat.settings
342
+
343
343
  // Local copy for editing
344
344
  const localSettings = ref(Object.assign({}, settings.value))
345
345