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
37 changes: 29 additions & 8 deletions src/popup/sections/GeneralPart.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next'
import { useLayoutEffect, useRef, useState } from 'react'
import FileSaver from 'file-saver'
import { isApiModeSelected, getApiModesFromConfig } from '../../utils/index.mjs'
import { getApiModesFromConfig, modelNameToDesc } from '../../utils/index.mjs'
import {
isUsingAzureOpenAiApiModel,
isUsingClaudeApiModel,
Expand All @@ -21,7 +21,11 @@ import { config as menuConfig } from '../../content-script/menu-tools'
import { PencilIcon } from '@primer/octicons-react'
import { importDataIntoStorage } from './import-data-cleanup.mjs'
import { resolveOpenAICompatibleRequest } from '../../services/apis/provider-registry.mjs'
import { getApiModeDisplayLabel } from './api-modes-provider-utils.mjs'
import {
getApiModeDisplayLabel,
getSelectedApiModeOptionValue,
UNMATCHED_API_MODE_VALUE,
} from './api-modes-provider-utils.mjs'
import {
buildProviderOverrideFinalConfigUpdate,
createProviderApiKeyDraftSelectionSignature,
Expand Down Expand Up @@ -107,6 +111,14 @@ export function GeneralPart({
config.apiMode && typeof config.apiMode === 'object'
? { apiMode: config.apiMode }
: { modelName: config.modelName }
const selectedApiModeValue = getSelectedApiModeOptionValue(apiModes, config)
Comment thread
PeterDaveHello marked this conversation as resolved.
Comment thread
PeterDaveHello marked this conversation as resolved.
const currentApiModeLabel = config.apiMode
? getApiModeDisplayLabel(
config.apiMode,
t,
Array.isArray(config.customOpenAIProviders) ? config.customOpenAIProviders : [],
) || t(Models.customModel.desc)
: modelNameToDesc(config.modelName, t, config.customModelName)
const selectedProviderRequest = resolveOpenAICompatibleRequest(config, selectedProviderSession)
const selectedProviderId = selectedProviderRequest?.providerId || ''
const selectedProviderSecretTargetId = resolveProviderSecretTargetId(selectedProviderRequest)
Expand Down Expand Up @@ -480,15 +492,26 @@ export function GeneralPart({
: undefined
}
required
value={selectedApiModeValue}
onChange={(e) => {
if (e.target.value === UNMATCHED_API_MODE_VALUE) return
if (e.target.value === '-1') {
updateConfig({ modelName: 'customModel', apiMode: null })
return
}
const apiMode = apiModes[e.target.value]
updateConfig({ apiMode: apiMode })
const apiMode = apiModes[Number(e.target.value)]
if (!apiMode) {
e.currentTarget.value = selectedApiModeValue
return
}
updateConfig({ apiMode })
Comment thread
PeterDaveHello marked this conversation as resolved.
}}
>
{selectedApiModeValue === UNMATCHED_API_MODE_VALUE && (
<option value={UNMATCHED_API_MODE_VALUE} disabled>
{currentApiModeLabel}
</option>
)}
{apiModes.map((apiMode, index) => {
const desc = getApiModeDisplayLabel(
apiMode,
Expand All @@ -497,15 +520,13 @@ export function GeneralPart({
)
if (desc) {
return (
<option value={index} key={index} selected={isApiModeSelected(apiMode, config)}>
<option value={index} key={index}>
{desc}
</option>
)
}
})}
<option value={-1} selected={!config.apiMode && config.modelName === 'customModel'}>
{t(Models.customModel.desc)}
</option>
<option value={-1}>{t(Models.customModel.desc)}</option>
</select>
{isUsingMultiModeModel(config) && (
<select
Expand Down
15 changes: 14 additions & 1 deletion src/popup/sections/api-modes-provider-utils.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import { AlwaysCustomGroups } from '../../config/index.mjs'
import { apiModeToModelName, modelNameToDesc } from '../../utils/model-name-convert.mjs'
import {
apiModeToModelName,
getUniquelySelectedApiModeIndex,
modelNameToDesc,
} from '../../utils/model-name-convert.mjs'

export const UNMATCHED_API_MODE_VALUE = '__current-api-mode__'

export function getSelectedApiModeOptionValue(apiModes, configOrSession) {
const selectedIndex = getUniquelySelectedApiModeIndex(apiModes, configOrSession)
if (selectedIndex !== -1) return String(selectedIndex)
if (!configOrSession?.apiMode && configOrSession?.modelName === 'customModel') return '-1'
return UNMATCHED_API_MODE_VALUE
}

function normalizeText(value) {
return String(value || '').trim()
Expand Down
99 changes: 99 additions & 0 deletions tests/unit/popup/api-modes-provider-utils.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
createProviderId,
getConversationAiName,
getApiModeDisplayLabel,
getSelectedApiModeOptionValue,
getConfiguredCustomApiModesForSessionRecovery,
getProviderDeleteDisabledReasonKey,
isProviderEndpointRewriteBlockedBySavedConversations,
Expand All @@ -29,9 +30,94 @@ import {
shouldIncludeSelectedApiModeInReferenceCheck,
shouldPersistDeletedProviderChanges,
shouldPersistPendingProviderChanges,
UNMATCHED_API_MODE_VALUE,
validateProviderEndpointDraft,
} from '../../../src/popup/sections/api-modes-provider-utils.mjs'

test('getSelectedApiModeOptionValue returns the selected active mode index', () => {
const apiModes = [
{
groupName: 'claudeWebModelKeys',
itemName: 'claude2WebFree',
active: true,
},
{
groupName: 'openRouterApiModelKeys',
itemName: 'openRouter_auto',
active: true,
},
]

assert.equal(getSelectedApiModeOptionValue(apiModes, { apiMode: { ...apiModes[1] } }), '1')
})

test('getSelectedApiModeOptionValue preserves an unmatched modelName', () => {
const apiModes = [
{
groupName: 'claudeWebModelKeys',
itemName: 'claude2WebFree',
active: true,
},
]

assert.equal(
getSelectedApiModeOptionValue(apiModes, { modelName: 'chatgptFree35', apiMode: null }),
UNMATCHED_API_MODE_VALUE,
)
})

test('getSelectedApiModeOptionValue preserves an unmatched apiMode', () => {
const apiModes = [
{
groupName: 'openRouterApiModelKeys',
itemName: 'openRouter_auto',
active: true,
},
]
const selectedApiMode = {
groupName: 'openRouterApiModelKeys',
itemName: 'openRouter_openai_gpt_5_5',
active: true,
}

assert.equal(
getSelectedApiModeOptionValue(apiModes, { apiMode: selectedApiMode }),
UNMATCHED_API_MODE_VALUE,
)
})

test('getSelectedApiModeOptionValue preserves a selection when no modes are active', () => {
const selectedApiMode = {
groupName: 'openRouterApiModelKeys',
itemName: 'openRouter_openai_gpt_5_5',
active: true,
}

assert.equal(
getSelectedApiModeOptionValue([], { apiMode: selectedApiMode }),
UNMATCHED_API_MODE_VALUE,
)
})

test('getSelectedApiModeOptionValue preserves an ambiguous selection', () => {
const duplicateApiMode = {
groupName: 'openRouterApiModelKeys',
itemName: 'openRouter_auto',
active: true,
}

assert.equal(
getSelectedApiModeOptionValue([{ ...duplicateApiMode }, { ...duplicateApiMode }], {
apiMode: duplicateApiMode,
}),
UNMATCHED_API_MODE_VALUE,
)
})

test('getSelectedApiModeOptionValue keeps the custom model option selected', () => {
assert.equal(getSelectedApiModeOptionValue([], { modelName: 'customModel', apiMode: null }), '-1')
})

test('createProviderId avoids reserved and existing ids', () => {
const existingProviders = [{ id: 'foo' }, { id: 'foo-2' }]
const reservedProviderIds = ['openai', 'deepseek']
Expand Down Expand Up @@ -1977,6 +2063,19 @@ test('getApiModeDisplayLabel falls back for legacy custom provider', () => {
assert.equal(getApiModeDisplayLabel(apiMode, t, []), 'Custom Model (deepseek-v3.2)')
})

test('getApiModeDisplayLabel falls back when a custom provider is missing', () => {
const apiMode = {
groupName: 'customApiModelKeys',
itemName: 'customModel',
isCustom: true,
customName: 'deepseek-v3.2',
providerId: 'missing-provider',
}
const t = (value) => value

assert.equal(getApiModeDisplayLabel(apiMode, t, []), 'Custom Model (deepseek-v3.2)')
})

test('removePendingProviderDeletion removes a recreated provider id from pending deletions', () => {
assert.deepEqual(removePendingProviderDeletion(['provider-a', 'provider-b'], ' provider-b '), [
'provider-a',
Expand Down