Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions src/main/presenter/llmProviderPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1630,44 +1630,43 @@ export class LLMProviderPresenter implements ILlmProviderPresenter {
}

// 获取 OllamaProvider 实例
getOllamaProviderInstance(): OllamaProvider | null {
// 从所有 provider 中找到已经启用的 ollama provider
for (const provider of this.providers.values()) {
if (provider.id === 'ollama' && provider.enable) {
const providerInstance = this.providerInstances.get(provider.id)
if (providerInstance instanceof OllamaProvider) {
return providerInstance
}
getOllamaProviderInstance(providerId: string): OllamaProvider | null {
try {
const instance = this.getProviderInstance(providerId)
if (instance instanceof OllamaProvider) {
return instance
}
console.warn(`Provider ${providerId} is not an Ollama provider instance`)
return null
} catch (error) {
console.warn(`Failed to get Ollama provider instance for ${providerId}:`, error)
return null
}
return null
}
// ollama api
listOllamaModels(): Promise<OllamaModel[]> {
const provider = this.getOllamaProviderInstance()
listOllamaModels(providerId: string): Promise<OllamaModel[]> {
const provider = this.getOllamaProviderInstance(providerId)
if (!provider) {
// console.error('Ollama provider not found')
return Promise.resolve([])
}
return provider.listModels()
}
showOllamaModelInfo(modelName: string): Promise<ShowResponse> {
const provider = this.getOllamaProviderInstance()
showOllamaModelInfo(providerId: string, modelName: string): Promise<ShowResponse> {
const provider = this.getOllamaProviderInstance(providerId)
if (!provider) {
throw new Error('Ollama provider not found')
}
return provider.showModelInfo(modelName)
}
listOllamaRunningModels(): Promise<OllamaModel[]> {
const provider = this.getOllamaProviderInstance()
listOllamaRunningModels(providerId: string): Promise<OllamaModel[]> {
const provider = this.getOllamaProviderInstance(providerId)
if (!provider) {
// console.error('Ollama provider not found')
return Promise.resolve([])
}
return provider.listRunningModels()
}
pullOllamaModels(modelName: string): Promise<boolean> {
const provider = this.getOllamaProviderInstance()
pullOllamaModels(providerId: string, modelName: string): Promise<boolean> {
const provider = this.getOllamaProviderInstance(providerId)
if (!provider) {
throw new Error('Ollama provider not found')
}
Expand All @@ -1679,6 +1678,7 @@ export class LLMProviderPresenter implements ILlmProviderPresenter {
// })
eventBus.sendToRenderer(OLLAMA_EVENTS.PULL_MODEL_PROGRESS, SendTarget.ALL_WINDOWS, {
eventId: 'pullOllamaModels',
providerId,
modelName: modelName,
...progress
})
Expand Down
20 changes: 17 additions & 3 deletions src/renderer/settings/components/AddCustomProviderDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
<SelectItem value="openai-responses">OpenAI Responses</SelectItem>
<SelectItem value="gemini">Gemini</SelectItem>
<SelectItem value="anthropic">Anthropic</SelectItem>
<!-- <SelectItem value="ollama">Ollama</SelectItem>
<SelectItem value="groq">Groq</SelectItem>
<SelectItem value="ollama">Ollama</SelectItem>
<!-- <SelectItem value="groq">Groq</SelectItem>
<SelectItem value="mistral">Mistral AI</SelectItem>
<SelectItem value="cohere">Cohere</SelectItem>
<SelectItem value="zhinao">智脑</SelectItem>
Expand All @@ -54,7 +54,7 @@
v-model="formData.apiKey"
class="col-span-3"
:placeholder="t('settings.provider.dialog.addCustomProvider.apiKeyPlaceholder')"
required
:required="formData.apiType !== 'ollama'"
/>
</div>
<div class="grid grid-cols-4 items-center gap-4">
Expand Down Expand Up @@ -175,6 +175,20 @@ watch(
}
)

watch(
() => formData.value.apiType,
(newType, oldType) => {
if (newType === 'ollama') {
if (!formData.value.baseUrl) {
formData.value.baseUrl = 'http://localhost:11434'
}
formData.value.apiKey = ''
} else if (oldType === 'ollama' && formData.value.baseUrl === 'http://localhost:11434') {
formData.value.baseUrl = ''
}
}
)

const onOpenChange = (open: boolean) => {
isOpen.value = open
if (!open) {
Expand Down
53 changes: 47 additions & 6 deletions src/renderer/settings/components/OllamaProviderSettingsDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@
<div class="flex flex-col items-start p-2 gap-2">
<div class="flex justify-between items-center w-full">
<Label :for="`${provider.id}-url`" class="flex-1 cursor-pointer">API URL</Label>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Localize labels

“API URL” and “API Key” labels are hardcoded. Replace with i18n keys.

- <Label ...>API URL</Label>
+ <Label ...>{{ t('settings.provider.apiUrl') }}</Label>

- <Label ...>API Key</Label>
+ <Label ...>{{ t('settings.provider.apiKey') }}</Label>

As per coding guidelines.

Also applies to: 35-35

🤖 Prompt for AI Agents
In src/renderer/settings/components/OllamaProviderSettingsDetail.vue around
lines 6 and 35, the labels "API URL" and "API Key" are hardcoded — replace them
with i18n keys (e.g. settings.apiUrl, settings.apiKey) by using the project's
i18n call in the template (so the Label text comes from $t('...')/t('...') per
project convention) and add corresponding entries to the locale JSON/YAML files;
ensure keys follow existing naming conventions and run a quick lint/compile to
verify the translations resolve.

<Button
v-if="provider.custom"
variant="destructive"
size="sm"
class="text-xs rounded-lg"
@click="showDeleteProviderDialog = true"
>
<Icon icon="lucide:trash-2" class="w-4 h-4 mr-1" />
{{ t('settings.provider.delete') }}
</Button>
</div>
<Input
:id="`${provider.id}-url`"
Expand Down Expand Up @@ -236,6 +246,25 @@
</DialogFooter>
</DialogContent>
</Dialog>

<Dialog v-model:open="showDeleteProviderDialog">
<DialogContent>
<DialogHeader>
<DialogTitle>{{ t('settings.provider.dialog.deleteProvider.title') }}</DialogTitle>
<DialogDescription>
{{ t('settings.provider.dialog.deleteProvider.content', { name: provider.name }) }}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" @click="showDeleteProviderDialog = false">
{{ t('dialog.cancel') }}
</Button>
<Button variant="destructive" @click="confirmDeleteProvider">
{{ t('settings.provider.dialog.deleteProvider.confirm') }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</section>
</template>

Expand Down Expand Up @@ -275,11 +304,14 @@ const showApiKey = ref(false)
const showPullModelDialog = ref(false)
const showCheckModelDialog = ref(false)
const checkResult = ref<boolean>(false)
const showDeleteProviderDialog = ref(false)

// 模型列表 - 从 settings store 获取
const runningModels = computed(() => settingsStore.ollamaRunningModels)
const localModels = computed(() => settingsStore.ollamaLocalModels)
const pullingModels = computed(() => settingsStore.ollamaPullingModels)
const runningModels = computed(() => settingsStore.getOllamaRunningModels(props.provider.id))
const localModels = computed(() => settingsStore.getOllamaLocalModels(props.provider.id))
const pullingModels = computed(
() => new Map(Object.entries(settingsStore.getOllamaPullingModels(props.provider.id)))
)
const providerModelMetas = computed<RENDERER_MODEL_META[]>(() => {
const providerEntry = settingsStore.allProviderModels.find(
(item) => item.providerId === props.provider.id
Expand Down Expand Up @@ -812,14 +844,14 @@ onMounted(() => {

// 刷新模型列表 - 使用 settings store
const refreshModels = async () => {
await settingsStore.refreshOllamaModels()
await settingsStore.refreshOllamaModels(props.provider.id)
}

// 拉取模型 - 使用 settings store
const pullModel = async (modelName: string) => {
try {
// 开始拉取
const success = await settingsStore.pullOllamaModel(modelName)
const success = await settingsStore.pullOllamaModel(props.provider.id, modelName)

// 成功开始拉取后关闭对话框
if (success) {
Expand Down Expand Up @@ -858,7 +890,7 @@ const formatModelSize = (sizeInBytes: number): string => {

// 使用 settings store 的辅助函数
const isModelLocal = (modelName: string): boolean => {
return settingsStore.isOllamaModelLocal(modelName)
return settingsStore.isOllamaModelLocal(props.provider.id, modelName)
}

// API URL 处理
Expand Down Expand Up @@ -905,6 +937,15 @@ const openModelCheckDialog = () => {
modelCheckStore.openDialog(props.provider.id)
}

const confirmDeleteProvider = async () => {
try {
await settingsStore.removeProvider(props.provider.id)
showDeleteProviderDialog.value = false
} catch (error) {
console.error('Failed to delete provider:', error)
}
}

// 监听 provider 变化
watch(
() => props.provider,
Expand Down
53 changes: 38 additions & 15 deletions src/renderer/src/components/icons/ModelIcon.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useSettingsStore } from '@/stores/settings'
import cherryinColorIcon from '@/assets/llm-icons/cherryin-color.png?url'
import adobeColorIcon from '@/assets/llm-icons/adobe-color.svg?url'
import zeaburColorIcon from '@/assets/llm-icons/zeabur-color.svg?url'
Expand Down Expand Up @@ -151,6 +152,13 @@ const props = withDefaults(defineProps<Props>(), {
isDark: false
})

const settingsStore = useSettingsStore()

const provider = computed(() => {
if (!props.modelId) return undefined
return settingsStore.providers.find((item) => item.id === props.modelId)
})
Comment on lines +157 to +160
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the provider lookup logic.

The provider lookup matches item.id === props.modelId, which semantically suggests a modelId prop is being used to search for providers by their ID. This could indicate either:

  1. A naming inconsistency where modelId can actually hold provider IDs in some contexts
  2. A logic error where providers should be looked up differently

Please verify this is the intended behavior. If modelId can represent both model and provider IDs, consider renaming the prop to something more generic (e.g., itemId or identifier) or adding a comment explaining the dual usage.


🏁 Script executed:

#!/bin/bash
# Description: Check how ModelIcon component is used across the codebase to verify if modelId is passed provider IDs

# Search for ModelIcon usage patterns to see what values are passed to modelId prop
rg -nP --type=vue -A3 -B3 '<ModelIcon\s+.*?modelId'

# Also search for modelId prop bindings
rg -nP --type=vue -A2 'modelId\s*='

Length of output: 55


🏁 Script executed:

#!/bin/bash
# Description: Check how ModelIcon component is used and how modelId prop is set

# Search for ModelIcon usage patterns in .vue files
echo "=== ModelIcon Usage Patterns ==="
rg -n '<ModelIcon' -g '*.vue' -A3 -B1

echo -e "\n=== modelId Prop Bindings ==="
# Search for modelId prop assignments/bindings
rg -n 'modelId\s*=' -g '*.vue' -A2 -B1

echo -e "\n=== File containing ModelIcon.vue definition ==="
# Verify the ModelIcon.vue file location
fd -e vue -x grep -l 'ModelIcon' {}

Length of output: 9425


🏁 Script executed:

#!/bin/bash
# Description: Verify the structure of model objects passed to ModelIcon to understand if their .id is provider ID or model ID

# Search for selectedImageModel definition/assignment
echo "=== selectedImageModel definition ==="
rg -n 'selectedImageModel' -g '*.vue' -B2 -A2 | head -40

# Search for selectEmbeddingModel definition/assignment  
echo -e "\n=== selectEmbeddingModel definition ==="
rg -n 'selectEmbeddingModel' -g '*.vue' -B2 -A2 | head -40

# Check the settingsStore to understand provider structure
echo -e "\n=== settingsStore providers structure ==="
rg -n 'providers' -g '*.ts' -B3 -A3 | grep -A5 -B5 'interface.*Provider\|type.*Provider' | head -50

Length of output: 10211


🏁 Script executed:

#!/bin/bash
# Description: Find RENDERER_MODEL_META type definition

echo "=== RENDERER_MODEL_META type definition ==="
rg -n 'RENDERER_MODEL_META' -g '*.ts' -B2 -A8 | head -60

echo -e "\n=== MODEL_META type definition ==="
rg -n 'type MODEL_META\|interface MODEL_META' -g '*.ts' -B2 -A10 | head -60

Length of output: 4682


Pass provider ID instead of model ID to ModelIcon component.

The ModelIcon component searches the providers array by ID, but several components are incorrectly passing model IDs instead of provider IDs. This causes the provider lookup to fail.

Affected locations:

  • src/renderer/src/components/mcp-config/mcpServerForm.vue:879 — should pass selectedImageModelProvider instead of selectedImageModel?.id
  • src/renderer/settings/components/BuiltinKnowledgeSettings.vue:187, 227 — should pass selectEmbeddingModel?.providerId and selectRerankModel?.providerId
  • src/renderer/settings/components/common/SearchAssistantModelSection.vue:16 — should pass selectedSearchModel?.providerId

All model objects (RENDERER_MODEL_META) have a providerId field available for this lookup.


const iconKey = computed(() => {
const modelIdLower = props.modelId.toLowerCase()
const iconEntries = Object.keys(icons)
Expand All @@ -159,28 +167,43 @@ const iconKey = computed(() => {
const matchedIcon = iconEntries.find((key) => {
return modelIdLower.includes(key)
})
return matchedIcon ? matchedIcon : 'default'
if (matchedIcon) {
return matchedIcon
}

const apiType = provider.value?.apiType?.toLowerCase()
if (apiType) {
const apiMatchedIcon = iconEntries.find((key) => apiType.includes(key))
if (apiMatchedIcon) {
return apiMatchedIcon
}
}

return 'default'
})

const invert = computed(() => {
if (!props.isDark) {
return false
}
if (
props.modelId.toLowerCase() === 'openai' ||
props.modelId.toLowerCase().includes('openai-responses') ||
props.modelId.toLowerCase().includes('openrouter') ||
props.modelId.toLowerCase().includes('ollama') ||
props.modelId.toLowerCase().includes('grok') ||
props.modelId.toLowerCase().includes('groq') ||
props.modelId.toLowerCase().includes('github') ||
props.modelId.toLowerCase().includes('moonshot') ||
props.modelId.toLowerCase().includes('lmstudio') ||
props.modelId.toLowerCase().includes('aws-bedrock')
) {
return true
const checkTargets = [props.modelId.toLowerCase()]
if (provider.value?.apiType) {
checkTargets.push(provider.value.apiType.toLowerCase())
}
return false
const invertKeywords = [
'openai',
'openai-responses',
'openrouter',
'ollama',
'grok',
'groq',
'github',
'moonshot',
'lmstudio',
'aws-bedrock'
]

return checkTargets.some((target) => invertKeywords.some((keyword) => target.includes(keyword)))
})
</script>

Expand Down
Loading