llms-py 2.0.14__py3-none-any.whl → 2.0.16__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/__init__.py +2 -0
  2. llms/__main__.py +9 -0
  3. llms/__pycache__/__init__.cpython-312.pyc +0 -0
  4. llms/__pycache__/__init__.cpython-313.pyc +0 -0
  5. llms/__pycache__/__init__.cpython-314.pyc +0 -0
  6. llms/__pycache__/__main__.cpython-312.pyc +0 -0
  7. llms/__pycache__/__main__.cpython-314.pyc +0 -0
  8. llms/__pycache__/llms.cpython-312.pyc +0 -0
  9. llms/__pycache__/main.cpython-312.pyc +0 -0
  10. llms/__pycache__/main.cpython-313.pyc +0 -0
  11. llms/__pycache__/main.cpython-314.pyc +0 -0
  12. {llms_py-2.0.14.data/data → llms}/index.html +5 -1
  13. llms/llms.json +1102 -0
  14. llms.py → llms/main.py +252 -14
  15. llms/ui/Analytics.mjs +1483 -0
  16. llms/ui/Brand.mjs +34 -0
  17. {llms_py-2.0.14.data/data → llms}/ui/ChatPrompt.mjs +58 -36
  18. {llms_py-2.0.14.data/data → llms}/ui/Main.mjs +205 -5
  19. llms/ui/ModelSelector.mjs +60 -0
  20. llms/ui/ProviderIcon.mjs +29 -0
  21. {llms_py-2.0.14.data/data → llms}/ui/Sidebar.mjs +20 -4
  22. {llms_py-2.0.14.data/data → llms}/ui/ai.mjs +1 -1
  23. {llms_py-2.0.14.data/data → llms}/ui/app.css +211 -64
  24. llms/ui/lib/chart.js +14 -0
  25. llms/ui/lib/charts.mjs +20 -0
  26. llms/ui/lib/color.js +14 -0
  27. llms/ui/lib/vue.mjs +18369 -0
  28. {llms_py-2.0.14.data/data → llms}/ui/tailwind.input.css +1 -1
  29. llms/ui/threadStore.mjs +524 -0
  30. {llms_py-2.0.14.data/data → llms}/ui/utils.mjs +36 -0
  31. {llms_py-2.0.14.dist-info → llms_py-2.0.16.dist-info}/METADATA +8 -35
  32. llms_py-2.0.16.dist-info/RECORD +56 -0
  33. llms_py-2.0.16.dist-info/entry_points.txt +2 -0
  34. llms_py-2.0.14.data/data/llms.json +0 -447
  35. llms_py-2.0.14.data/data/requirements.txt +0 -1
  36. llms_py-2.0.14.data/data/ui/Brand.mjs +0 -23
  37. llms_py-2.0.14.data/data/ui/ModelSelector.mjs +0 -29
  38. llms_py-2.0.14.data/data/ui/threadStore.mjs +0 -273
  39. llms_py-2.0.14.dist-info/RECORD +0 -40
  40. llms_py-2.0.14.dist-info/entry_points.txt +0 -2
  41. {llms_py-2.0.14.data/data → llms}/ui/App.mjs +0 -0
  42. {llms_py-2.0.14.data/data → llms}/ui/Avatar.mjs +0 -0
  43. {llms_py-2.0.14.data/data → llms}/ui/ProviderStatus.mjs +0 -0
  44. {llms_py-2.0.14.data/data → llms}/ui/Recents.mjs +0 -0
  45. {llms_py-2.0.14.data/data → llms}/ui/SettingsDialog.mjs +0 -0
  46. {llms_py-2.0.14.data/data → llms}/ui/SignIn.mjs +0 -0
  47. {llms_py-2.0.14.data/data → llms}/ui/SystemPromptEditor.mjs +0 -0
  48. {llms_py-2.0.14.data/data → llms}/ui/SystemPromptSelector.mjs +0 -0
  49. {llms_py-2.0.14.data/data → llms}/ui/Welcome.mjs +0 -0
  50. {llms_py-2.0.14.data/data → llms}/ui/fav.svg +0 -0
  51. {llms_py-2.0.14.data/data → llms}/ui/lib/highlight.min.mjs +0 -0
  52. {llms_py-2.0.14.data/data → llms}/ui/lib/idb.min.mjs +0 -0
  53. {llms_py-2.0.14.data/data → llms}/ui/lib/marked.min.mjs +0 -0
  54. {llms_py-2.0.14.data/data → llms}/ui/lib/servicestack-client.mjs +0 -0
  55. {llms_py-2.0.14.data/data → llms}/ui/lib/servicestack-vue.mjs +0 -0
  56. {llms_py-2.0.14.data/data → llms}/ui/lib/vue-router.min.mjs +0 -0
  57. {llms_py-2.0.14.data/data → llms}/ui/lib/vue.min.mjs +0 -0
  58. {llms_py-2.0.14.data/data → llms}/ui/markdown.mjs +0 -0
  59. {llms_py-2.0.14.data/data → llms}/ui/typography.css +0 -0
  60. {llms_py-2.0.14.data/data → llms}/ui.json +0 -0
  61. {llms_py-2.0.14.dist-info → llms_py-2.0.16.dist-info}/WHEEL +0 -0
  62. {llms_py-2.0.14.dist-info → llms_py-2.0.16.dist-info}/licenses/LICENSE +0 -0
  63. {llms_py-2.0.14.dist-info → llms_py-2.0.16.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  /* tailwindcss -i ./tailwind.input.css -o ./ui/app.css --watch */
2
2
  @import "tailwindcss";
3
- @source "./lib/servicestack-vue.min.mjs";
3
+ @source "./lib/servicestack-vue.mjs";
4
4
 
5
5
  @custom-variant dark (&:where(.dark, .dark *));
6
6
 
@@ -0,0 +1,524 @@
1
+ import { ref, computed, unref } from 'vue'
2
+ import { openDB } from 'idb'
3
+ import { nextId, toModelInfo } from './utils.mjs'
4
+
5
+ // Thread store for managing chat threads with IndexedDB
6
+ const threads = ref([])
7
+ const currentThread = ref(null)
8
+ const isLoading = ref(false)
9
+
10
+ let db = null
11
+
12
+ // Initialize IndexedDB
13
+ async function initDB() {
14
+ if (db) return db
15
+
16
+ db = await openDB('LlmsThreads', 3, {
17
+ upgrade(db, _oldVersion, _newVersion, transaction) {
18
+ if (!db.objectStoreNames.contains('threads')) {
19
+ // Create threads store
20
+ const threadStore = db.createObjectStore('threads', {
21
+ keyPath: 'id',
22
+ autoIncrement: false
23
+ })
24
+
25
+ // Create indexes for efficient querying
26
+ threadStore.createIndex('createdAt', 'createdAt')
27
+ threadStore.createIndex('updatedAt', 'updatedAt')
28
+ threadStore.createIndex('title', 'title')
29
+ }
30
+ if (!db.objectStoreNames.contains('requests')) {
31
+ // Create requests store
32
+ const requestStore = db.createObjectStore('requests', {
33
+ keyPath: 'id',
34
+ autoIncrement: false
35
+ })
36
+ requestStore.createIndex('threadId', 'threadId')
37
+ requestStore.createIndex('model', 'model')
38
+ requestStore.createIndex('provider', 'provider')
39
+ requestStore.createIndex('inputTokens', 'inputTokens')
40
+ requestStore.createIndex('outputTokens', 'outputTokens')
41
+ requestStore.createIndex('cost', 'cost')
42
+ requestStore.createIndex('duration', 'duration')
43
+ requestStore.createIndex('created', 'created')
44
+ }
45
+ }
46
+ })
47
+
48
+ return db
49
+ }
50
+
51
+ // Generate unique thread ID
52
+ function generateThreadId() {
53
+ return Date.now().toString()
54
+ }
55
+
56
+ async function logRequest(threadId, model, request, response) {
57
+ await initDB()
58
+ const metadata = response.metadata || {}
59
+ const usage = response.usage || {}
60
+ const [inputPrice, outputPrice] = metadata.pricing ? metadata.pricing.split('/') : [0, 0]
61
+ const lastUserContent = request.messages?.slice().reverse().find(m => m.role === 'user')?.content
62
+ const content = Array.isArray(lastUserContent)
63
+ ? lastUserContent.filter(c => c?.text).map(c => c.text).join(' ')
64
+ : lastUserContent
65
+ const title = content.slice(0, 100) + (content.length > 100 ? '...' : '')
66
+ const inputTokens = usage?.prompt_tokens ?? 0
67
+ const outputTokens = usage?.completion_tokens ?? 0
68
+ const inputCachedTokens = usage?.prompt_token_details?.cached_tokens ?? 0
69
+ const finishReason = response.choices[0]?.finish_reason || 'unknown'
70
+
71
+ const subtractDays = (date, days) => {
72
+ const result = new Date(date * 1000)
73
+ result.setDate(result.getDate() - days)
74
+ return parseInt(result.valueOf() / 1000)
75
+ }
76
+
77
+ const log = {
78
+ id: nextId(),
79
+ threadId: threadId,
80
+ model: model.id,
81
+ provider: model.provider,
82
+ providerModel: response.model || model.provider_model,
83
+ title,
84
+ inputTokens,
85
+ outputTokens,
86
+ inputCachedTokens,
87
+ totalTokens: usage.total_tokens ?? (inputTokens + outputTokens),
88
+ inputPrice,
89
+ outputPrice,
90
+ cost: (parseFloat(inputPrice) * inputTokens) + (parseFloat(outputPrice) * outputTokens),
91
+ duration: metadata.duration ?? 0,
92
+ created: subtractDays(response.created ?? Math.floor(Date.now() / 1000), 1),
93
+ finishReason,
94
+ providerRef: response.provider,
95
+ ref: response.id || undefined,
96
+ usage: usage,
97
+ }
98
+ console.debug('logRequest', log)
99
+ const tx = db.transaction(['requests'], 'readwrite')
100
+ await tx.objectStore('requests').add(log)
101
+ await tx.complete
102
+ return log
103
+ }
104
+
105
+ // Create a new thread
106
+ async function createThread(title = 'New Chat', model = null, systemPrompt = '') {
107
+ await initDB()
108
+
109
+ const thread = {
110
+ id: generateThreadId(),
111
+ title: title,
112
+ model: model?.id ?? '',
113
+ info: toModelInfo(model),
114
+ systemPrompt: systemPrompt,
115
+ messages: [],
116
+ createdAt: new Date().toISOString(),
117
+ updatedAt: new Date().toISOString()
118
+ }
119
+
120
+ const tx = db.transaction(['threads'], 'readwrite')
121
+ await tx.objectStore('threads').add(thread)
122
+ await tx.complete
123
+
124
+ threads.value.unshift(thread)
125
+ // Note: currentThread will be set by router navigation
126
+
127
+ return thread
128
+ }
129
+
130
+ // Update thread
131
+ async function updateThread(threadId, updates) {
132
+ await initDB()
133
+
134
+ const tx = db.transaction(['threads'], 'readwrite')
135
+ const store = tx.objectStore('threads')
136
+
137
+ const thread = await store.get(threadId)
138
+ if (!thread) throw new Error('Thread not found')
139
+
140
+ const updatedThread = {
141
+ ...thread,
142
+ ...updates,
143
+ updatedAt: new Date().toISOString()
144
+ }
145
+
146
+ await store.put(updatedThread)
147
+ await tx.complete
148
+
149
+ // Update in memory
150
+ const index = threads.value.findIndex(t => t.id === threadId)
151
+ if (index !== -1) {
152
+ threads.value[index] = updatedThread
153
+ }
154
+
155
+ if (currentThread.value?.id === threadId) {
156
+ currentThread.value = updatedThread
157
+ }
158
+
159
+ return updatedThread
160
+ }
161
+
162
+ async function calculateThreadStats(threadId) {
163
+ await initDB()
164
+
165
+ const tx = db.transaction(['requests'], 'readonly')
166
+ const store = tx.objectStore('requests')
167
+ const index = store.index('threadId')
168
+
169
+ const requests = await index.getAll(threadId)
170
+
171
+ let inputTokens = 0
172
+ let outputTokens = 0
173
+ let cost = 0.0
174
+ let duration = 0
175
+
176
+ requests.forEach(req => {
177
+ inputTokens += req.inputTokens || 0
178
+ outputTokens += req.outputTokens || 0
179
+ cost += req.cost || 0.0
180
+ duration += req.duration || 0
181
+ })
182
+
183
+ return {
184
+ inputTokens,
185
+ outputTokens,
186
+ cost,
187
+ duration,
188
+ requests: requests.length
189
+ }
190
+ }
191
+
192
+ // Add message to thread
193
+ async function addMessageToThread(threadId, message, usage) {
194
+ const thread = await getThread(threadId)
195
+ if (!thread) throw new Error('Thread not found')
196
+
197
+ const newMessage = {
198
+ id: nextId(),
199
+ timestamp: new Date().toISOString(),
200
+ ...message
201
+ }
202
+
203
+ // Add input and output token usage to previous 'input' message
204
+ if (usage?.prompt_tokens != null) {
205
+ const lastMessage = thread.messages[thread.messages.length - 1]
206
+ if (lastMessage && lastMessage.role === 'user') {
207
+ lastMessage.usage = {
208
+ tokens: parseInt(usage.prompt_tokens),
209
+ price: usage.input || '0',
210
+ }
211
+ }
212
+ }
213
+ if (usage?.completion_tokens != null) {
214
+ newMessage.usage = {
215
+ tokens: parseInt(usage.completion_tokens),
216
+ price: usage.output || '0',
217
+ duration: usage.duration || undefined,
218
+ }
219
+ }
220
+
221
+ const updatedMessages = [...thread.messages, newMessage]
222
+
223
+ // Auto-generate title from first user message if still "New Chat"
224
+ let title = thread.title
225
+ if (title === 'New Chat' && message.role === 'user' && updatedMessages.length <= 2) {
226
+ title = message.content.slice(0, 200) + (message.content.length > 200 ? '...' : '')
227
+ }
228
+
229
+ const stats = await calculateThreadStats(threadId)
230
+
231
+ await updateThread(threadId, {
232
+ messages: updatedMessages,
233
+ title: title,
234
+ stats,
235
+ })
236
+
237
+ return newMessage
238
+ }
239
+
240
+ async function deleteMessageFromThread(threadId, messageId) {
241
+ const thread = await getThread(threadId)
242
+ if (!thread) throw new Error('Thread not found')
243
+ const updatedMessages = thread.messages.filter(m => m.id !== messageId)
244
+ await updateThread(threadId, { messages: updatedMessages })
245
+ }
246
+
247
+ async function updateMessageInThread(threadId, messageId, updates) {
248
+ const thread = await getThread(threadId)
249
+ if (!thread) throw new Error('Thread not found')
250
+
251
+ const messageIndex = thread.messages.findIndex(m => m.id === messageId)
252
+ if (messageIndex === -1) throw new Error('Message not found')
253
+
254
+ const updatedMessages = [...thread.messages]
255
+ updatedMessages[messageIndex] = {
256
+ ...updatedMessages[messageIndex],
257
+ ...updates
258
+ }
259
+
260
+ await updateThread(threadId, { messages: updatedMessages })
261
+ }
262
+
263
+ async function redoMessageFromThread(threadId, messageId) {
264
+ const thread = await getThread(threadId)
265
+ if (!thread) throw new Error('Thread not found')
266
+
267
+ // Find the index of the message to redo
268
+ const messageIndex = thread.messages.findIndex(m => m.id === messageId)
269
+ if (messageIndex === -1) throw new Error('Message not found')
270
+
271
+ // Keep only messages up to and including the target message
272
+ const updatedMessages = thread.messages.slice(0, messageIndex + 1)
273
+
274
+ // Update the thread with the new messages
275
+ await updateThread(threadId, { messages: updatedMessages })
276
+ }
277
+
278
+ // Get all threads
279
+ async function loadThreads() {
280
+ await initDB()
281
+ isLoading.value = true
282
+
283
+ try {
284
+ const tx = db.transaction(['threads'], 'readonly')
285
+ const store = tx.objectStore('threads')
286
+ const index = store.index('updatedAt')
287
+
288
+ const allThreads = await index.getAll()
289
+ threads.value = allThreads.reverse() // Most recent first
290
+
291
+ return threads.value
292
+ } finally {
293
+ isLoading.value = false
294
+ }
295
+ }
296
+
297
+ // Get single thread
298
+ async function getThread(threadId) {
299
+ await initDB()
300
+
301
+ const tx = db.transaction(['threads'], 'readonly')
302
+ const thread = await tx.objectStore('threads').get(threadId)
303
+
304
+ return thread
305
+ }
306
+
307
+ // Delete thread
308
+ async function deleteThread(threadId) {
309
+ await initDB()
310
+
311
+ const tx = db.transaction(['threads'], 'readwrite')
312
+ await tx.objectStore('threads').delete(threadId)
313
+ await tx.complete
314
+
315
+ threads.value = threads.value.filter(t => t.id !== threadId)
316
+
317
+ if (currentThread.value?.id === threadId) {
318
+ currentThread.value = null
319
+ }
320
+ }
321
+
322
+ // Set current thread
323
+ async function setCurrentThread(threadId) {
324
+ const thread = await getThread(threadId)
325
+ if (thread) {
326
+ currentThread.value = thread
327
+ }
328
+ return thread
329
+ }
330
+
331
+ // Set current thread from router params (router-aware version)
332
+ async function setCurrentThreadFromRoute(threadId, router) {
333
+ if (!threadId) {
334
+ currentThread.value = null
335
+ return null
336
+ }
337
+
338
+ const thread = await getThread(threadId)
339
+ if (thread) {
340
+ currentThread.value = thread
341
+ return thread
342
+ } else {
343
+ // Thread not found, redirect to home
344
+ if (router) {
345
+ router.push((globalThis.ai?.base || '') + '/')
346
+ }
347
+ currentThread.value = null
348
+ return null
349
+ }
350
+ }
351
+
352
+ // Clear current thread (go back to initial state)
353
+ function clearCurrentThread() {
354
+ currentThread.value = null
355
+ }
356
+
357
+ function getGroupedThreads(total) {
358
+ const now = new Date()
359
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
360
+ const lastWeek = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)
361
+ const lastMonth = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000)
362
+
363
+ const groups = {
364
+ today: [],
365
+ lastWeek: [],
366
+ lastMonth: [],
367
+ older: {}
368
+ }
369
+
370
+ const takeThreads = threads.value.slice(0, total)
371
+
372
+ takeThreads.forEach(thread => {
373
+ const threadDate = new Date(thread.updatedAt)
374
+
375
+ if (threadDate >= today) {
376
+ groups.today.push(thread)
377
+ } else if (threadDate >= lastWeek) {
378
+ groups.lastWeek.push(thread)
379
+ } else if (threadDate >= lastMonth) {
380
+ groups.lastMonth.push(thread)
381
+ } else {
382
+ const year = threadDate.getFullYear()
383
+ const month = threadDate.toLocaleString('default', { month: 'long' })
384
+ const key = `${month} ${year}`
385
+
386
+ if (!groups.older[key]) {
387
+ groups.older[key] = []
388
+ }
389
+ groups.older[key].push(thread)
390
+ }
391
+ })
392
+
393
+ return groups
394
+ }
395
+
396
+ // Group threads by time periods
397
+ const groupedThreads = computed(() => getGroupedThreads(threads.value.length))
398
+
399
+ // Query requests with pagination and filtering
400
+ async function getRequests(filters = {}, limit = 20, offset = 0) {
401
+ try {
402
+ await initDB()
403
+
404
+ const {
405
+ model = null,
406
+ provider = null,
407
+ threadId = null,
408
+ sortBy = 'created',
409
+ sortOrder = 'desc',
410
+ startDate = null,
411
+ endDate = null
412
+ } = filters
413
+
414
+ const tx = db.transaction(['requests'], 'readonly')
415
+ const store = tx.objectStore('requests')
416
+
417
+ // Get all requests and filter in memory (IndexedDB limitations)
418
+ const allRequests = await store.getAll()
419
+
420
+ // Apply filters
421
+ let results = allRequests.filter(req => {
422
+ if (model && req.model !== model) return false
423
+ if (provider && req.provider !== provider) return false
424
+ if (threadId && req.threadId !== threadId) return false
425
+ if (startDate && req.created < startDate) return false
426
+ if (endDate && req.created > endDate) return false
427
+ return true
428
+ })
429
+
430
+ // Sort
431
+ results.sort((a, b) => {
432
+ let aVal = a[sortBy]
433
+ let bVal = b[sortBy]
434
+
435
+ if (sortOrder === 'desc') {
436
+ return bVal - aVal
437
+ } else {
438
+ return aVal - bVal
439
+ }
440
+ })
441
+
442
+ // Paginate
443
+ const total = results.length
444
+ const paginatedResults = results.slice(offset, offset + limit)
445
+
446
+ return {
447
+ requests: paginatedResults,
448
+ total,
449
+ hasMore: offset + limit < total
450
+ }
451
+ } catch (error) {
452
+ console.error('Error in getRequests:', error)
453
+ return {
454
+ requests: [],
455
+ total: 0,
456
+ hasMore: false
457
+ }
458
+ }
459
+ }
460
+
461
+ // Get unique values for filter options
462
+ async function getFilterOptions() {
463
+ try {
464
+ await initDB()
465
+
466
+ const tx = db.transaction(['requests'], 'readonly')
467
+ const store = tx.objectStore('requests')
468
+ const allRequests = await store.getAll()
469
+
470
+ const models = [...new Set(allRequests.map(r => r.model).filter(m => m))].sort()
471
+ const providers = [...new Set(allRequests.map(r => r.provider).filter(p => p))].sort()
472
+
473
+ return {
474
+ models,
475
+ providers
476
+ }
477
+ } catch (error) {
478
+ console.error('Error in getFilterOptions:', error)
479
+ return {
480
+ models: [],
481
+ providers: []
482
+ }
483
+ }
484
+ }
485
+
486
+ // Delete a request by ID
487
+ async function deleteRequest(requestId) {
488
+ await initDB()
489
+
490
+ const tx = db.transaction(['requests'], 'readwrite')
491
+ await tx.objectStore('requests').delete(requestId)
492
+ await tx.complete
493
+ }
494
+
495
+ // Export the store
496
+ export function useThreadStore() {
497
+ return {
498
+ // State
499
+ threads,
500
+ currentThread,
501
+ isLoading,
502
+ groupedThreads,
503
+
504
+ // Actions
505
+ initDB,
506
+ logRequest,
507
+ createThread,
508
+ updateThread,
509
+ addMessageToThread,
510
+ deleteMessageFromThread,
511
+ updateMessageInThread,
512
+ redoMessageFromThread,
513
+ loadThreads,
514
+ getThread,
515
+ deleteThread,
516
+ setCurrentThread,
517
+ setCurrentThreadFromRoute,
518
+ clearCurrentThread,
519
+ getGroupedThreads,
520
+ getRequests,
521
+ getFilterOptions,
522
+ deleteRequest,
523
+ }
524
+ }
@@ -54,6 +54,42 @@ export function fileToDataUri(file) {
54
54
  })
55
55
  }
56
56
 
57
+ export function toModelInfo(model) {
58
+ if (!model) return undefined
59
+ return Object.assign({}, model, { pricing: Object.assign({}, model.pricing) || undefined })
60
+ }
61
+
62
+ const numFmt = new Intl.NumberFormat(undefined,{style:'currency',currency:'USD', maximumFractionDigits:6})
63
+ export function tokenCost(price) {
64
+ if (!price) return ''
65
+ var ret = numFmt.format(parseFloat(price))
66
+ return ret.endsWith('.00') ? ret.slice(0, -3) : ret
67
+ }
68
+ export function formatCost(cost) {
69
+ if (!cost) return ''
70
+ return numFmt.format(parseFloat(cost))
71
+ }
72
+ export function statsTitle(stats) {
73
+ let title = []
74
+ // Each stat on its own line
75
+ if (stats.cost) {
76
+ title.push(`Total Cost: ${formatCost(stats.cost)}`)
77
+ }
78
+ if (stats.inputTokens) {
79
+ title.push(`Input Tokens: ${stats.inputTokens}`)
80
+ }
81
+ if (stats.outputTokens) {
82
+ title.push(`Output Tokens: ${stats.outputTokens}`)
83
+ }
84
+ if (stats.requests) {
85
+ title.push(`Requests: ${stats.requests}`)
86
+ }
87
+ if (stats.duration) {
88
+ title.push(`Duration: ${stats.duration}ms`)
89
+ }
90
+ return title.join('\n')
91
+ }
92
+
57
93
  const svg = {
58
94
  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>`,
59
95
  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>`,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llms-py
3
- Version: 2.0.14
3
+ Version: 2.0.16
4
4
  Summary: A lightweight CLI tool and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers
5
5
  Home-page: https://github.com/ServiceStack/llms
6
6
  Author: ServiceStack
@@ -42,7 +42,7 @@ Dynamic: requires-python
42
42
 
43
43
  Lightweight CLI and OpenAI-compatible server for querying multiple Large Language Model (LLM) providers.
44
44
 
45
- Configure additional providers and models in [llms.json](llms.json)
45
+ Configure additional providers and models in [llms.json](llms/llms.json)
46
46
  - Mix and match local models with models from different API providers
47
47
  - Requests automatically routed to available providers that supports the requested model (in defined order)
48
48
  - Define free/cheapest/local providers first to save on costs
@@ -73,28 +73,10 @@ Read the [Introductory Blog Post](https://servicestack.net/posts/llms-py-ui).
73
73
 
74
74
  ## Installation
75
75
 
76
- ### Option 1: Install from PyPI
77
-
78
76
  ```bash
79
77
  pip install llms-py
80
78
  ```
81
79
 
82
- ### Option 2: Download directly
83
-
84
- 1. Download [llms.py](llms.py)
85
-
86
- ```bash
87
- curl -O https://raw.githubusercontent.com/ServiceStack/llms/main/llms.py
88
- chmod +x llms.py
89
- mv llms.py ~/.local/bin/llms
90
- ```
91
-
92
- 2. Install single dependency:
93
-
94
- ```bash
95
- pip install aiohttp
96
- ```
97
-
98
80
  ## Quick Start
99
81
 
100
82
  ### 1. Set API Keys
@@ -102,12 +84,12 @@ pip install aiohttp
102
84
  Set environment variables for the providers you want to use:
103
85
 
104
86
  ```bash
105
- export OPENROUTER_FREE_API_KEY="..."
87
+ export OPENROUTER_API_KEY="..."
106
88
  ```
107
89
 
108
90
  | Provider | Variable | Description | Example |
109
91
  |-----------------|---------------------------|---------------------|---------|
110
- | openrouter_free | `OPENROUTER_FREE_API_KEY` | OpenRouter FREE models API key | `sk-or-...` |
92
+ | openrouter_free | `OPENROUTER_API_KEY` | OpenRouter FREE models API key | `sk-or-...` |
111
93
  | groq | `GROQ_API_KEY` | Groq API key | `gsk_...` |
112
94
  | google_free | `GOOGLE_FREE_API_KEY` | Google FREE API key | `AIza...` |
113
95
  | codestral | `CODESTRAL_API_KEY` | Codestral API key | `...` |
@@ -151,7 +133,7 @@ llms "What is the capital of France?"
151
133
 
152
134
  ## Configuration
153
135
 
154
- The configuration file [llms.json](llms.json) is saved to `~/.llms/llms.json` and defines available providers, models, and default settings. Key sections:
136
+ The configuration file [llms.json](llms/llms.json) is saved to `~/.llms/llms.json` and defines available providers, models, and default settings. Key sections:
155
137
 
156
138
  ### Defaults
157
139
  - `headers`: Common HTTP headers for all requests
@@ -193,7 +175,7 @@ llms "Explain quantum computing" --raw
193
175
 
194
176
  ### Using a Chat Template
195
177
 
196
- By default llms uses the `defaults/text` chat completion request defined in [llms.json](llms.json).
178
+ By default llms uses the `defaults/text` chat completion request defined in [llms.json](llms/llms.json).
197
179
 
198
180
  You can instead use a custom chat completion request with `--chat`, e.g:
199
181
 
@@ -485,19 +467,10 @@ llms --default grok-4
485
467
 
486
468
  ### Update
487
469
 
488
- 1. Installed from PyPI
489
-
490
470
  ```bash
491
471
  pip install llms-py --upgrade
492
472
  ```
493
473
 
494
- 2. Using Direct Download
495
-
496
- ```bash
497
- # Update to latest version (Downloads latest llms.py)
498
- llms --update
499
- ```
500
-
501
474
  ### Advanced Options
502
475
 
503
476
  ```bash
@@ -596,7 +569,7 @@ llms --update
596
569
  ```
597
570
 
598
571
  This command:
599
- - Downloads the latest `llms.py` from `https://raw.githubusercontent.com/ServiceStack/llms/refs/heads/main/llms.py`
572
+ - Downloads the latest `llms.py` from `github.com/ServiceStack/llms/blob/main/llms/main.py`
600
573
  - Overwrites your current `llms.py` file with the latest version
601
574
  - Preserves your existing configuration file (`llms.json`)
602
575
  - Requires an internet connection to download the update
@@ -633,7 +606,7 @@ or directly in your `llms.json`.
633
606
 
634
607
  | Provider | Variable | Description | Example |
635
608
  |-----------------|---------------------------|---------------------|---------|
636
- | openrouter_free | `OPENROUTER_FREE_API_KEY` | OpenRouter FREE models API key | `sk-or-...` |
609
+ | openrouter_free | `OPENROUTER_API_KEY` | OpenRouter FREE models API key | `sk-or-...` |
637
610
  | groq | `GROQ_API_KEY` | Groq API key | `gsk_...` |
638
611
  | google_free | `GOOGLE_FREE_API_KEY` | Google FREE API key | `AIza...` |
639
612
  | codestral | `CODESTRAL_API_KEY` | Codestral API key | `...` |