Skip to content

Commit 5a1739f

Browse files
committed
Improve Provider List
1 parent 797f15f commit 5a1739f

File tree

10 files changed

+79
-97
lines changed

10 files changed

+79
-97
lines changed

src/core/llm/providers/poe.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,12 +252,18 @@ async def get_available_models(self) -> List[Dict[str, Any]]:
252252
if request_price > 0.10:
253253
continue
254254

255+
# Skip free models (no pricing = free tier, often lower quality/rate limited)
256+
if prompt_price == 0 and completion_price == 0 and request_price == 0:
257+
continue
258+
255259
# Build display name with context info
256260
description = m.get("description", "")
261+
# Use display name from API if available, otherwise use model_id
262+
display_name = m.get("name") or model_id
257263

258264
model_info = {
259265
"id": model_id,
260-
"name": model_id,
266+
"name": display_name,
261267
"description": description[:100] if description else "",
262268
"owned_by": m.get("owned_by", ""),
263269
"context_length": self._get_context_limit_for_model(model_id),
2.9 KB
Loading
3.33 KB
Loading
3.33 KB
Loading
2.97 KB
Loading
3.12 KB
Loading
2.92 KB
Loading

src/web/static/js/providers/provider-manager.js

Lines changed: 49 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -219,29 +219,65 @@ function populateModelSelect(models, defaultModel = null, provider = 'ollama') {
219219
}
220220
modelSelect.appendChild(option);
221221
});
222-
} else if (provider === 'openrouter') {
222+
} else if (provider === 'openrouter' || provider === 'poe') {
223+
// OpenRouter and Poe share the same pricing display format
224+
// Poe additionally supports grouping by owned_by and request-based pricing
225+
let currentGroup = null;
226+
let optgroup = null;
227+
223228
models.forEach(model => {
229+
// For Poe: group by owned_by or fallback group
230+
if (provider === 'poe') {
231+
const groupKey = model.group || model.owned_by;
232+
if (groupKey && groupKey !== currentGroup) {
233+
currentGroup = groupKey;
234+
optgroup = document.createElement('optgroup');
235+
optgroup.label = currentGroup;
236+
modelSelect.appendChild(optgroup);
237+
}
238+
}
239+
224240
const option = document.createElement('option');
225-
// Handle both API response format (id) and fallback format (value)
226241
const modelId = model.id || model.value;
227242
option.value = modelId;
228243

229244
// Format label with pricing info if available
230-
if (model.pricing && model.pricing.prompt_per_million !== undefined) {
231-
const inputPrice = formatPrice(model.pricing.prompt_per_million);
232-
const outputPrice = formatPrice(model.pricing.completion_per_million);
233-
option.textContent = `${model.name || modelId} (In: ${inputPrice}/M, Out: ${outputPrice}/M)`;
234-
option.title = `Context: ${model.context_length || 'N/A'} tokens`;
245+
if (model.pricing && (model.pricing.prompt_per_million !== undefined || model.pricing.request)) {
246+
if (model.pricing.request && model.pricing.request > 0) {
247+
// Request-based pricing (Poe specific)
248+
option.textContent = `${model.name || modelId} ($${model.pricing.request.toFixed(4)}/req)`;
249+
} else {
250+
// Token-based pricing (shared format)
251+
const inputPrice = formatPrice(model.pricing.prompt_per_million);
252+
const outputPrice = formatPrice(model.pricing.completion_per_million);
253+
option.textContent = `${model.name || modelId} (In: ${inputPrice}/M, Out: ${outputPrice}/M)`;
254+
}
235255
} else {
236-
// Fallback format
256+
// Fallback format (no pricing)
237257
option.textContent = model.label || model.name || modelId;
238258
}
239259

260+
// Build tooltip
261+
let tooltip = [];
262+
if (model.context_length) {
263+
tooltip.push(`Context: ${model.context_length} tokens`);
264+
}
265+
if (model.description) {
266+
tooltip.push(model.description);
267+
}
268+
option.title = tooltip.length > 0 ? tooltip.join(' | ') : '';
269+
240270
if (modelId === defaultModel) {
241271
option.selected = true;
242272
defaultModelFound = true;
243273
}
244-
modelSelect.appendChild(option);
274+
275+
// Add to optgroup if exists (Poe), otherwise to select
276+
if (optgroup) {
277+
optgroup.appendChild(option);
278+
} else {
279+
modelSelect.appendChild(option);
280+
}
245281
});
246282
} else if (provider === 'mistral') {
247283
models.forEach(model => {
@@ -271,73 +307,6 @@ function populateModelSelect(models, defaultModel = null, provider = 'ollama') {
271307
}
272308
modelSelect.appendChild(option);
273309
});
274-
} else if (provider === 'poe') {
275-
// Poe models - support groups for fallback, pricing for API response
276-
let currentGroup = null;
277-
let optgroup = null;
278-
279-
models.forEach(model => {
280-
// Create optgroup if group changed (for fallback models with groups)
281-
if (model.group && model.group !== currentGroup) {
282-
currentGroup = model.group;
283-
optgroup = document.createElement('optgroup');
284-
optgroup.label = currentGroup;
285-
modelSelect.appendChild(optgroup);
286-
}
287-
288-
// For API response models, group by owned_by
289-
if (!model.group && model.owned_by && model.owned_by !== currentGroup) {
290-
currentGroup = model.owned_by;
291-
optgroup = document.createElement('optgroup');
292-
optgroup.label = currentGroup;
293-
modelSelect.appendChild(optgroup);
294-
}
295-
296-
const option = document.createElement('option');
297-
const modelId = model.value || model.id;
298-
option.value = modelId;
299-
300-
// Format label with pricing info if available (from API)
301-
if (model.pricing && (model.pricing.prompt_per_million !== undefined || model.pricing.request)) {
302-
if (model.pricing.request) {
303-
// Request-based pricing
304-
const requestPrice = model.pricing.request;
305-
option.textContent = `${model.label || model.name || modelId} ($${requestPrice.toFixed(4)}/req)`;
306-
} else {
307-
// Token-based pricing
308-
const inputPrice = formatPrice(model.pricing.prompt_per_million);
309-
const outputPrice = formatPrice(model.pricing.completion_per_million);
310-
option.textContent = `${model.label || model.name || modelId} (In: ${inputPrice}/M, Out: ${outputPrice}/M)`;
311-
}
312-
} else {
313-
// Fallback format (no pricing)
314-
option.textContent = model.label || model.name || modelId;
315-
}
316-
317-
// Build tooltip with context and description
318-
let tooltip = [];
319-
if (model.context_length) {
320-
tooltip.push(`Context: ${model.context_length} tokens`);
321-
}
322-
if (model.description) {
323-
tooltip.push(model.description);
324-
}
325-
if (tooltip.length > 0) {
326-
option.title = tooltip.join(' | ');
327-
}
328-
329-
if (modelId === defaultModel) {
330-
option.selected = true;
331-
defaultModelFound = true;
332-
}
333-
334-
// Add to optgroup if exists, otherwise to select
335-
if (optgroup) {
336-
optgroup.appendChild(option);
337-
} else {
338-
modelSelect.appendChild(option);
339-
}
340-
});
341310
} else {
342311
// Ollama - models are strings
343312
models.forEach(modelName => {
@@ -1035,20 +1004,14 @@ export const ProviderManager = {
10351004
if (data.models && data.models.length > 0) {
10361005
MessageLogger.showMessage('', '');
10371006

1038-
// Format models for the dropdown
1039-
const formattedModels = data.models.map(m => ({
1040-
value: m.id,
1041-
label: m.name || m.id,
1042-
context_length: m.context_length
1043-
}));
1044-
1045-
populateModelSelect(formattedModels, data.default, 'poe');
1046-
MessageLogger.addLog(`${data.count} Poe model(s) loaded`);
1007+
// Pass models directly (same format as OpenRouter)
1008+
populateModelSelect(data.models, data.default, 'poe');
1009+
MessageLogger.addLog(`${data.count} Poe model(s) loaded (sorted by provider)`);
10471010

10481011
SettingsManager.applyPendingModelSelection();
10491012
ModelDetector.checkAndShowRecommendation();
10501013

1051-
StateManager.setState('models.availableModels', formattedModels.map(m => m.value));
1014+
StateManager.setState('models.availableModels', data.models.map(m => m.id));
10521015
StatusManager.setConnected('poe', data.count);
10531016
} else {
10541017
// Use fallback list

src/web/static/js/ui/searchable-select.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,10 @@ export class SearchableSelect {
300300
: '<span class="option-check"></span>';
301301
li.innerHTML = `
302302
${checkmark}
303-
<span class="option-label">${DomHelpers.escapeHtml(opt.label)}</span>
304-
${opt.description ? `<span class="option-description">${DomHelpers.escapeHtml(opt.description)}</span>` : ''}
303+
<span class="option-content">
304+
<span class="option-label">${DomHelpers.escapeHtml(opt.label)}</span>
305+
${opt.description ? `<span class="option-description">${DomHelpers.escapeHtml(opt.description)}</span>` : ''}
306+
</span>
305307
`;
306308
}
307309

src/web/static/style.css

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,10 +1741,20 @@ html.dark .file-info {
17411741
justify-content: center;
17421742
}
17431743

1744-
.searchable-select-option .option-label {
1744+
.searchable-select-option .option-content {
17451745
flex: 1;
1746+
min-width: 0;
1747+
display: flex;
1748+
flex-direction: column;
1749+
gap: 0.125rem;
1750+
}
1751+
1752+
.searchable-select-option .option-label {
17461753
font-size: 0.875rem;
17471754
color: var(--text-dark);
1755+
white-space: nowrap;
1756+
overflow: hidden;
1757+
text-overflow: ellipsis;
17481758
}
17491759

17501760
.searchable-select-option.selected .option-label {
@@ -1755,27 +1765,28 @@ html.dark .file-info {
17551765
.searchable-select-option .option-description {
17561766
font-size: 0.75rem;
17571767
color: var(--text-muted-light);
1768+
white-space: nowrap;
1769+
overflow: hidden;
1770+
text-overflow: ellipsis;
17581771
}
17591772

1760-
/* Group headers */
1773+
/* Group headers - sticky with opaque background */
17611774
.searchable-select-group {
17621775
padding: 0.625rem 1rem 0.375rem;
17631776
font-size: 0.7rem;
17641777
font-weight: 700;
17651778
text-transform: uppercase;
17661779
letter-spacing: 0.05em;
17671780
color: var(--primary-light);
1768-
background: rgba(54, 118, 216, 0.05);
1769-
border-top: 1px solid var(--border-light);
1770-
margin-top: 0.25rem;
1781+
background: #f8f9fc;
1782+
border-bottom: 1px solid var(--border-light);
17711783
position: sticky;
17721784
top: 0;
1773-
z-index: 1;
1785+
z-index: 10;
17741786
}
17751787

17761788
.searchable-select-group:first-child {
17771789
border-top: none;
1778-
margin-top: 0;
17791790
}
17801791

17811792
/* No results */
@@ -1851,7 +1862,7 @@ html.dark .searchable-select-option.selected {
18511862
}
18521863

18531864
html.dark .searchable-select-group {
1854-
background: rgba(90, 142, 232, 0.08);
1865+
background: #2a2a2a;
18551866
border-color: #444444;
18561867
}
18571868

0 commit comments

Comments
 (0)