Add support for multiple providers
This commit is contained in:
@@ -2,6 +2,7 @@ const STREAM_ENDPOINT = '/v1/chat/voice/stream';
|
||||
const ICONS_URL = '/static/icons.svg';
|
||||
const MODELS_ENDPOINT = '/v1/models';
|
||||
const MODEL_KEY = 'odidere_model';
|
||||
const PROVIDER_KEY = 'odidere_provider';
|
||||
const STORAGE_KEY = 'odidere_history';
|
||||
const SYSTEM_MESSAGE_KEY = 'odidere_system_message';
|
||||
const VOICE_KEY = 'odidere_voice';
|
||||
@@ -243,7 +244,9 @@ class Odidere {
|
||||
|
||||
// Save selections on change.
|
||||
this.$model.addEventListener('change', () => {
|
||||
localStorage.setItem(MODEL_KEY, this.$model.value);
|
||||
const $opt = this.$model.options[this.$model.selectedIndex];
|
||||
localStorage.setItem(PROVIDER_KEY, $opt.dataset.provider);
|
||||
localStorage.setItem(MODEL_KEY, $opt.dataset.model);
|
||||
});
|
||||
this.$voice.addEventListener('change', () => {
|
||||
localStorage.setItem(VOICE_KEY, this.$voice.value);
|
||||
@@ -1348,13 +1351,18 @@ class Odidere {
|
||||
// ====================
|
||||
/**
|
||||
* #fetchModels fetches available models from the API and populates selectors.
|
||||
* The response now includes models with availability status and provider grouping.
|
||||
*/
|
||||
async #fetchModels() {
|
||||
try {
|
||||
const res = await fetch(MODELS_ENDPOINT);
|
||||
if (!res.ok) throw new Error(`${res.status}`);
|
||||
const data = await res.json();
|
||||
this.#populateModels(data.models, data.default_model);
|
||||
this.#populateModels(
|
||||
data.providers,
|
||||
data.default_provider,
|
||||
data.default_model,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('failed to fetch models:', e);
|
||||
this.#populateModelsFallback();
|
||||
@@ -1452,10 +1460,12 @@ class Odidere {
|
||||
);
|
||||
}
|
||||
|
||||
const $opt = this.$model.options[this.$model.selectedIndex];
|
||||
const payload = {
|
||||
messages,
|
||||
voice: voice ?? this.$voice.value,
|
||||
model: this.$model.value,
|
||||
provider: $opt?.dataset.provider,
|
||||
model: $opt?.dataset.model,
|
||||
};
|
||||
const systemMessage = localStorage.getItem(SYSTEM_MESSAGE_KEY);
|
||||
if (systemMessage) {
|
||||
@@ -1795,21 +1805,58 @@ class Odidere {
|
||||
// RENDER: SELECTS
|
||||
// ====================
|
||||
/**
|
||||
* #populateModels populates the model selector with available options.
|
||||
* @param {Array<{id: string}>} models
|
||||
* #populateModels populates the model selector grouped by provider.
|
||||
* Models are displayed with availability indicators:
|
||||
* - Available: normal selectable option
|
||||
* - Not found: red strikethrough, disabled
|
||||
* - Provider error: grayed out, disabled
|
||||
* @param {Object<string, Array<{model: string, status: string}>>} providers
|
||||
* @param {string} defaultProvider
|
||||
* @param {string} defaultModel
|
||||
*/
|
||||
#populateModels(models, defaultModel) {
|
||||
#populateModels(providers, defaultProvider, defaultModel) {
|
||||
this.$model.innerHTML = '';
|
||||
|
||||
for (const m of models) {
|
||||
const $opt = this.document.createElement('option');
|
||||
$opt.value = m.id;
|
||||
$opt.textContent = m.id;
|
||||
this.$model.appendChild($opt);
|
||||
// Sort provider names.
|
||||
const sortedProviders = Object.keys(providers).sort();
|
||||
|
||||
// Create optgroups for each provider.
|
||||
for (const provider of sortedProviders) {
|
||||
const $optgroup = this.document.createElement('optgroup');
|
||||
$optgroup.label = provider;
|
||||
|
||||
// Sort models within provider.
|
||||
for (const m of providers[provider].sort((a, b) =>
|
||||
a.model.localeCompare(b.model),
|
||||
)) {
|
||||
const $opt = this.document.createElement('option');
|
||||
$opt.value = m.model;
|
||||
$opt.dataset.provider = provider;
|
||||
$opt.dataset.model = m.model;
|
||||
|
||||
switch (m.status) {
|
||||
case 'available':
|
||||
$opt.textContent = m.model;
|
||||
break;
|
||||
case 'not_found':
|
||||
$opt.textContent = `~~${m.model}~~ (not found)`;
|
||||
$opt.disabled = true;
|
||||
$opt.classList.add('model-unavailable-not-found');
|
||||
break;
|
||||
case 'provider_error':
|
||||
$opt.textContent = `${m.model} (provider error)`;
|
||||
$opt.disabled = true;
|
||||
$opt.classList.add('model-unavailable-error');
|
||||
break;
|
||||
}
|
||||
|
||||
$optgroup.appendChild($opt);
|
||||
}
|
||||
|
||||
this.$model.appendChild($optgroup);
|
||||
}
|
||||
|
||||
this.#loadModel(defaultModel);
|
||||
this.#loadModel(defaultProvider, defaultModel);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1829,24 +1876,37 @@ class Odidere {
|
||||
/**
|
||||
* #loadModel restores the model selection from localStorage or uses the
|
||||
* default.
|
||||
* @param {string} defaultProvider
|
||||
* @param {string} defaultModel
|
||||
*/
|
||||
#loadModel(defaultModel) {
|
||||
const stored = localStorage.getItem(MODEL_KEY);
|
||||
const escaped = stored ? CSS.escape(stored) : null;
|
||||
#loadModel(defaultProvider, defaultModel) {
|
||||
const storedProvider = localStorage.getItem(PROVIDER_KEY);
|
||||
const storedModel = localStorage.getItem(MODEL_KEY);
|
||||
|
||||
// Try stored value first, then default, then first available option.
|
||||
let selectedValue;
|
||||
if (escaped && this.$model.querySelector(`option[value="${escaped}"]`)) {
|
||||
selectedValue = stored;
|
||||
} else if (defaultModel) {
|
||||
selectedValue = defaultModel;
|
||||
} else if (this.$model.options.length > 0) {
|
||||
selectedValue = this.$model.options[0].value;
|
||||
let $opt;
|
||||
if (storedProvider && storedModel) {
|
||||
$opt = this.$model.querySelector(
|
||||
`option[data-provider="${CSS.escape(storedProvider)}"][data-model="${CSS.escape(storedModel)}"]`,
|
||||
);
|
||||
}
|
||||
if (!$opt && defaultProvider && defaultModel) {
|
||||
$opt = this.$model.querySelector(
|
||||
`option[data-provider="${CSS.escape(defaultProvider)}"][data-model="${CSS.escape(defaultModel)}"]`,
|
||||
);
|
||||
}
|
||||
if (!$opt && this.$model.options.length > 0) {
|
||||
// Find first available (non-disabled) option.
|
||||
for (const opt of this.$model.options) {
|
||||
if (!opt.disabled) {
|
||||
$opt = opt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedValue) {
|
||||
this.$model.value = selectedValue;
|
||||
if ($opt) {
|
||||
this.$model.value = $opt.value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user