llms-py 2.0.15__py3-none-any.whl → 2.0.17__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-313.pyc +0 -0
- llms/__pycache__/__init__.cpython-314.pyc +0 -0
- llms/__pycache__/__main__.cpython-314.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/index.html +5 -1
- llms/llms.json +722 -67
- llms/main.py +203 -11
- llms/ui/Analytics.mjs +1517 -0
- llms/ui/Brand.mjs +19 -8
- llms/ui/ChatPrompt.mjs +58 -36
- llms/ui/Main.mjs +205 -5
- llms/ui/ModelSelector.mjs +35 -4
- llms/ui/ProviderIcon.mjs +29 -0
- llms/ui/Sidebar.mjs +20 -4
- llms/ui/ai.mjs +1 -1
- llms/ui/app.css +211 -64
- llms/ui/lib/chart.js +14 -0
- llms/ui/lib/charts.mjs +20 -0
- llms/ui/lib/color.js +14 -0
- llms/ui/tailwind.input.css +1 -1
- llms/ui/threadStore.mjs +270 -19
- llms/ui/utils.mjs +36 -0
- {llms_py-2.0.15.dist-info → llms_py-2.0.17.dist-info}/METADATA +8 -35
- llms_py-2.0.17.dist-info/RECORD +56 -0
- llms_py-2.0.15.dist-info/RECORD +0 -46
- {llms_py-2.0.15.dist-info → llms_py-2.0.17.dist-info}/WHEEL +0 -0
- {llms_py-2.0.15.dist-info → llms_py-2.0.17.dist-info}/entry_points.txt +0 -0
- {llms_py-2.0.15.dist-info → llms_py-2.0.17.dist-info}/licenses/LICENSE +0 -0
- {llms_py-2.0.15.dist-info → llms_py-2.0.17.dist-info}/top_level.txt +0 -0
llms/ui/threadStore.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ref, computed } from 'vue'
|
|
1
|
+
import { ref, computed, unref } from 'vue'
|
|
2
2
|
import { openDB } from 'idb'
|
|
3
|
-
import { nextId } from './utils.mjs'
|
|
3
|
+
import { nextId, toModelInfo } from './utils.mjs'
|
|
4
4
|
|
|
5
5
|
// Thread store for managing chat threads with IndexedDB
|
|
6
6
|
const threads = ref([])
|
|
@@ -13,18 +13,35 @@ let db = null
|
|
|
13
13
|
async function initDB() {
|
|
14
14
|
if (db) return db
|
|
15
15
|
|
|
16
|
-
db = await openDB('LlmsThreads',
|
|
17
|
-
upgrade(db) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
+
}
|
|
28
45
|
}
|
|
29
46
|
})
|
|
30
47
|
|
|
@@ -36,14 +53,64 @@ function generateThreadId() {
|
|
|
36
53
|
return Date.now().toString()
|
|
37
54
|
}
|
|
38
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: response.created ?? Math.floor(Date.now() / 1000),
|
|
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
|
+
|
|
39
105
|
// Create a new thread
|
|
40
|
-
async function createThread(title = 'New Chat', model =
|
|
106
|
+
async function createThread(title = 'New Chat', model = null, systemPrompt = '') {
|
|
41
107
|
await initDB()
|
|
42
|
-
|
|
108
|
+
|
|
43
109
|
const thread = {
|
|
44
110
|
id: generateThreadId(),
|
|
45
111
|
title: title,
|
|
46
|
-
model: model,
|
|
112
|
+
model: model?.id ?? '',
|
|
113
|
+
info: toModelInfo(model),
|
|
47
114
|
systemPrompt: systemPrompt,
|
|
48
115
|
messages: [],
|
|
49
116
|
createdAt: new Date().toISOString(),
|
|
@@ -92,8 +159,38 @@ async function updateThread(threadId, updates) {
|
|
|
92
159
|
return updatedThread
|
|
93
160
|
}
|
|
94
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
|
+
|
|
95
192
|
// Add message to thread
|
|
96
|
-
async function addMessageToThread(threadId, message) {
|
|
193
|
+
async function addMessageToThread(threadId, message, usage) {
|
|
97
194
|
const thread = await getThread(threadId)
|
|
98
195
|
if (!thread) throw new Error('Thread not found')
|
|
99
196
|
|
|
@@ -103,6 +200,24 @@ async function addMessageToThread(threadId, message) {
|
|
|
103
200
|
...message
|
|
104
201
|
}
|
|
105
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
|
+
|
|
106
221
|
const updatedMessages = [...thread.messages, newMessage]
|
|
107
222
|
|
|
108
223
|
// Auto-generate title from first user message if still "New Chat"
|
|
@@ -110,10 +225,13 @@ async function addMessageToThread(threadId, message) {
|
|
|
110
225
|
if (title === 'New Chat' && message.role === 'user' && updatedMessages.length <= 2) {
|
|
111
226
|
title = message.content.slice(0, 200) + (message.content.length > 200 ? '...' : '')
|
|
112
227
|
}
|
|
228
|
+
|
|
229
|
+
const stats = await calculateThreadStats(threadId)
|
|
113
230
|
|
|
114
231
|
await updateThread(threadId, {
|
|
115
232
|
messages: updatedMessages,
|
|
116
|
-
title: title
|
|
233
|
+
title: title,
|
|
234
|
+
stats,
|
|
117
235
|
})
|
|
118
236
|
|
|
119
237
|
return newMessage
|
|
@@ -126,6 +244,37 @@ async function deleteMessageFromThread(threadId, messageId) {
|
|
|
126
244
|
await updateThread(threadId, { messages: updatedMessages })
|
|
127
245
|
}
|
|
128
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
|
+
|
|
129
278
|
// Get all threads
|
|
130
279
|
async function loadThreads() {
|
|
131
280
|
await initDB()
|
|
@@ -247,6 +396,102 @@ function getGroupedThreads(total) {
|
|
|
247
396
|
// Group threads by time periods
|
|
248
397
|
const groupedThreads = computed(() => getGroupedThreads(threads.value.length))
|
|
249
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
|
+
|
|
250
495
|
// Export the store
|
|
251
496
|
export function useThreadStore() {
|
|
252
497
|
return {
|
|
@@ -258,10 +503,13 @@ export function useThreadStore() {
|
|
|
258
503
|
|
|
259
504
|
// Actions
|
|
260
505
|
initDB,
|
|
506
|
+
logRequest,
|
|
261
507
|
createThread,
|
|
262
508
|
updateThread,
|
|
263
509
|
addMessageToThread,
|
|
264
510
|
deleteMessageFromThread,
|
|
511
|
+
updateMessageInThread,
|
|
512
|
+
redoMessageFromThread,
|
|
265
513
|
loadThreads,
|
|
266
514
|
getThread,
|
|
267
515
|
deleteThread,
|
|
@@ -269,5 +517,8 @@ export function useThreadStore() {
|
|
|
269
517
|
setCurrentThreadFromRoute,
|
|
270
518
|
clearCurrentThread,
|
|
271
519
|
getGroupedThreads,
|
|
520
|
+
getRequests,
|
|
521
|
+
getFilterOptions,
|
|
522
|
+
deleteRequest,
|
|
272
523
|
}
|
|
273
524
|
}
|
llms/ui/utils.mjs
CHANGED
|
@@ -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.
|
|
3
|
+
Version: 2.0.17
|
|
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
|
|
87
|
+
export OPENROUTER_API_KEY="..."
|
|
106
88
|
```
|
|
107
89
|
|
|
108
90
|
| Provider | Variable | Description | Example |
|
|
109
91
|
|-----------------|---------------------------|---------------------|---------|
|
|
110
|
-
| openrouter_free | `
|
|
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 `
|
|
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 | `
|
|
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 | `...` |
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
llms/__init__.py,sha256=Mk6eHi13yoUxLlzhwfZ6A1IjsfSQt9ShhOdbLXTvffU,53
|
|
2
|
+
llms/__main__.py,sha256=hrBulHIt3lmPm1BCyAEVtB6DQ0Hvc3gnIddhHCmJasg,151
|
|
3
|
+
llms/index.html,sha256=OA9mRmgh-dQrPqb0Z2Jv-cwEZ3YLPRxcWUN7ASjxO8s,2658
|
|
4
|
+
llms/llms.json,sha256=nqFxASGkpITzrfT3r4bRW7v7Ys2GuGIhmz3HZGsppnw,40925
|
|
5
|
+
llms/main.py,sha256=8d78tejZl7EA-JYtzA7CNy7-xBAGBkgEmP3DMgesTMA,71376
|
|
6
|
+
llms/ui.json,sha256=iBOmpNeD5-o8AgUa51ymS-KemovJ7bm9J1fnL0nf8jk,134025
|
|
7
|
+
llms/__pycache__/__init__.cpython-312.pyc,sha256=e6TG6e0PTRxM_LRg25eL4FvjQ0wBbfeahICORONYgDk,173
|
|
8
|
+
llms/__pycache__/__init__.cpython-313.pyc,sha256=DvmykIYAjV4Xjv5NeC70BA0JIX8qtwEWP2dfzmOBrps,173
|
|
9
|
+
llms/__pycache__/__init__.cpython-314.pyc,sha256=6G4fhFLhDJOx_Jk5SXH-4GnLvCtwxcQDo5KofT1ZFKY,175
|
|
10
|
+
llms/__pycache__/__main__.cpython-312.pyc,sha256=fcu9LVl5nfdSzg9HqoKyDGoDvENeIXKkjygN7FENEGE,321
|
|
11
|
+
llms/__pycache__/__main__.cpython-314.pyc,sha256=IFxtGVpJq_3whKuM5Ln7YMweKFNbHVp8M7VSrLK5h2A,324
|
|
12
|
+
llms/__pycache__/llms.cpython-312.pyc,sha256=S5dFI79JdUe2dQW4ogdB-CCNhudQeFaFGcfKxgJGBms,72080
|
|
13
|
+
llms/__pycache__/main.cpython-312.pyc,sha256=pqZ-h6Itl6gNQpNwpp_npEYGl-cNxAYDmogr721XoeU,72085
|
|
14
|
+
llms/__pycache__/main.cpython-313.pyc,sha256=6NQ__SJ2rC9ItFLKLHL5ewb5RqxLzZabwgczA9wZd-w,74814
|
|
15
|
+
llms/__pycache__/main.cpython-314.pyc,sha256=WtfggYctsz-pwCdo4gGPO3UcFZAf6VQEK_Gy-fveeEg,89937
|
|
16
|
+
llms/ui/Analytics.mjs,sha256=mAS5AUQjpnEIMyzGzOGE6fZxwxoVyq5QCitYQSSCEpQ,69151
|
|
17
|
+
llms/ui/App.mjs,sha256=hXtUjaL3GrcIHieEK3BzIG72OVzrorBBS4RkE1DOGc4,439
|
|
18
|
+
llms/ui/Avatar.mjs,sha256=3rHpxe_LuCDiNP895F3FOjWx4j377JA9rD1FLluvtgA,851
|
|
19
|
+
llms/ui/Brand.mjs,sha256=0NN2JBLUC0OWERuLz9myrimlcA7v7D5B_EMd0sQQVDo,1905
|
|
20
|
+
llms/ui/ChatPrompt.mjs,sha256=85O_kLVKWbbUDOUlvkuAineam_jrd6lzrj4O00p1XOg,21172
|
|
21
|
+
llms/ui/Main.mjs,sha256=BatB0UZVhWrv0dYSTy5EGeVDAF_T_H2XEVPlWRExRvE,38124
|
|
22
|
+
llms/ui/ModelSelector.mjs,sha256=ASLTUaqig3cDMiGup01rpubC2RrrZvPd8IFrYcK8GyQ,2565
|
|
23
|
+
llms/ui/ProviderIcon.mjs,sha256=HTjlgtXEpekn8iNN_S0uswbbvL0iGb20N15-_lXdojk,9054
|
|
24
|
+
llms/ui/ProviderStatus.mjs,sha256=qF_rPdhyt9GffKdPCJdU0yanrDJ3cw1HLPygFP_KjEs,5744
|
|
25
|
+
llms/ui/Recents.mjs,sha256=hmj7V-RXVw-DqMXjUr3OhFHTYQTkvkEhuNEDTGBf3Qw,8448
|
|
26
|
+
llms/ui/SettingsDialog.mjs,sha256=Jm21s5CcZT97ZEhPc7c1WgnLOLdfSiC1SDroYsh7zM4,18095
|
|
27
|
+
llms/ui/Sidebar.mjs,sha256=XPdbJo87H1vgTG0R9Ssqo_8XNhn9umkGwbubLpIOWqY,10519
|
|
28
|
+
llms/ui/SignIn.mjs,sha256=df3b-7L3ZIneDGbJWUk93K9RGo40gVeuR5StzT1ZH9g,2324
|
|
29
|
+
llms/ui/SystemPromptEditor.mjs,sha256=2CyIUvkIubqYPyIp5zC6_I8CMxvYINuYNjDxvMz4VRU,1265
|
|
30
|
+
llms/ui/SystemPromptSelector.mjs,sha256=AuEtRwUf_RkGgene3nVA9bw8AeMb-b5_6ZLJCTWA8KQ,3051
|
|
31
|
+
llms/ui/Welcome.mjs,sha256=QFAxN7sjWlhMvOIJCmHjNFCQcvpM_T-b4ze1ld9Hj7I,912
|
|
32
|
+
llms/ui/ai.mjs,sha256=t-dabzCgosc75Lbwp35KbJxBqGVncWVlm7aCmSFCYgM,2346
|
|
33
|
+
llms/ui/app.css,sha256=e81FHQ-K7TlS7Cr2x_CCHqrvmVvg9I-m0InLQHRT_Dg,98992
|
|
34
|
+
llms/ui/fav.svg,sha256=_R6MFeXl6wBFT0lqcUxYQIDWgm246YH_3hSTW0oO8qw,734
|
|
35
|
+
llms/ui/markdown.mjs,sha256=O5UspOeD8-E23rxOLWcS4eyy2YejMbPwszCYteVtuoU,6221
|
|
36
|
+
llms/ui/tailwind.input.css,sha256=yo_3A50uyiVSUHUWeqAMorXMhCWpZoE5lTO6OJIFlYg,11974
|
|
37
|
+
llms/ui/threadStore.mjs,sha256=JKimOl-9c4p9qQ9L93tZZktmKwuzpiudXiWb4N9Ca3U,15557
|
|
38
|
+
llms/ui/typography.css,sha256=Z5Fe2IQWnh7bu1CMXniYt0SkaN2fXOFlOuniXUW8oGM,19325
|
|
39
|
+
llms/ui/utils.mjs,sha256=cYrP17JwpQk7lLqTWNgVTOD_ZZAovbWnx2QSvKzeB24,5333
|
|
40
|
+
llms/ui/lib/chart.js,sha256=dx8FdDX0Rv6OZtZjr9FQh5h-twFsKjfnb-FvFlQ--cU,196176
|
|
41
|
+
llms/ui/lib/charts.mjs,sha256=MNym9qE_2eoH6M7_8Gj9i6e6-Y3b7zw9UQWCUHRF6x0,1088
|
|
42
|
+
llms/ui/lib/color.js,sha256=DDG7Pr-qzJHTPISZNSqP_qJR8UflKHEc_56n6xrBugQ,8273
|
|
43
|
+
llms/ui/lib/highlight.min.mjs,sha256=sG7wq8bF-IKkfie7S4QSyh5DdHBRf0NqQxMOEH8-MT0,127458
|
|
44
|
+
llms/ui/lib/idb.min.mjs,sha256=CeTXyV4I_pB5vnibvJuyXdMs0iVF2ZL0Z7cdm3w_QaI,3853
|
|
45
|
+
llms/ui/lib/marked.min.mjs,sha256=QRHb_VZugcBJRD2EP6gYlVFEsJw5C2fQ8ImMf_pA2_s,39488
|
|
46
|
+
llms/ui/lib/servicestack-client.mjs,sha256=UVafVbzhJ_0N2lzv7rlzIbzwnWpoqXxGk3N3FSKgOOc,54534
|
|
47
|
+
llms/ui/lib/servicestack-vue.mjs,sha256=r_-khYokisXJAIPDLh8Wq6YtcLAY6HNjtJlCZJjLy74,215181
|
|
48
|
+
llms/ui/lib/vue-router.min.mjs,sha256=fR30GHoXI1u81zyZ26YEU105pZgbbAKSXbpnzFKIxls,30418
|
|
49
|
+
llms/ui/lib/vue.min.mjs,sha256=iXh97m5hotl0eFllb3aoasQTImvp7mQoRJ_0HoxmZkw,163811
|
|
50
|
+
llms/ui/lib/vue.mjs,sha256=dS8LKOG01t9CvZ04i0tbFXHqFXOO_Ha4NmM3BytjQAs,537071
|
|
51
|
+
llms_py-2.0.17.dist-info/licenses/LICENSE,sha256=rRryrddGfVftpde-rmAZpW0R8IJihqJ8t8wpfDXoKiQ,1549
|
|
52
|
+
llms_py-2.0.17.dist-info/METADATA,sha256=deZTzjc27mr_v6Nfs9zqz_ZYeWsJ7p0dOHZPXRlpmJ8,27902
|
|
53
|
+
llms_py-2.0.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
54
|
+
llms_py-2.0.17.dist-info/entry_points.txt,sha256=WswyE7PfnkZMIxboC-MS6flBD6wm-CYU7JSUnMhqMfM,40
|
|
55
|
+
llms_py-2.0.17.dist-info/top_level.txt,sha256=gC7hk9BKSeog8gyg-EM_g2gxm1mKHwFRfK-10BxOsa4,5
|
|
56
|
+
llms_py-2.0.17.dist-info/RECORD,,
|
llms_py-2.0.15.dist-info/RECORD
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
llms/__init__.py,sha256=Mk6eHi13yoUxLlzhwfZ6A1IjsfSQt9ShhOdbLXTvffU,53
|
|
2
|
-
llms/__main__.py,sha256=hrBulHIt3lmPm1BCyAEVtB6DQ0Hvc3gnIddhHCmJasg,151
|
|
3
|
-
llms/index.html,sha256=RmvPW7H49tHR3DdQvvSrcR_OeCt1InYd5dirBxfZCpo,2417
|
|
4
|
-
llms/llms.json,sha256=aQFm_lXsPLTWCndE2DNhTY04P576U7NFN9hx5-QwL3E,19582
|
|
5
|
-
llms/main.py,sha256=Bhmj_RE8lalMiO7U0QOuIhLIxIHLaKNFw87hIDMFOFU,62546
|
|
6
|
-
llms/ui.json,sha256=iBOmpNeD5-o8AgUa51ymS-KemovJ7bm9J1fnL0nf8jk,134025
|
|
7
|
-
llms/__pycache__/__init__.cpython-312.pyc,sha256=e6TG6e0PTRxM_LRg25eL4FvjQ0wBbfeahICORONYgDk,173
|
|
8
|
-
llms/__pycache__/__main__.cpython-312.pyc,sha256=fcu9LVl5nfdSzg9HqoKyDGoDvENeIXKkjygN7FENEGE,321
|
|
9
|
-
llms/__pycache__/llms.cpython-312.pyc,sha256=S5dFI79JdUe2dQW4ogdB-CCNhudQeFaFGcfKxgJGBms,72080
|
|
10
|
-
llms/__pycache__/main.cpython-312.pyc,sha256=Idq1-5l1oPv0fCjS4atoQ7CVS5TSj1KXCFrbHZsYXnk,72080
|
|
11
|
-
llms/ui/App.mjs,sha256=hXtUjaL3GrcIHieEK3BzIG72OVzrorBBS4RkE1DOGc4,439
|
|
12
|
-
llms/ui/Avatar.mjs,sha256=3rHpxe_LuCDiNP895F3FOjWx4j377JA9rD1FLluvtgA,851
|
|
13
|
-
llms/ui/Brand.mjs,sha256=ZkJ1Yd9ZgdTbs7f3ezM0UtsHMcm6v4-L-m3avnSXAXU,1184
|
|
14
|
-
llms/ui/ChatPrompt.mjs,sha256=IbfCx3W0SlX8x9lvaFjB1rqJqNkbeUvfqFHP9iKzZ9s,19776
|
|
15
|
-
llms/ui/Main.mjs,sha256=kLNWyXWUgHAaX3C3kVkAHOLvEI3H4bd5k9yMkUIRQPU,27348
|
|
16
|
-
llms/ui/ModelSelector.mjs,sha256=qiI-7DBwif5ipNZtzgaZQ2o_wHc23dBRMY6zVkMOCak,947
|
|
17
|
-
llms/ui/ProviderStatus.mjs,sha256=qF_rPdhyt9GffKdPCJdU0yanrDJ3cw1HLPygFP_KjEs,5744
|
|
18
|
-
llms/ui/Recents.mjs,sha256=hmj7V-RXVw-DqMXjUr3OhFHTYQTkvkEhuNEDTGBf3Qw,8448
|
|
19
|
-
llms/ui/SettingsDialog.mjs,sha256=Jm21s5CcZT97ZEhPc7c1WgnLOLdfSiC1SDroYsh7zM4,18095
|
|
20
|
-
llms/ui/Sidebar.mjs,sha256=ERm2Q7pftKTHLCjf-Q76joy1dsvtNWLoYH-wO2mS1LI,9781
|
|
21
|
-
llms/ui/SignIn.mjs,sha256=df3b-7L3ZIneDGbJWUk93K9RGo40gVeuR5StzT1ZH9g,2324
|
|
22
|
-
llms/ui/SystemPromptEditor.mjs,sha256=2CyIUvkIubqYPyIp5zC6_I8CMxvYINuYNjDxvMz4VRU,1265
|
|
23
|
-
llms/ui/SystemPromptSelector.mjs,sha256=AuEtRwUf_RkGgene3nVA9bw8AeMb-b5_6ZLJCTWA8KQ,3051
|
|
24
|
-
llms/ui/Welcome.mjs,sha256=QFAxN7sjWlhMvOIJCmHjNFCQcvpM_T-b4ze1ld9Hj7I,912
|
|
25
|
-
llms/ui/ai.mjs,sha256=isXZR2GUy-7W627mLllpuQVaWQHRtPA0LVWDJO-zMxA,2346
|
|
26
|
-
llms/ui/app.css,sha256=1Z4coREGsjMw7lYabejdaG0ZMwPpP5lmjRHRDYygq-g,94964
|
|
27
|
-
llms/ui/fav.svg,sha256=_R6MFeXl6wBFT0lqcUxYQIDWgm246YH_3hSTW0oO8qw,734
|
|
28
|
-
llms/ui/markdown.mjs,sha256=O5UspOeD8-E23rxOLWcS4eyy2YejMbPwszCYteVtuoU,6221
|
|
29
|
-
llms/ui/tailwind.input.css,sha256=P6QxCwbTBAaG2079ddgtFuuXDLIztEkJDFwj0wmOvkk,11978
|
|
30
|
-
llms/ui/threadStore.mjs,sha256=nM53p1E78Jc7tPOoyS3J3SW9F1njcZ-Fw5FlR9l-3yY,7215
|
|
31
|
-
llms/ui/typography.css,sha256=Z5Fe2IQWnh7bu1CMXniYt0SkaN2fXOFlOuniXUW8oGM,19325
|
|
32
|
-
llms/ui/utils.mjs,sha256=UKyaBsuJYEc-MWoL9b1M5-t3x6h5_HV9HsNBoDVtBRw,4226
|
|
33
|
-
llms/ui/lib/highlight.min.mjs,sha256=sG7wq8bF-IKkfie7S4QSyh5DdHBRf0NqQxMOEH8-MT0,127458
|
|
34
|
-
llms/ui/lib/idb.min.mjs,sha256=CeTXyV4I_pB5vnibvJuyXdMs0iVF2ZL0Z7cdm3w_QaI,3853
|
|
35
|
-
llms/ui/lib/marked.min.mjs,sha256=QRHb_VZugcBJRD2EP6gYlVFEsJw5C2fQ8ImMf_pA2_s,39488
|
|
36
|
-
llms/ui/lib/servicestack-client.mjs,sha256=UVafVbzhJ_0N2lzv7rlzIbzwnWpoqXxGk3N3FSKgOOc,54534
|
|
37
|
-
llms/ui/lib/servicestack-vue.mjs,sha256=r_-khYokisXJAIPDLh8Wq6YtcLAY6HNjtJlCZJjLy74,215181
|
|
38
|
-
llms/ui/lib/vue-router.min.mjs,sha256=fR30GHoXI1u81zyZ26YEU105pZgbbAKSXbpnzFKIxls,30418
|
|
39
|
-
llms/ui/lib/vue.min.mjs,sha256=iXh97m5hotl0eFllb3aoasQTImvp7mQoRJ_0HoxmZkw,163811
|
|
40
|
-
llms/ui/lib/vue.mjs,sha256=dS8LKOG01t9CvZ04i0tbFXHqFXOO_Ha4NmM3BytjQAs,537071
|
|
41
|
-
llms_py-2.0.15.dist-info/licenses/LICENSE,sha256=rRryrddGfVftpde-rmAZpW0R8IJihqJ8t8wpfDXoKiQ,1549
|
|
42
|
-
llms_py-2.0.15.dist-info/METADATA,sha256=kEJJM0Bs_c7BPRaTNR-em3T1yRkP59X8wTC-0uNUefA,28351
|
|
43
|
-
llms_py-2.0.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
44
|
-
llms_py-2.0.15.dist-info/entry_points.txt,sha256=WswyE7PfnkZMIxboC-MS6flBD6wm-CYU7JSUnMhqMfM,40
|
|
45
|
-
llms_py-2.0.15.dist-info/top_level.txt,sha256=gC7hk9BKSeog8gyg-EM_g2gxm1mKHwFRfK-10BxOsa4,5
|
|
46
|
-
llms_py-2.0.15.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|