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.
- llms/__pycache__/__init__.cpython-312.pyc +0 -0
- llms/__pycache__/__init__.cpython-313.pyc +0 -0
- llms/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/__pycache__/__main__.cpython-312.pyc +0 -0
- llms/__pycache__/__main__.cpython-314.pyc +0 -0
- llms/__pycache__/llms.cpython-312.pyc +0 -0
- llms/__pycache__/main.cpython-312.pyc +0 -0
- llms/__pycache__/main.cpython-313.pyc +0 -0
- llms/__pycache__/main.cpython-314.pyc +0 -0
- llms/__pycache__/plugins.cpython-314.pyc +0 -0
- llms/index.html +27 -57
- llms/llms.json +48 -15
- llms/main.py +923 -624
- llms/providers/__pycache__/anthropic.cpython-314.pyc +0 -0
- llms/providers/__pycache__/chutes.cpython-314.pyc +0 -0
- llms/providers/__pycache__/google.cpython-314.pyc +0 -0
- llms/providers/__pycache__/nvidia.cpython-314.pyc +0 -0
- llms/providers/__pycache__/openai.cpython-314.pyc +0 -0
- llms/providers/__pycache__/openrouter.cpython-314.pyc +0 -0
- llms/providers/anthropic.py +189 -0
- llms/providers/chutes.py +152 -0
- llms/providers/google.py +306 -0
- llms/providers/nvidia.py +107 -0
- llms/providers/openai.py +159 -0
- llms/providers/openrouter.py +70 -0
- llms/providers-extra.json +356 -0
- llms/providers.json +1 -1
- llms/ui/App.mjs +150 -57
- llms/ui/ai.mjs +84 -50
- llms/ui/app.css +1 -4963
- llms/ui/ctx.mjs +196 -0
- llms/ui/index.mjs +117 -0
- llms/ui/lib/charts.mjs +9 -13
- llms/ui/markdown.mjs +6 -0
- llms/ui/{Analytics.mjs → modules/analytics.mjs} +76 -64
- llms/ui/{Main.mjs → modules/chat/ChatBody.mjs} +91 -179
- llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +8 -8
- llms/ui/{ChatPrompt.mjs → modules/chat/index.mjs} +281 -96
- llms/ui/modules/layout.mjs +267 -0
- llms/ui/modules/model-selector.mjs +851 -0
- llms/ui/{Recents.mjs → modules/threads/Recents.mjs} +10 -11
- llms/ui/{Sidebar.mjs → modules/threads/index.mjs} +48 -45
- llms/ui/{threadStore.mjs → modules/threads/threadStore.mjs} +21 -7
- llms/ui/tailwind.input.css +441 -79
- llms/ui/utils.mjs +83 -123
- {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b3.dist-info}/METADATA +1 -1
- llms_py-3.0.0b3.dist-info/RECORD +65 -0
- llms/ui/Avatar.mjs +0 -85
- llms/ui/Brand.mjs +0 -52
- llms/ui/ModelSelector.mjs +0 -693
- llms/ui/OAuthSignIn.mjs +0 -92
- llms/ui/ProviderIcon.mjs +0 -36
- llms/ui/ProviderStatus.mjs +0 -105
- llms/ui/SignIn.mjs +0 -64
- llms/ui/SystemPromptEditor.mjs +0 -31
- llms/ui/SystemPromptSelector.mjs +0 -56
- llms/ui/Welcome.mjs +0 -8
- llms/ui.json +0 -1069
- llms_py-3.0.0b1.dist-info/RECORD +0 -49
- {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b3.dist-info}/WHEEL +0 -0
- {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b3.dist-info}/entry_points.txt +0 -0
- {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b3.dist-info}/licenses/LICENSE +0 -0
- {llms_py-3.0.0b1.dist-info → llms_py-3.0.0b3.dist-info}/top_level.txt +0 -0
|
@@ -1,13 +1,115 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
import { ref, computed, watch, nextTick, inject } from 'vue'
|
|
2
3
|
import { useRouter } from 'vue-router'
|
|
3
|
-
import { lastRightPart } from
|
|
4
|
-
import
|
|
5
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<!--
|
|
129
|
-
<div
|
|
130
|
-
<div
|
|
131
|
-
<
|
|
132
|
-
|
|
133
|
-
<
|
|
134
|
-
|
|
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
|
|
157
|
-
const
|
|
295
|
+
const ctx = inject('ctx')
|
|
296
|
+
const config = ctx.state.config
|
|
297
|
+
const ai = ctx.ai
|
|
158
298
|
const router = useRouter()
|
|
159
|
-
const
|
|
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 =
|
|
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 } =
|
|
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()
|
|
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(
|
|
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
|
|
515
|
+
// Update the existing thread's model to match current selection
|
|
393
516
|
await threads.updateThread(threadId, {
|
|
394
|
-
model
|
|
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
|
|
452
|
-
model
|
|
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
|
-
|
|
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(
|
|
475
|
-
chatRequest.metadata.threadId = threadId
|
|
588
|
+
applySettings(request)
|
|
476
589
|
|
|
477
|
-
|
|
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
|
|
482
|
-
body: JSON.stringify(
|
|
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
|
|
487
|
-
if (!
|
|
614
|
+
let response = null
|
|
615
|
+
if (!res.ok) {
|
|
488
616
|
errorStatus.value = {
|
|
489
|
-
errorCode: `HTTP ${
|
|
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
|
|
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
|
-
|
|
517
|
-
|
|
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 (
|
|
660
|
+
if (response?.error) {
|
|
528
661
|
errorStatus.value ??= {
|
|
529
662
|
errorCode: 'Error',
|
|
530
663
|
}
|
|
531
|
-
errorStatus.value.message =
|
|
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 =
|
|
669
|
+
const assistantMessage = response.choices?.[0]?.message
|
|
537
670
|
|
|
538
|
-
const usage =
|
|
671
|
+
const usage = response.usage
|
|
539
672
|
if (usage) {
|
|
540
|
-
if (
|
|
541
|
-
const [input, output] =
|
|
542
|
-
usage.duration =
|
|
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,
|
|
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
|
+
}
|