llms-py 3.0.0b1__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 (63) hide show
  1. llms/__pycache__/__init__.cpython-312.pyc +0 -0
  2. llms/__pycache__/__init__.cpython-313.pyc +0 -0
  3. llms/__pycache__/__init__.cpython-314.pyc +0 -0
  4. llms/__pycache__/__main__.cpython-312.pyc +0 -0
  5. llms/__pycache__/__main__.cpython-314.pyc +0 -0
  6. llms/__pycache__/llms.cpython-312.pyc +0 -0
  7. llms/__pycache__/main.cpython-312.pyc +0 -0
  8. llms/__pycache__/main.cpython-313.pyc +0 -0
  9. llms/__pycache__/main.cpython-314.pyc +0 -0
  10. llms/__pycache__/plugins.cpython-314.pyc +0 -0
  11. llms/index.html +27 -57
  12. llms/llms.json +48 -15
  13. llms/main.py +923 -624
  14. llms/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
  15. llms/providers/__pycache__/chutes.cpython-314.pyc +0 -0
  16. llms/providers/__pycache__/google.cpython-314.pyc +0 -0
  17. llms/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
  18. llms/providers/__pycache__/openai.cpython-314.pyc +0 -0
  19. llms/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
  20. llms/providers/anthropic.py +189 -0
  21. llms/providers/chutes.py +152 -0
  22. llms/providers/google.py +306 -0
  23. llms/providers/nvidia.py +107 -0
  24. llms/providers/openai.py +159 -0
  25. llms/providers/openrouter.py +70 -0
  26. llms/providers-extra.json +356 -0
  27. llms/providers.json +1 -1
  28. llms/ui/App.mjs +150 -57
  29. llms/ui/ai.mjs +84 -50
  30. llms/ui/app.css +1 -4963
  31. llms/ui/ctx.mjs +196 -0
  32. llms/ui/index.mjs +117 -0
  33. llms/ui/lib/charts.mjs +9 -13
  34. llms/ui/markdown.mjs +6 -0
  35. llms/ui/{Analytics.mjs → modules/analytics.mjs} +76 -64
  36. llms/ui/{Main.mjs → modules/chat/ChatBody.mjs} +91 -179
  37. llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +8 -8
  38. llms/ui/{ChatPrompt.mjs → modules/chat/index.mjs} +281 -96
  39. llms/ui/modules/layout.mjs +267 -0
  40. llms/ui/modules/model-selector.mjs +851 -0
  41. llms/ui/{Recents.mjs → modules/threads/Recents.mjs} +10 -11
  42. llms/ui/{Sidebar.mjs → modules/threads/index.mjs} +48 -45
  43. llms/ui/{threadStore.mjs → modules/threads/threadStore.mjs} +21 -7
  44. llms/ui/tailwind.input.css +441 -79
  45. llms/ui/utils.mjs +83 -123
  46. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b3.dist-info}/METADATA +1 -1
  47. llms_py-3.0.0b3.dist-info/RECORD +65 -0
  48. llms/ui/Avatar.mjs +0 -85
  49. llms/ui/Brand.mjs +0 -52
  50. llms/ui/ModelSelector.mjs +0 -693
  51. llms/ui/OAuthSignIn.mjs +0 -92
  52. llms/ui/ProviderIcon.mjs +0 -36
  53. llms/ui/ProviderStatus.mjs +0 -105
  54. llms/ui/SignIn.mjs +0 -64
  55. llms/ui/SystemPromptEditor.mjs +0 -31
  56. llms/ui/SystemPromptSelector.mjs +0 -56
  57. llms/ui/Welcome.mjs +0 -8
  58. llms/ui.json +0 -1069
  59. llms_py-3.0.0b1.dist-info/RECORD +0 -49
  60. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b3.dist-info}/WHEEL +0 -0
  61. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b3.dist-info}/entry_points.txt +0 -0
  62. {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b3.dist-info}/licenses/LICENSE +0 -0
  63. {llms_py-3.0.0b1.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
  `,
@@ -146,18 +289,14 @@ export default {
146
289
  model: {
147
290
  type: Object,
148
291
  default: null
149
- },
150
- systemPrompt: {
151
- type: String,
152
- default: ''
153
292
  }
154
293
  },
155
294
  setup(props) {
156
- const ai = inject('ai')
157
- const chatSettings = inject('chatSettings')
295
+ const ctx = inject('ctx')
296
+ const config = ctx.state.config
297
+ const ai = ctx.ai
158
298
  const router = useRouter()
159
- const config = inject('config')
160
- const chatPrompt = inject('chatPrompt')
299
+ const chatPrompt = ctx.chat
161
300
  const {
162
301
  messageText,
163
302
  attachedFiles,
@@ -166,17 +305,21 @@ export default {
166
305
  hasImage,
167
306
  hasAudio,
168
307
  hasFile,
169
- editingMessageId
308
+ editingMessageId,
170
309
  } = chatPrompt
171
- const threads = inject('threads')
310
+ const threads = ctx.threads
172
311
  const {
173
312
  currentThread,
174
- } = threads
313
+ } = ctx.threads
175
314
 
176
315
  const fileInput = ref(null)
177
316
  const refMessage = ref(null)
178
317
  const showSettings = ref(false)
179
- const { applySettings } = chatSettings
318
+ const { applySettings } = ctx.chat.settings
319
+
320
+ const canGenerateImages = computed(() => {
321
+ return props.model?.modalities?.output?.includes('image')
322
+ })
180
323
 
181
324
  // File attachments (+) handlers
182
325
  const triggerFilePicker = () => {
@@ -188,7 +331,7 @@ export default {
188
331
  // Upload files immediately
189
332
  const uploadedFiles = await Promise.all(files.map(async f => {
190
333
  try {
191
- const response = await uploadFile(f)
334
+ const response = await ctx.ai.uploadFile(f)
192
335
  const metadata = {
193
336
  url: response.url,
194
337
  name: f.name,
@@ -234,24 +377,6 @@ export default {
234
377
  attachedFiles.value.splice(i, 1)
235
378
  }
236
379
 
237
- // Helper function to add files and set default message
238
- const addFilesAndSetMessage = (files) => {
239
- if (files.length === 0) return
240
-
241
- attachedFiles.value.push(...files)
242
-
243
- // Set default message text if empty
244
- if (!messageText.value.trim()) {
245
- if (hasImage()) {
246
- messageText.value = getTextContent(config.defaults.image)
247
- } else if (hasAudio()) {
248
- messageText.value = getTextContent(config.defaults.audio)
249
- } else {
250
- messageText.value = getTextContent(config.defaults.file)
251
- }
252
- }
253
- }
254
-
255
380
  // Handle paste events for clipboard images, audio, and files
256
381
  const onPaste = async (e) => {
257
382
  // Use the paste event's clipboardData directly (works best for paste events)
@@ -325,29 +450,22 @@ export default {
325
450
  }
326
451
  }
327
452
 
328
- function createChatRequest() {
329
- if (hasImage()) {
330
- return deepClone(config.defaults.image)
331
- }
332
- if (hasAudio()) {
333
- return deepClone(config.defaults.audio)
334
- }
335
- if (attachedFiles.value.length) {
336
- return deepClone(config.defaults.file)
337
- }
338
- const text = deepClone(config.defaults.text)
339
- return text
340
- }
341
-
342
453
  function getTextContent(chat) {
343
454
  const textMessage = chat.messages.find(m =>
344
455
  m.role === 'user' && Array.isArray(m.content) && m.content.some(c => c.type === 'text'))
345
456
  return textMessage?.content.find(c => c.type === 'text')?.text || ''
346
457
  }
347
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
+
348
465
  // Send message
349
466
  const sendMessage = async () => {
350
- 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
351
469
 
352
470
  // Clear any existing error message
353
471
  errorStatus.value = null
@@ -377,23 +495,27 @@ export default {
377
495
  // Create AbortController for this request
378
496
  const controller = new AbortController()
379
497
  chatPrompt.abortController.value = controller
498
+ const model = props.model.name
380
499
 
381
500
  try {
382
501
  let threadId
383
502
 
384
503
  // Create thread if none exists
385
504
  if (!currentThread.value) {
386
- const newThread = await threads.createThread('New Chat', props.model, props.systemPrompt)
505
+ const newThread = await threads.createThread({
506
+ title: 'New Chat',
507
+ model,
508
+ info: toModelInfo(props.model),
509
+ })
387
510
  threadId = newThread.id
388
511
  // Navigate to the new thread URL
389
512
  router.push(`${ai.base}/c/${newThread.id}`)
390
513
  } else {
391
514
  threadId = currentThread.value.id
392
- // Update the existing thread's model and systemPrompt to match current selection
515
+ // Update the existing thread's model to match current selection
393
516
  await threads.updateThread(threadId, {
394
- model: props.model.name,
517
+ model,
395
518
  info: toModelInfo(props.model),
396
- systemPrompt: props.systemPrompt
397
519
  })
398
520
  }
399
521
 
@@ -448,51 +570,57 @@ export default {
448
570
  isGenerating.value = true
449
571
 
450
572
  // Construct API Request from History
451
- const chatRequest = {
452
- model: props.model.name,
573
+ const request = {
574
+ model,
453
575
  messages: [],
454
576
  metadata: {}
455
577
  }
456
578
 
457
- // Add system prompt if present
458
- if (props.systemPrompt?.trim()) {
459
- chatRequest.messages.push({
460
- role: 'system',
461
- content: props.systemPrompt // assuming system prompt is just string
462
- })
463
- }
464
-
465
579
  // Add History
466
580
  thread.messages.forEach(m => {
467
- chatRequest.messages.push({
581
+ request.messages.push({
468
582
  role: m.role,
469
583
  content: m.content
470
584
  })
471
585
  })
472
586
 
473
587
  // Apply user settings
474
- applySettings(chatRequest)
475
- chatRequest.metadata.threadId = threadId
588
+ applySettings(request)
476
589
 
477
- console.debug('chatRequest', chatRequest)
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
+
597
+ request.metadata.threadId = threadId
598
+
599
+ const ctxRequest = {
600
+ request,
601
+ thread,
602
+ }
603
+ ctx.chatRequestFilters.forEach(f => f(ctxRequest))
604
+
605
+ console.debug('chatRequest', request)
478
606
 
479
607
  // Send to API
480
608
  const startTime = Date.now()
481
- const response = await ai.post('/v1/chat/completions', {
482
- body: JSON.stringify(chatRequest),
609
+ const res = await ai.post('/v1/chat/completions', {
610
+ body: JSON.stringify(request),
483
611
  signal: controller.signal
484
612
  })
485
613
 
486
- let result = null
487
- if (!response.ok) {
614
+ let response = null
615
+ if (!res.ok) {
488
616
  errorStatus.value = {
489
- errorCode: `HTTP ${response.status} ${response.statusText}`,
617
+ errorCode: `HTTP ${res.status} ${res.statusText}`,
490
618
  message: null,
491
619
  stackTrace: null
492
620
  }
493
621
  let errorBody = null
494
622
  try {
495
- errorBody = await response.text()
623
+ errorBody = await res.text()
496
624
  if (errorBody) {
497
625
  // Try to parse as JSON for better formatting
498
626
  try {
@@ -513,8 +641,13 @@ export default {
513
641
  }
514
642
  } else {
515
643
  try {
516
- result = await response.json()
517
- console.debug('chatResponse', JSON.stringify(result, null, 2))
644
+ response = await res.json()
645
+ const ctxResponse = {
646
+ response,
647
+ thread,
648
+ }
649
+ ctx.chatResponseFilters.forEach(f => f(ctxResponse))
650
+ console.debug('chatResponse', JSON.stringify(response, null, 2))
518
651
  } catch (e) {
519
652
  errorStatus.value = {
520
653
  errorCode: 'Error',
@@ -524,29 +657,29 @@ export default {
524
657
  }
525
658
  }
526
659
 
527
- if (result?.error) {
660
+ if (response?.error) {
528
661
  errorStatus.value ??= {
529
662
  errorCode: 'Error',
530
663
  }
531
- errorStatus.value.message = result.error
664
+ errorStatus.value.message = response.error
532
665
  }
533
666
 
534
667
  if (!errorStatus.value) {
535
668
  // Add assistant response (save entire message including reasoning)
536
- const assistantMessage = result.choices?.[0]?.message
669
+ const assistantMessage = response.choices?.[0]?.message
537
670
 
538
- const usage = result.usage
671
+ const usage = response.usage
539
672
  if (usage) {
540
- if (result.metadata?.pricing) {
541
- const [input, output] = result.metadata.pricing.split('/')
542
- usage.duration = result.metadata.duration ?? (Date.now() - startTime)
673
+ if (response.metadata?.pricing) {
674
+ const [input, output] = response.metadata.pricing.split('/')
675
+ usage.duration = response.metadata.duration ?? (Date.now() - startTime)
543
676
  usage.input = input
544
677
  usage.output = output
545
678
  usage.tokens = usage.completion_tokens
546
679
  usage.price = usage.output
547
- 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))
548
681
  }
549
- await threads.logRequest(threadId, props.model, chatRequest, result)
682
+ await threads.logRequest(threadId, props.model, request, response)
550
683
  }
551
684
  await threads.addMessageToThread(threadId, assistantMessage, usage)
552
685
 
@@ -554,6 +687,8 @@ export default {
554
687
 
555
688
  attachedFiles.value = []
556
689
  // Error will be cleared when user sends next message (no auto-timeout)
690
+ } else {
691
+ ctx.chatErrorFilters.forEach(f => f(errorStatus.value))
557
692
  }
558
693
  } catch (error) {
559
694
  // Check if the error is due to abort
@@ -583,6 +718,10 @@ export default {
583
718
  //messageText.value += '\n'
584
719
  }
585
720
 
721
+ watch(() => ctx.state.selectedAspectRatio, newValue => {
722
+ ctx.setPrefs({ aspectRatio: newValue })
723
+ })
724
+
586
725
  return {
587
726
  isGenerating,
588
727
  attachedFiles,
@@ -601,6 +740,52 @@ export default {
601
740
  sendMessage,
602
741
  cancelRequest,
603
742
  addNewLine,
743
+ imageAspectRatios,
744
+ canGenerateImages,
604
745
  }
605
746
  }
606
- }
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
+ }