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,13 +1,115 @@
1
- import { ref, nextTick, inject, unref } from 'vue'
1
+
2
+ import { ref, computed, watch, nextTick, inject } from 'vue'
2
3
  import { useRouter } from 'vue-router'
3
- import { lastRightPart } from '@servicestack/client'
4
- import { deepClone, fileToDataUri, fileToBase64, addCopyButtons, toModelInfo, tokenCost, uploadFile } from './utils.mjs'
5
- import { toRaw } from 'vue'
4
+ import { $$, createElement, lastRightPart } from "@servicestack/client"
5
+ import SettingsDialog, { useSettings } from './SettingsDialog.mjs'
6
+ import ChatBody from './ChatBody.mjs'
7
+ import { AppContext } from '../../ctx.mjs'
6
8
 
7
9
  const imageExts = 'png,webp,jpg,jpeg,gif,bmp,svg,tiff,ico'.split(',')
8
10
  const audioExts = 'mp3,wav,ogg,flac,m4a,opus,webm'.split(',')
9
11
 
10
- export function useChatPrompt() {
12
+ /* Example image generation request: https://openrouter.ai/docs/guides/overview/multimodal/image-generation
13
+ {
14
+ "model": "google/gemini-2.5-flash-image-preview",
15
+ "messages": [
16
+ {
17
+ "role": "user",
18
+ "content": "Create a picture of a nano banana dish in a fancy restaurant with a Gemini theme"
19
+ }
20
+ ],
21
+ "modalities": ["image", "text"],
22
+ "image_config": {
23
+ "aspect_ratio": "16:9"
24
+ }
25
+ }
26
+ Example response:
27
+ {
28
+ "choices": [
29
+ {
30
+ "message": {
31
+ "role": "assistant",
32
+ "content": "I've generated a beautiful sunset image for you.",
33
+ "images": [
34
+ {
35
+ "type": "image_url",
36
+ "image_url": {
37
+ "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."
38
+ }
39
+ }
40
+ ]
41
+ }
42
+ }
43
+ ]
44
+ }
45
+ */
46
+ const imageAspectRatios = {
47
+ '1024×1024': '1:1',
48
+ '832×1248': '2:3',
49
+ '1248×832': '3:2',
50
+ '864×1184': '3:4',
51
+ '1184×864': '4:3',
52
+ '896×1152': '4:5',
53
+ '1152×896': '5:4',
54
+ '768×1344': '9:16',
55
+ '1344×768': '16:9',
56
+ '1536×672': '21:9',
57
+ }
58
+
59
+ const svg = {
60
+ clipboard: `<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none"><path d="M8 5H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1M8 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M8 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m0 0h2a2 2 0 0 1 2 2v3m2 4H10m0 0l3-3m-3 3l3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></svg>`,
61
+ check: `<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>`,
62
+ }
63
+
64
+ function copyBlock(btn) {
65
+ // console.log('copyBlock',btn)
66
+ const label = btn.previousElementSibling
67
+ const code = btn.parentElement.nextElementSibling
68
+ label.classList.remove('hidden')
69
+ label.innerHTML = 'copied'
70
+ btn.classList.add('border-gray-600', 'bg-gray-700')
71
+ btn.classList.remove('border-gray-700')
72
+ btn.innerHTML = svg.check
73
+ navigator.clipboard.writeText(code.innerText)
74
+ setTimeout(() => {
75
+ label.classList.add('hidden')
76
+ label.innerHTML = ''
77
+ btn.innerHTML = svg.clipboard
78
+ btn.classList.remove('border-gray-600', 'bg-gray-700')
79
+ btn.classList.add('border-gray-700')
80
+ }, 2000)
81
+ }
82
+
83
+ function addCopyButtonToCodeBlocks(sel) {
84
+ globalThis.copyBlock ??= copyBlock
85
+ //console.log('addCopyButtonToCodeBlocks', sel, [...$$(sel)].length)
86
+
87
+ $$(sel).forEach(code => {
88
+ let pre = code.parentElement;
89
+ if (pre.classList.contains('group')) return
90
+ pre.classList.add('relative', 'group')
91
+
92
+ const div = createElement('div', { attrs: { className: 'opacity-0 group-hover:opacity-100 transition-opacity duration-100 flex absolute right-2 -mt-1 select-none' } })
93
+ const label = createElement('div', { attrs: { className: 'hidden font-sans p-1 px-2 mr-1 rounded-md border border-gray-600 bg-gray-700 text-gray-400' } })
94
+ const btn = createElement('button', {
95
+ attrs: {
96
+ type: 'button',
97
+ className: 'p-1 rounded-md border block text-gray-500 hover:text-gray-400 border-gray-700 hover:border-gray-600',
98
+ onclick: 'copyBlock(this)'
99
+ }
100
+ })
101
+ btn.innerHTML = svg.clipboard
102
+ div.appendChild(label)
103
+ div.appendChild(btn)
104
+ pre.insertBefore(div, code)
105
+ })
106
+ }
107
+
108
+ export function addCopyButtons() {
109
+ addCopyButtonToCodeBlocks('.prose pre>code')
110
+ }
111
+
112
+ export function useChatPrompt(ctx) {
11
113
  const messageText = ref('')
12
114
  const attachedFiles = ref([])
13
115
  const isGenerating = ref(false)
@@ -39,6 +141,30 @@ export function useChatPrompt() {
39
141
  abortController.value = null
40
142
  }
41
143
 
144
+ const settings = useSettings()
145
+
146
+ function getModel(name) {
147
+ return ctx.state.models.find(x => x.name === name) ?? ctx.state.models.find(x => x.id === name)
148
+ }
149
+
150
+ function getSelectedModel() {
151
+ const candidates = [ctx.state.selectedModel, ctx.state.config.defaults.text.model]
152
+ return candidates.map(name => name && getModel(name)).find(x => !!x)
153
+ }
154
+
155
+ function setSelectedModel(model) {
156
+ ctx.setState({
157
+ selectedModel: model.name
158
+ })
159
+ ctx.setPrefs({
160
+ model: model.name
161
+ })
162
+ }
163
+
164
+ function getProviderForModel(model) {
165
+ return getModel(model)?.provider
166
+ }
167
+
42
168
  return {
43
169
  messageText,
44
170
  attachedFiles,
@@ -55,10 +181,16 @@ export function useChatPrompt() {
55
181
  // hasText,
56
182
  reset,
57
183
  cancel,
184
+ settings,
185
+ addCopyButtons,
186
+ getModel,
187
+ getSelectedModel,
188
+ setSelectedModel,
189
+ getProviderForModel,
58
190
  }
59
191
  }
60
192
 
61
- export default {
193
+ const ChatPrompt = {
62
194
  template: `
63
195
  <div class="mx-auto max-w-3xl">
64
196
  <SettingsDialog :isOpen="showSettings" @close="showSettings = false" />
@@ -125,20 +257,31 @@ export default {
125
257
  </button>
126
258
  </div>
127
259
 
128
- <!-- Attached files preview -->
129
- <div v-if="attachedFiles.length" class="mt-2 flex flex-wrap gap-2">
130
- <div v-for="(f,i) in attachedFiles" :key="i" class="flex items-center gap-2 px-2 py-1 rounded-md border border-gray-300 dark:border-gray-600 text-xs text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800">
131
- <span class="truncate max-w-48" :title="f.name">{{ f.name }}</span>
132
- <button type="button" class="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200" @click="removeAttachment(i)" title="Remove Attachment">
133
- <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
134
- </button>
260
+ <!-- Attachments & Image Options -->
261
+ <div class="mt-2 flex justify-between items-start gap-2">
262
+ <div class="flex flex-wrap gap-2">
263
+ <div v-for="(f,i) in attachedFiles" :key="i" class="flex items-center gap-2 px-2 py-1 rounded-md border border-gray-300 dark:border-gray-600 text-xs text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800">
264
+ <span class="truncate max-w-48" :title="f.name">{{ f.name }}</span>
265
+ <button type="button" class="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200" @click="removeAttachment(i)" title="Remove Attachment">
266
+ <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
267
+ </button>
268
+ </div>
269
+ </div>
270
+
271
+ <!-- Image Aspect Ratio Selector -->
272
+ <div v-if="canGenerateImages" class="min-w-[120px]">
273
+ <select name="aspect_ratio" v-model="$state.selectedAspectRatio"
274
+ class="block w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-xs text-gray-700 dark:text-gray-300 pl-2 pr-6 py-1 focus:ring-blue-500 focus:border-blue-500">
275
+ <option v-for="(ratio, size) in imageAspectRatios" :key="size" :value="size">
276
+ {{ size }} ({{ ratio }})
277
+ </option>
278
+ </select>
135
279
  </div>
136
280
  </div>
137
281
 
138
282
  <div v-if="!model" class="mt-2 text-sm text-red-600 dark:text-red-400">
139
283
  Please select a model
140
284
  </div>
141
- </div>
142
285
  </div>
143
286
  </div>
144
287
  `,
@@ -152,9 +295,8 @@ export default {
152
295
  const ctx = inject('ctx')
153
296
  const config = ctx.state.config
154
297
  const ai = ctx.ai
155
- const chatSettings = inject('chatSettings')
156
298
  const router = useRouter()
157
- const chatPrompt = inject('chatPrompt')
299
+ const chatPrompt = ctx.chat
158
300
  const {
159
301
  messageText,
160
302
  attachedFiles,
@@ -163,17 +305,21 @@ export default {
163
305
  hasImage,
164
306
  hasAudio,
165
307
  hasFile,
166
- editingMessageId
308
+ editingMessageId,
167
309
  } = chatPrompt
168
- const threads = inject('threads')
310
+ const threads = ctx.threads
169
311
  const {
170
312
  currentThread,
171
- } = threads
313
+ } = ctx.threads
172
314
 
173
315
  const fileInput = ref(null)
174
316
  const refMessage = ref(null)
175
317
  const showSettings = ref(false)
176
- const { applySettings } = chatSettings
318
+ const { applySettings } = ctx.chat.settings
319
+
320
+ const canGenerateImages = computed(() => {
321
+ return props.model?.modalities?.output?.includes('image')
322
+ })
177
323
 
178
324
  // File attachments (+) handlers
179
325
  const triggerFilePicker = () => {
@@ -185,7 +331,7 @@ export default {
185
331
  // Upload files immediately
186
332
  const uploadedFiles = await Promise.all(files.map(async f => {
187
333
  try {
188
- const response = await uploadFile(f)
334
+ const response = await ctx.ai.uploadFile(f)
189
335
  const metadata = {
190
336
  url: response.url,
191
337
  name: f.name,
@@ -231,24 +377,6 @@ export default {
231
377
  attachedFiles.value.splice(i, 1)
232
378
  }
233
379
 
234
- // Helper function to add files and set default message
235
- const addFilesAndSetMessage = (files) => {
236
- if (files.length === 0) return
237
-
238
- attachedFiles.value.push(...files)
239
-
240
- // Set default message text if empty
241
- if (!messageText.value.trim()) {
242
- if (hasImage()) {
243
- messageText.value = getTextContent(config.defaults.image)
244
- } else if (hasAudio()) {
245
- messageText.value = getTextContent(config.defaults.audio)
246
- } else {
247
- messageText.value = getTextContent(config.defaults.file)
248
- }
249
- }
250
- }
251
-
252
380
  // Handle paste events for clipboard images, audio, and files
253
381
  const onPaste = async (e) => {
254
382
  // Use the paste event's clipboardData directly (works best for paste events)
@@ -328,9 +456,16 @@ export default {
328
456
  return textMessage?.content.find(c => c.type === 'text')?.text || ''
329
457
  }
330
458
 
459
+ function toModelInfo(model) {
460
+ if (!model) return undefined
461
+ const { id, name, provider, cost, modalities } = model
462
+ return ctx.utils.deepClone({ id, name, provider, cost, modalities })
463
+ }
464
+
331
465
  // Send message
332
466
  const sendMessage = async () => {
333
- if (!messageText.value.trim() || isGenerating.value || !props.model) return
467
+ if (!messageText.value.trim() && !hasImage() && !hasAudio() && !hasFile()) return
468
+ if (isGenerating.value || !props.model) return
334
469
 
335
470
  // Clear any existing error message
336
471
  errorStatus.value = null
@@ -360,6 +495,7 @@ export default {
360
495
  // Create AbortController for this request
361
496
  const controller = new AbortController()
362
497
  chatPrompt.abortController.value = controller
498
+ const model = props.model.name
363
499
 
364
500
  try {
365
501
  let threadId
@@ -368,7 +504,7 @@ export default {
368
504
  if (!currentThread.value) {
369
505
  const newThread = await threads.createThread({
370
506
  title: 'New Chat',
371
- model: props.model.id,
507
+ model,
372
508
  info: toModelInfo(props.model),
373
509
  })
374
510
  threadId = newThread.id
@@ -378,7 +514,7 @@ export default {
378
514
  threadId = currentThread.value.id
379
515
  // Update the existing thread's model to match current selection
380
516
  await threads.updateThread(threadId, {
381
- model: props.model.name,
517
+ model,
382
518
  info: toModelInfo(props.model),
383
519
  })
384
520
  }
@@ -435,7 +571,7 @@ export default {
435
571
 
436
572
  // Construct API Request from History
437
573
  const request = {
438
- model: props.model.name,
574
+ model,
439
575
  messages: [],
440
576
  metadata: {}
441
577
  }
@@ -450,6 +586,14 @@ export default {
450
586
 
451
587
  // Apply user settings
452
588
  applySettings(request)
589
+
590
+ if (canGenerateImages.value) {
591
+ request.image_config = {
592
+ aspect_ratio: imageAspectRatios[ctx.state.selectedAspectRatio] || '1:1'
593
+ }
594
+ request.modalities = ["image", "text"]
595
+ }
596
+
453
597
  request.metadata.threadId = threadId
454
598
 
455
599
  const ctxRequest = {
@@ -533,7 +677,7 @@ export default {
533
677
  usage.output = output
534
678
  usage.tokens = usage.completion_tokens
535
679
  usage.price = usage.output
536
- usage.cost = tokenCost(usage.prompt_tokens / 1_000_000 * parseFloat(input) + usage.completion_tokens / 1_000_000 * parseFloat(output))
680
+ usage.cost = ctx.fmt.tokenCost(usage.prompt_tokens / 1_000_000 * parseFloat(input) + usage.completion_tokens / 1_000_000 * parseFloat(output))
537
681
  }
538
682
  await threads.logRequest(threadId, props.model, request, response)
539
683
  }
@@ -574,6 +718,10 @@ export default {
574
718
  //messageText.value += '\n'
575
719
  }
576
720
 
721
+ watch(() => ctx.state.selectedAspectRatio, newValue => {
722
+ ctx.setPrefs({ aspectRatio: newValue })
723
+ })
724
+
577
725
  return {
578
726
  isGenerating,
579
727
  attachedFiles,
@@ -592,6 +740,52 @@ export default {
592
740
  sendMessage,
593
741
  cancelRequest,
594
742
  addNewLine,
743
+ imageAspectRatios,
744
+ canGenerateImages,
595
745
  }
596
746
  }
597
- }
747
+ }
748
+
749
+ export default {
750
+ /**@param {AppContext} ctx */
751
+ install(ctx) {
752
+ const Home = ChatBody
753
+ ctx.components({
754
+ SettingsDialog,
755
+ ChatPrompt,
756
+ ChatBody,
757
+ Home,
758
+ })
759
+ ctx.setGlobals({
760
+ chat: useChatPrompt(ctx)
761
+ })
762
+
763
+ ctx.setLeftIcons({
764
+ chat: {
765
+ component: {
766
+ template: `<svg @click="$ctx.togglePath('/')" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="currentColor" d="M8 2.19c3.13 0 5.68 2.25 5.68 5s-2.55 5-5.68 5a5.7 5.7 0 0 1-1.89-.29l-.75-.26l-.56.56a14 14 0 0 1-2 1.55a.13.13 0 0 1-.07 0v-.06a6.58 6.58 0 0 0 .15-4.29a5.25 5.25 0 0 1-.55-2.16c0-2.77 2.55-5 5.68-5M8 .94c-3.83 0-6.93 2.81-6.93 6.27a6.4 6.4 0 0 0 .64 2.64a5.53 5.53 0 0 1-.18 3.48a1.32 1.32 0 0 0 2 1.5a15 15 0 0 0 2.16-1.71a6.8 6.8 0 0 0 2.31.36c3.83 0 6.93-2.81 6.93-6.27S11.83.94 8 .94"/><ellipse cx="5.2" cy="7.7" fill="currentColor" rx=".8" ry=".75"/><ellipse cx="8" cy="7.7" fill="currentColor" rx=".8" ry=".75"/><ellipse cx="10.8" cy="7.7" fill="currentColor" rx=".8" ry=".75"/></svg>`,
767
+ },
768
+ isActive({ path }) {
769
+ return path === '/' || path.startsWith('/c/')
770
+ }
771
+ }
772
+ })
773
+
774
+ const title = 'Chat'
775
+ ctx.setState({
776
+ title
777
+ })
778
+
779
+ const meta = { title }
780
+ ctx.routes.push(...[
781
+ { path: '/', component: Home, meta },
782
+ { path: '/c/:id', component: ChatBody, meta },
783
+ ])
784
+
785
+ const prefs = ctx.getPrefs()
786
+ if (prefs.model) {
787
+ ctx.state.selectedModel = prefs.model
788
+ }
789
+ ctx.state.selectedAspectRatio = prefs.aspectRatio || '1:1'
790
+ }
791
+ }