llms-py 3.0.0b2__py3-none-any.whl → 3.0.0b3__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 +1 -4962
  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} +56 -133
  28. llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +8 -8
  29. llms/ui/{ChatPrompt.mjs → modules/chat/index.mjs} +239 -45
  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.0b3.dist-info}/METADATA +1 -1
  37. llms_py-3.0.0b3.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.0b3.dist-info}/WHEEL +0 -0
  49. {llms_py-3.0.0b2.dist-info → llms_py-3.0.0b3.dist-info}/entry_points.txt +0 -0
  50. {llms_py-3.0.0b2.dist-info → llms_py-3.0.0b3.dist-info}/licenses/LICENSE +0 -0
  51. {llms_py-3.0.0b2.dist-info → llms_py-3.0.0b3.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">
@@ -149,6 +70,13 @@ export default {
149
70
 
150
71
  <!-- Messages -->
151
72
  <div v-else class="space-y-6">
73
+ <div v-if="currentThread.messages.length && currentThread.model" class="absolute -mt-2">
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">
@@ -242,12 +179,12 @@ export default {
242
179
  </div>
243
180
 
244
181
  <div class="mt-2 text-xs opacity-70">
245
- <span>{{ formatTime(message.timestamp) }}</span>
182
+ <span>{{ $fmt.time(message.timestamp) }}</span>
246
183
  <span v-if="message.usage" :title="tokensTitle(message.usage)">
247
184
  &#8226;
248
- {{ humanifyNumber(message.usage.tokens) }} tokens
185
+ {{ $fmt.humanifyNumber(message.usage.tokens) }} tokens
249
186
  <span v-if="message.usage.cost">&#183; {{ message.usage.cost }}</span>
250
- <span v-if="message.usage.duration"> in {{ humanifyMs(message.usage.duration) }}</span>
187
+ <span v-if="message.usage.duration"> in {{ $fmt.humanifyMs(message.usage.duration) }}</span>
251
188
  </span>
252
189
  </div>
253
190
  </div>
@@ -274,8 +211,8 @@ export default {
274
211
  </div>
275
212
 
276
213
  <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) }}
214
+ <span :title="$fmt.statsTitle(currentThread.stats)">
215
+ {{ 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
216
  </span>
280
217
  </div>
281
218
 
@@ -342,20 +279,21 @@ export default {
342
279
 
343
280
  <!-- Input Area -->
344
281
  <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" />
282
+ <ChatPrompt :model="$chat.getSelectedModel()" />
346
283
  </div>
347
284
 
348
285
  <!-- 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">
286
+ <div v-if="lightboxUrl" class="fixed inset-0 z-[100] bg-black/90 flex items-center justify-center p-4 cursor-pointer"
287
+ @click="closeLightbox">
288
+ <button type="button" @click="closeLightbox"
289
+ class="absolute top-4 right-4 text-white/70 hover:text-white p-2 rounded-full hover:bg-white/10 transition-colors z-[101]"
290
+ title="Close">
291
+ <svg class="size-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
292
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
293
+ </svg>
294
+ </button>
350
295
  <div class="relative max-w-full max-h-full">
351
296
  <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
297
  </div>
360
298
  </div>
361
299
  </div>
@@ -364,19 +302,13 @@ export default {
364
302
  const ctx = inject('ctx')
365
303
  const models = ctx.state.models
366
304
  const config = ctx.state.config
305
+ const threads = ctx.threads
306
+ const chatPrompt = ctx.chat
307
+ const { currentThread } = threads
308
+ const { errorStatus, isGenerating } = ctx.chat
309
+
367
310
  const router = useRouter()
368
311
  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
312
 
381
313
  const prefs = ctx.getPrefs()
382
314
 
@@ -399,6 +331,13 @@ export default {
399
331
  lightboxUrl.value = null
400
332
  }
401
333
 
334
+ const resolveUrl = (url) => {
335
+ if (url && url.startsWith('~')) {
336
+ return '/' + url
337
+ }
338
+ return ctx.ai.resolveUrl(url)
339
+ }
340
+
402
341
  // Auto-scroll to bottom when new messages arrive
403
342
  const scrollToBottom = async () => {
404
343
  await nextTick()
@@ -422,7 +361,7 @@ export default {
422
361
  if (!newId) {
423
362
  chatPrompt.reset()
424
363
  }
425
- nextTick(addCopyButtons)
364
+ nextTick(ctx.chat.addCopyButtons)
426
365
  }, { immediate: true })
427
366
 
428
367
  watch(() => [selectedModel.value], () => {
@@ -622,14 +561,6 @@ export default {
622
561
  }
623
562
  }
624
563
 
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
564
  // Reasoning collapse state and helpers
634
565
  const expandedReasoning = ref(new Set())
635
566
  const isReasoningExpanded = (id) => expandedReasoning.value.has(id)
@@ -721,7 +652,7 @@ export default {
721
652
  text = message.content
722
653
  }
723
654
 
724
- const infos = await fetchCacheInfos(getCacheInfos)
655
+ const infos = await ctx.ai.fetchCacheInfos(getCacheInfos)
725
656
  // replace name with info.name
726
657
  for (let i = 0; i < files.length; i++) {
727
658
  const url = files[i]?.url
@@ -801,22 +732,20 @@ export default {
801
732
  let title = []
802
733
  if (usage.tokens && usage.price) {
803
734
  const msg = parseFloat(usage.price) > 0
804
- ? `${usage.tokens} tokens @ ${usage.price} = ${tokenCost(usage.price, usage.tokens)}`
735
+ ? `${usage.tokens} tokens @ ${usage.price} = ${ctx.fmt.tokenCostLong(usage.price, usage.tokens)}`
805
736
  : `${usage.tokens} tokens`
806
737
  const duration = usage.duration ? ` in ${usage.duration}ms` : ''
807
738
  title.push(msg + duration)
808
739
  }
809
740
  return title.join('\n')
810
741
  }
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
742
 
743
+ let sub
817
744
  onMounted(() => {
818
- setTimeout(addCopyButtons, 1)
745
+ sub = ctx.events.subscribe(`keydown:Escape`, closeLightbox)
746
+ setTimeout(ctx.chat.addCopyButtons, 1)
819
747
  })
748
+ onUnmounted(() => sub?.unsubscribe())
820
749
 
821
750
  return {
822
751
  config,
@@ -829,8 +758,6 @@ export default {
829
758
  messagesContainer,
830
759
  errorStatus,
831
760
  copying,
832
- formatTime,
833
- renderMarkdown,
834
761
  isReasoningExpanded,
835
762
  toggleReasoning,
836
763
  formatReasoning,
@@ -847,16 +774,12 @@ export default {
847
774
  isImporting,
848
775
  fileInput,
849
776
  tokensTitle,
850
- humanifyMs,
851
- humanifyNumber,
852
- formatCost,
853
- formatCost,
854
- statsTitle,
855
777
  getAttachments,
856
778
  hasAttachments,
857
779
  lightboxUrl,
858
780
  openLightbox,
859
781
  closeLightbox,
782
+ resolveUrl,
860
783
  }
861
784
  }
862
785
  }
@@ -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