-
-
Notifications
You must be signed in to change notification settings - Fork 213
Add Ollama AI support with local model detection and configuration #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1913,6 +1913,11 @@ function setupEventListeners() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Ollama detect models button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.getElementById('ollama-detect-btn').addEventListener('click', async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await detectOllamaModels(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.getElementById('settings-modal').addEventListener('click', (e) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (e.target.id === 'settings-modal') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.getElementById('settings-modal').classList.remove('visible'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2928,9 +2933,10 @@ async function aiTranslateAll() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Get selected provider and API key | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const provider = getSelectedProvider(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const providerConfig = llmProviders[provider]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = localStorage.getItem(providerConfig.storageKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = providerConfig.storageKey ? localStorage.getItem(providerConfig.storageKey) : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!apiKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Ollama doesn't need an API key, other providers do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!providerConfig.isLocal && !apiKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setTranslateStatus(`Add your LLM API key in Settings to use AI translation.`, 'error'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -2985,6 +2991,8 @@ Translate to these language codes: ${targetLangs.join(', ')}`; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| responseText = await translateWithOpenAI(apiKey, prompt); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (provider === 'google') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| responseText = await translateWithGoogle(apiKey, prompt); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (provider === 'ollama') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| responseText = await translateWithOllama(prompt); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Clean up response - remove markdown code blocks if present | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3011,7 +3019,14 @@ Translate to these language codes: ${targetLangs.join(', ')}`; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Translation error:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error.message === 'Failed to fetch') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setTranslateStatus('Connection failed. Check your API key in Settings.', 'error'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const provider = getSelectedProvider(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (provider === 'ollama') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setTranslateStatus('Connection failed. Is Ollama running? Check Settings.', 'error'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setTranslateStatus('Connection failed. Check your API key in Settings.', 'error'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (error.message === 'OLLAMA_MODEL_NOT_FOUND') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setTranslateStatus('Model not found. Pull it first with: ollama pull <model>', 'error'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (error.message === 'AI_UNAVAILABLE' || error.message.includes('401') || error.message.includes('403')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setTranslateStatus('Invalid API key. Update it in Settings (gear icon).', 'error'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3241,9 +3256,10 @@ async function translateAllText() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Get selected provider and API key | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const provider = getSelectedProvider(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const providerConfig = llmProviders[provider]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = localStorage.getItem(providerConfig.storageKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const apiKey = providerConfig.storageKey ? localStorage.getItem(providerConfig.storageKey) : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!apiKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Ollama doesn't need an API key, other providers do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!providerConfig.isLocal && !apiKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await showAppAlert('Add your LLM API key in Settings to use AI translation.', 'error'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3381,6 +3397,8 @@ Translate to these language codes: ${targetLangs.join(', ')}`; | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| responseText = await translateWithOpenAI(apiKey, prompt); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (provider === 'google') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| responseText = await translateWithGoogle(apiKey, prompt); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (provider === 'ollama') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| responseText = await translateWithOllama(prompt); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| updateStatus('Processing response...', 'Parsing translations'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -3534,6 +3552,32 @@ async function translateWithGoogle(apiKey, prompt) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return data.candidates[0].content.parts[0].text; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function translateWithOllama(prompt) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const model = getSelectedModel('ollama'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const baseUrl = getOllamaUrl(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await fetch(`${baseUrl}/api/chat`, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: "POST", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Content-Type": "application/json" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model: model, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messages: [{ role: "user", content: prompt }], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stream: false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!response.ok) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const status = response.status; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (status === 404) throw new Error('OLLAMA_MODEL_NOT_FOUND'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Ollama request failed: ${status}. Make sure Ollama is running.`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const data = await response.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const data = await response.json(); | |
| const data = await response.json(); | |
| if (!data || !data.message || typeof data.message.content !== 'string') { | |
| throw new Error('OLLAMA_INVALID_RESPONSE'); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Auto-detecting models when switching to Ollama could create a race condition. If a user quickly switches between providers or closes the settings modal before detection completes, the async detectOllamaModels() call could update UI elements that no longer exist or are in an unexpected state. Consider checking if the modal is still open and the Ollama section is still visible before updating the UI in the detectOllamaModels function.
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The server URL input lacks validation. Users could enter invalid URLs (e.g., 'not-a-url', 'javascript:alert(1)') which could cause unexpected behavior or errors when constructing API endpoints. Consider validating that the URL is properly formatted (starts with http:// or https://) before attempting to use it, or sanitizing the input.
| const baseUrl = urlInput.value.trim() || llmProviders.ollama.defaultUrl; | |
| const rawUrl = urlInput.value.trim(); | |
| let baseUrl = rawUrl || llmProviders.ollama.defaultUrl; | |
| // Validate user-provided URL (if any) to ensure it is a proper http(s) URL | |
| if (rawUrl) { | |
| try { | |
| const parsed = new URL(baseUrl); | |
| if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { | |
| throw new Error('Invalid protocol'); | |
| } | |
| // Normalize to origin so we always have a clean base URL | |
| baseUrl = parsed.origin; | |
| } catch (e) { | |
| status.textContent = 'Invalid Ollama URL. Please use a valid http:// or https:// address.'; | |
| status.className = 'settings-key-status error'; | |
| btn.disabled = false; | |
| btn.innerHTML = ` | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M23 4v6h-6M1 20v-6h6"/> | |
| <path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/> | |
| </svg> | |
| Detect | |
| `; | |
| return; | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer that
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a potential XSS vulnerability here. Model names returned from the Ollama API are directly interpolated into HTML without escaping. If a malicious model name contains HTML/JavaScript code, it could be executed. Consider escaping the model name before inserting it into the HTML, or use textContent instead of innerHTML to populate the options.
| // Populate dropdown | |
| select.innerHTML = models.map(model => { | |
| const name = model.name; | |
| const size = model.size ? ` (${formatBytes(model.size)})` : ''; | |
| const selected = name === savedModel ? ' selected' : ''; | |
| return `<option value="${name}"${selected}>${name}${size}</option>`; | |
| }).join(''); | |
| // Populate dropdown without using innerHTML to avoid XSS | |
| // Clear existing options | |
| select.innerHTML = ''; | |
| models.forEach(model => { | |
| const name = model.name; | |
| const size = model.size ? ` (${formatBytes(model.size)})` : ''; | |
| const option = document.createElement('option'); | |
| option.value = name; | |
| option.textContent = `${name}${size}`; | |
| if (name === savedModel) { | |
| option.selected = true; | |
| } | |
| select.appendChild(option); | |
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty low probability, that this would ever happen. We should do this nontheless.
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The formatBytes function doesn't handle the case when the calculated index i is greater than the sizes array length. For very large byte values (e.g., terabytes), Math.floor(Math.log(bytes) / Math.log(k)) could return 4 or higher, but the sizes array only has 4 elements (indices 0-3). This would result in "undefined" being appended to the formatted string. Consider adding 'TB', 'PB' to the sizes array or clamping the index to the array length.
| const sizes = ['B', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; | |
| const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thats not realistic in this case
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When no models are found or connection fails, the dropdown is populated with a placeholder option with an empty value. If a user then tries to save settings without detecting models, an empty string could be saved as the model name. This may cause issues when attempting to use the API. Consider either requiring a valid model selection before allowing save, or showing a validation error if the model is empty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer requiring valid model selection
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The server URL input lacks validation. Invalid URLs are saved to localStorage without verification. Consider validating that the URL is properly formatted (starts with http:// or https://) before saving it to localStorage.
| if (urlInput) { | |
| const url = urlInput.value.trim() || config.defaultUrl; | |
| localStorage.setItem('ollamaUrl', url); | |
| } | |
| if (modelInput) { | |
| const model = modelInput.value.trim() || config.defaultModel; | |
| localStorage.setItem(config.modelStorageKey, model); | |
| } | |
| if (status) { | |
| let localSettingsValid = true; | |
| if (urlInput) { | |
| const url = urlInput.value.trim() || config.defaultUrl; | |
| const hasValidScheme = /^https?:\/\//i.test(url); | |
| if (!hasValidScheme) { | |
| if (status) { | |
| status.textContent = 'Invalid URL. Must start with http:// or https://'; | |
| status.className = 'settings-key-status error'; | |
| } | |
| if (provider === selectedProvider) { | |
| allValid = false; | |
| } | |
| localSettingsValid = false; | |
| } else { | |
| localStorage.setItem('ollamaUrl', url); | |
| } | |
| } | |
| if (modelInput) { | |
| const model = modelInput.value.trim() || config.defaultModel; | |
| localStorage.setItem(config.modelStorageKey, model); | |
| } | |
| if (status && localSettingsValid) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same thing, URLs should be validated to give the user feedback
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -1085,6 +1085,10 @@ <h4 class="settings-section-title"> | |||||
| <input type="radio" name="ai-provider" value="google"> | ||||||
| <span class="provider-label">Google (Gemini)</span> | ||||||
| </label> | ||||||
| <label class="settings-provider-option"> | ||||||
| <input type="radio" name="ai-provider" value="ollama"> | ||||||
| <span class="provider-label">Ollama (Local)</span> | ||||||
| </label> | ||||||
| </div> | ||||||
| </div> | ||||||
|
|
||||||
|
|
@@ -1181,6 +1185,43 @@ <h4 class="settings-section-title"> | |||||
| </a> | ||||||
| </div> | ||||||
|
|
||||||
| <div class="settings-section settings-api-section" id="settings-ollama" data-provider="ollama" style="display: none;"> | ||||||
| <h4 class="settings-section-title"> | ||||||
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||||||
| <rect x="2" y="3" width="20" height="14" rx="2" ry="2"/> | ||||||
| <path d="M8 21h8M12 17v4"/> | ||||||
| </svg> | ||||||
| Ollama Configuration | ||||||
| </h4> | ||||||
| <p class="settings-description" style="margin-bottom: 12px;">Run AI models locally with Ollama. No API key required.</p> | ||||||
| <div class="settings-model-group" style="margin-bottom: 12px;"> | ||||||
| <label class="settings-model-label">Server URL</label> | ||||||
|
||||||
| <label class="settings-model-label">Server URL</label> | |
| <label for="settings-ollama-url" class="settings-model-label">Server URL</label> |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Model select element lacks an accessible label association. While there is a visible label element, it should be associated with the select using the 'for' attribute matching the select's id, or the select should have an 'aria-label' attribute. This would improve accessibility for screen reader users.
| <label class="settings-model-label">Model</label> | |
| <label for="settings-model-ollama" class="settings-model-label">Model</label> |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -37,12 +37,23 @@ const llmProviders = { | |||||||||||
| { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro ($$$)' } | ||||||||||||
| ], | ||||||||||||
| defaultModel: 'gemini-2.5-flash' | ||||||||||||
| }, | ||||||||||||
| ollama: { | ||||||||||||
| name: 'Ollama (Local)', | ||||||||||||
| keyPrefix: null, // No API key required | ||||||||||||
| storageKey: null, // No API key storage | ||||||||||||
| modelStorageKey: 'ollamaModel', | ||||||||||||
| urlStorageKey: 'ollamaUrl', | ||||||||||||
| defaultUrl: 'http://localhost:11434', | ||||||||||||
| models: [], // User specifies their own model | ||||||||||||
| defaultModel: 'llama3.2', | ||||||||||||
| isLocal: true | ||||||||||||
| } | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Get the selected model for a provider | ||||||||||||
| * @param {string} provider - Provider key (anthropic, openai, google) | ||||||||||||
| * @param {string} provider - Provider key (anthropic, openai, google, ollama) | ||||||||||||
| * @returns {string} - Model ID | ||||||||||||
| */ | ||||||||||||
| function getSelectedModel(provider) { | ||||||||||||
|
|
@@ -51,6 +62,39 @@ function getSelectedModel(provider) { | |||||||||||
| return localStorage.getItem(config.modelStorageKey) || config.defaultModel; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Get the Ollama base URL | ||||||||||||
| * @returns {string} - Ollama URL | ||||||||||||
| */ | ||||||||||||
| function getOllamaUrl() { | ||||||||||||
| return localStorage.getItem('ollamaUrl') || llmProviders.ollama.defaultUrl; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Fetch available models from Ollama | ||||||||||||
| * @param {string} baseUrl - Ollama server URL (optional, uses saved URL if not provided) | ||||||||||||
| * @returns {Promise<Array>} - Array of model objects with name and size | ||||||||||||
| */ | ||||||||||||
| async function fetchOllamaModels(baseUrl = null) { | ||||||||||||
| const url = baseUrl || getOllamaUrl(); | ||||||||||||
| try { | ||||||||||||
| const response = await fetch(`${url}/api/tags`, { | ||||||||||||
|
Comment on lines
+80
to
+81
|
||||||||||||
| try { | |
| const response = await fetch(`${url}/api/tags`, { | |
| const normalizedUrl = url.replace(/\/+$/, ''); | |
| try { | |
| const response = await fetch(`${normalizedUrl}/api/tags`, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The URL concatenation doesn't handle trailing slashes properly. If the user enters a URL with a trailing slash (e.g., 'http://localhost:11434/'), the resulting URL will be 'http://localhost:11434//api/chat' with a double slash. While this typically works, it's not ideal. Consider normalizing the URL by removing trailing slashes before concatenation.