diff --git a/package.json b/package.json index 7860d3a..b9f5f60 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@huggingface/transformers": "^3.8.1", "electron-squirrel-startup": "^1.0.1", "ollama": "^0.6.3", + "openai": "^6.22.0", "p-retry": "^4.6.2", "ws": "^8.19.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 64d14f4..27608cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: ollama: specifier: ^0.6.3 version: 0.6.3 + openai: + specifier: ^6.22.0 + version: 6.22.0(ws@8.19.0) p-retry: specifier: 4.6.2 version: 4.6.2 @@ -1750,6 +1753,18 @@ packages: onnxruntime-web@1.22.0-dev.20250409-89f8206ba4: resolution: {integrity: sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==} + openai@6.22.0: + resolution: {integrity: sha512-7Yvy17F33Bi9RutWbsaYt5hJEEJ/krRPOrwan+f9aCPuMat1WVsb2VNSII5W1EksKT6fF69TG/xj4XzodK3JZw==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} @@ -4522,6 +4537,10 @@ snapshots: platform: 1.3.6 protobufjs: 7.5.4 + openai@6.22.0(ws@8.19.0): + optionalDependencies: + ws: 8.19.0 + ora@5.4.1: dependencies: bl: 4.1.0 diff --git a/src/components/views/MainView.js b/src/components/views/MainView.js index 1182d82..3f9b199 100644 --- a/src/components/views/MainView.js +++ b/src/components/views/MainView.js @@ -417,6 +417,9 @@ export class MainView extends LitElement { _openaiCompatibleApiKey: { state: true }, _openaiCompatibleBaseUrl: { state: true }, _openaiCompatibleModel: { state: true }, + _availableModels: { state: true }, + _loadingModels: { state: true }, + _manualModelInput: { state: true }, _responseProvider: { state: true }, _tokenError: { state: true }, _keyError: { state: true }, @@ -444,6 +447,9 @@ export class MainView extends LitElement { this._openaiCompatibleApiKey = ''; this._openaiCompatibleBaseUrl = ''; this._openaiCompatibleModel = ''; + this._availableModels = []; + this._loadingModels = false; + this._manualModelInput = false; this._responseProvider = 'gemini'; this._tokenError = false; this._keyError = false; @@ -491,6 +497,11 @@ export class MainView extends LitElement { this._whisperModel = prefs.whisperModel || 'Xenova/whisper-small'; this.requestUpdate(); + + // Auto-load models if OpenAI-compatible is selected and URL is set + if (this._responseProvider === 'openai-compatible' && this._openaiCompatibleBaseUrl) { + this._loadModels(); + } } catch (e) { console.error('Error loading MainView storage:', e); } @@ -505,6 +516,7 @@ export class MainView extends LitElement { super.disconnectedCallback(); document.removeEventListener('keydown', this.boundKeydownHandler); if (this._animId) cancelAnimationFrame(this._animId); + if (this._loadModelsTimeout) clearTimeout(this._loadModelsTimeout); } updated(changedProperties) { @@ -656,6 +668,8 @@ export class MainView extends LitElement { this._openaiCompatibleModel ); this.requestUpdate(); + // Auto-load models when both key and URL are set + this._debouncedLoadModels(); } async _saveOpenAICompatibleBaseUrl(val) { @@ -666,6 +680,8 @@ export class MainView extends LitElement { this._openaiCompatibleModel ); this.requestUpdate(); + // Auto-load models when both key and URL are set + this._debouncedLoadModels(); } async _saveOpenAICompatibleModel(val) { @@ -682,6 +698,79 @@ export class MainView extends LitElement { this._responseProvider = val; await cheatingDaddy.storage.updatePreference('responseProvider', val); this.requestUpdate(); + + // Auto-load models when switching to openai-compatible + if (val === 'openai-compatible' && this._openaiCompatibleBaseUrl) { + this._loadModels(); + } + } + + async _loadModels() { + if (this._responseProvider !== 'openai-compatible' || !this._openaiCompatibleBaseUrl) { + return; + } + + this._loadingModels = true; + this._availableModels = []; + this.requestUpdate(); + + try { + let modelsUrl = this._openaiCompatibleBaseUrl.trim(); + modelsUrl = modelsUrl.replace(/\/$/, ''); + if (!modelsUrl.includes('/models')) { + modelsUrl = modelsUrl.includes('/v1') ? modelsUrl + '/models' : modelsUrl + '/v1/models'; + } + + console.log('Loading models from:', modelsUrl); + + const headers = { + 'Content-Type': 'application/json' + }; + + if (this._openaiCompatibleApiKey) { + headers['Authorization'] = `Bearer ${this._openaiCompatibleApiKey}`; + } + + const response = await fetch(modelsUrl, { headers }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const data = await response.json(); + + if (data.data && Array.isArray(data.data)) { + this._availableModels = data.data.map(m => m.id || m.model || m.name).filter(Boolean); + } else if (Array.isArray(data)) { + this._availableModels = data.map(m => m.id || m.model || m.name || m).filter(Boolean); + } + + console.log('Loaded models:', this._availableModels.length); + + if (this._availableModels.length > 0 && !this._availableModels.includes(this._openaiCompatibleModel)) { + await this._saveOpenAICompatibleModel(this._availableModels[0]); + } + } catch (error) { + console.log('Could not load models:', error.message); + this._availableModels = []; + } finally { + this._loadingModels = false; + this.requestUpdate(); + } + } + + _debouncedLoadModels() { + if (this._loadModelsTimeout) { + clearTimeout(this._loadModelsTimeout); + } + this._loadModelsTimeout = setTimeout(() => { + this._loadModels(); + }, 500); + } + + _toggleManualInput() { + this._manualModelInput = !this._manualModelInput; + this.requestUpdate(); } async _saveOllamaHost(val) { @@ -824,15 +913,57 @@ export class MainView extends LitElement { .value=${this._openaiCompatibleBaseUrl} @input=${e => this._saveOpenAICompatibleBaseUrl(e.target.value)} /> - this._saveOpenAICompatibleModel(e.target.value)} - /> + ${this._loadingModels ? html` + + ` : this._availableModels.length > 0 && !this._manualModelInput ? html` +