From 382fc8c2345545eebfb5ba23462975b1310bb90b Mon Sep 17 00:00:00 2001 From: Yishen Tu Date: Wed, 4 Mar 2026 01:29:55 +0800 Subject: [PATCH] feat: remove custom model settings Remove custom model selection from options UI and settings schema. Simplify gemini interaction and validation logic to use the default gemini-2.0-flash-exp model only. Update tests to reflect the removal of model configuration. --- src/background/features/gemini/gemini.ts | 37 ++--- .../features/gemini/gemini/interaction.ts | 9 +- .../features/gemini/gemini/title.ts | 2 +- .../features/composer/input-toolbar.ts | 24 ---- .../conversation/conversation-flow.ts | 3 + .../features/messages/message-renderer.ts | 4 + src/chatpanel/template/composer-template.ts | 6 +- src/options/dom.ts | 12 +- src/options/form-state.ts | 124 +++-------------- src/options/options.html | 35 ++--- src/options/options.ts | 23 +--- src/options/validation.ts | 4 +- src/shared/messages.ts | 2 + src/shared/settings.ts | 28 ++-- src/shared/tool-validation.ts | 43 +----- .../contract/gemini-request.contract.test.ts | 24 ++-- .../background/features/gemini/gemini.test.ts | 49 +++---- .../features/composer/input-toolbar.test.ts | 108 ++------------- .../conversation/conversation-flow.test.ts | 3 + .../messages/message-renderer.test.ts | 26 ++++ .../unit/chatpanel/template/template.test.ts | 21 +++ tests/unit/options/dom.test.ts | 14 +- tests/unit/options/form-state.test.ts | 110 +++------------ tests/unit/options/options.test.ts | 129 +++--------------- tests/unit/options/validation.test.ts | 15 +- tests/unit/shared/settings.test.ts | 30 +++- 26 files changed, 258 insertions(+), 627 deletions(-) diff --git a/src/background/features/gemini/gemini.ts b/src/background/features/gemini/gemini.ts index da8093d..1021fc0 100644 --- a/src/background/features/gemini/gemini.ts +++ b/src/background/features/gemini/gemini.ts @@ -2,7 +2,6 @@ import type { FileDataAttachmentPayload } from '../../../shared/runtime'; import type { GeminiSettings } from '../../../shared/settings'; import { isRecord, toErrorMessage } from '../../core/utils'; import type { ChatSession, GeminiContent } from '../session/types'; -import { getGeminiClient } from './gemini-client'; import { composeGeminiInteractionRequest } from './gemini-request'; import { normalizeContent } from './gemini/content-normalize'; import { @@ -10,7 +9,7 @@ import { renderContentForChat, renderThinkingSummaryForChat, } from './gemini/content-render'; -import type { GeminiStreamDelta, SDKCreateInteractionRequest } from './gemini/contracts'; +import type { GeminiStreamDelta } from './gemini/contracts'; import { InvalidPreviousInteractionIdError, isInvalidPreviousInteractionIdError, @@ -202,7 +201,7 @@ async function callGeminiInteractionWithFunctionResultRetry(input: { }): Promise>> { try { return await callGeminiInteractionWithOptionalStreaming({ - settings: input.settings, + apiKey: input.settings.apiKey, request: input.requestPlan.request, ...(input.onStreamDelta ? { onStreamDelta: input.onStreamDelta } : {}), }); @@ -229,7 +228,7 @@ async function callGeminiInteractionWithFunctionResultRetry(input: { }); return callGeminiInteractionWithOptionalStreaming({ - settings: input.settings, + apiKey: input.settings.apiKey, request: retryRequestPlan.request, ...(input.onStreamDelta ? { onStreamDelta: input.onStreamDelta } : {}), }); @@ -237,20 +236,20 @@ async function callGeminiInteractionWithFunctionResultRetry(input: { } async function callGeminiInteractionWithOptionalStreaming(input: { - settings: GeminiSettings; + apiKey: string; request: unknown; onStreamDelta?: (delta: GeminiStreamDelta) => void; }): Promise>> { if (input.onStreamDelta) { return callGeminiInteractionStream({ - settings: input.settings, + apiKey: input.apiKey, request: input.request, onStreamDelta: input.onStreamDelta, }); } return callGeminiInteraction({ - settings: input.settings, + apiKey: input.apiKey, request: input.request, }); } @@ -346,23 +345,17 @@ export async function generateSessionTitle( })), ]; - const client = getGeminiClient(normalizedApiKey); - const response = (await client.interactions.create({ - model: SESSION_TITLE_MODEL, - input, - store: false, - } as unknown as SDKCreateInteractionRequest)) as unknown; - - if (!isRecord(response)) { - throw new Error('Gemini title generation response payload was not a JSON object.'); - } + const interaction = await callGeminiInteraction({ + apiKey: normalizedApiKey, + request: { + model: SESSION_TITLE_MODEL, + input, + store: false, + }, + }); - const rawOutputs = Array.isArray(response.outputs) ? response.outputs : []; + const rawOutputs = interaction.outputs ?? []; for (const rawOutput of rawOutputs) { - if (!isRecord(rawOutput)) { - continue; - } - if (rawOutput.type !== 'text' || typeof rawOutput.text !== 'string') { continue; } diff --git a/src/background/features/gemini/gemini/interaction.ts b/src/background/features/gemini/gemini/interaction.ts index 759aa9a..491b2c5 100644 --- a/src/background/features/gemini/gemini/interaction.ts +++ b/src/background/features/gemini/gemini/interaction.ts @@ -1,4 +1,3 @@ -import type { GeminiSettings } from '../../../../shared/settings'; import { isRecord } from '../../../core/utils'; import { getGeminiClient } from '../gemini-client'; import { readStringField } from './common'; @@ -22,15 +21,15 @@ import { } from './streaming'; export async function callGeminiInteraction(input: { - settings: GeminiSettings; + apiKey: string; request: unknown; }): Promise { - const response = await createInteraction(input.settings.apiKey, input.request); + const response = await createInteraction(input.apiKey, input.request); return normalizeGeminiInteractionResponse(response); } export async function callGeminiInteractionStream(input: { - settings: GeminiSettings; + apiKey: string; request: unknown; onStreamDelta: (delta: GeminiStreamDelta) => void; }): Promise { @@ -39,7 +38,7 @@ export async function callGeminiInteractionStream(input: { stream: true, }; - const stream = await createInteraction(input.settings.apiKey, streamRequest); + const stream = await createInteraction(input.apiKey, streamRequest); if (!isAsyncIterable(stream)) { throw new Error('Gemini streaming response payload was not an async iterable.'); diff --git a/src/background/features/gemini/gemini/title.ts b/src/background/features/gemini/gemini/title.ts index 0c37b21..de7da2e 100644 --- a/src/background/features/gemini/gemini/title.ts +++ b/src/background/features/gemini/gemini/title.ts @@ -1,5 +1,5 @@ const MAX_SESSION_TITLE_LENGTH = 60; -export const SESSION_TITLE_MODEL = 'gemini-flash-lite-latest'; +export const SESSION_TITLE_MODEL = 'gemini-3.1-flash-lite-preview'; export function buildSessionTitlePrompt(firstUserQuery: string): string { const lines = [ diff --git a/src/chatpanel/features/composer/input-toolbar.ts b/src/chatpanel/features/composer/input-toolbar.ts index 4263f2e..fbfc0fc 100644 --- a/src/chatpanel/features/composer/input-toolbar.ts +++ b/src/chatpanel/features/composer/input-toolbar.ts @@ -1,7 +1,6 @@ import { DEFAULT_GEMINI_MODEL, GEMINI_SETTINGS_STORAGE_KEY, - getModelDisplayLabel, getModelThinkingLevels, normalizeGeminiSettings, } from '../../../shared/settings'; @@ -115,28 +114,6 @@ export function createInputToolbar(shadowRoot: ShadowRoot): InputToolbar { ); } - function applyCustomModels(customModels: string[]): void { - const existing = new Set(); - for (const item of Array.from(modelMenu.querySelectorAll('.dropup-item'))) { - if ((item as HTMLElement).dataset.custom) { - item.remove(); - } else { - existing.add(item.getAttribute('data-value') ?? ''); - } - } - for (const model of customModels) { - if (!existing.has(model)) { - const btn = document.createElement('button'); - btn.type = 'button'; - btn.className = 'dropup-item'; - btn.dataset.value = model; - btn.dataset.custom = '1'; - btn.textContent = getModelDisplayLabel(model); - modelMenu.appendChild(btn); - } - } - } - modelTrigger.addEventListener('click', (e) => { e.stopPropagation(); const wasOpen = modelMenuController.isOpen(); @@ -185,7 +162,6 @@ export function createInputToolbar(shadowRoot: ShadowRoot): InputToolbar { function applySettings(settingsValue: unknown, keepThinkingSelection = false): void { const settings = normalizeGeminiSettings(settingsValue); modelThinkingLevelMap = settings.modelThinkingLevelMap; - applyCustomModels(settings.customModels); const preferred = keepThinkingSelection ? thinkingTrigger.dataset.value : undefined; updateThinkingOptions(selectedModelValue(), preferred); } diff --git a/src/chatpanel/features/conversation/conversation-flow.ts b/src/chatpanel/features/conversation/conversation-flow.ts index 3733b92..59cdbdb 100644 --- a/src/chatpanel/features/conversation/conversation-flow.ts +++ b/src/chatpanel/features/conversation/conversation-flow.ts @@ -138,6 +138,7 @@ export function createConversationFlowController( id: draft.assistantMessageId, role: 'assistant', content: draft.text, + isStreaming: true, }; if (draft.thinkingSummary) { streamMessage.thinkingSummary = draft.thinkingSummary; @@ -297,6 +298,7 @@ export function createConversationFlowController( id: assistantPlaceholderId, role: 'assistant', content: '', + isStreaming: true, }); if (input.shouldResetComposerText) { @@ -377,6 +379,7 @@ export function createConversationFlowController( id: regenPlaceholderMessageId, role: 'assistant', content: '', + isStreaming: true, }); regenStreamRequestId = crypto.randomUUID(); diff --git a/src/chatpanel/features/messages/message-renderer.ts b/src/chatpanel/features/messages/message-renderer.ts index 5a5a3cc..56b1425 100644 --- a/src/chatpanel/features/messages/message-renderer.ts +++ b/src/chatpanel/features/messages/message-renderer.ts @@ -383,6 +383,10 @@ function createAssistantActionBar( return null; } + if (message.isStreaming) { + return null; + } + const hasRegenerateAction = !!message.interactionId?.trim() && !!options.onAssistantAction; const branchOptions = message.branchOptionInteractionIds ?? []; const hasBranchSwitchAction = branchOptions.length > 1 && !!options.onAssistantBranchSelect; diff --git a/src/chatpanel/template/composer-template.ts b/src/chatpanel/template/composer-template.ts index b54af2c..e70b22a 100644 --- a/src/chatpanel/template/composer-template.ts +++ b/src/chatpanel/template/composer-template.ts @@ -1,5 +1,5 @@ import { - BUILTIN_GEMINI_MODEL_CATALOG, + type BuiltinGeminiModelKey, type ThinkingLevel, getBuiltinGeminiModelByKey, } from '../../shared/settings'; @@ -10,9 +10,11 @@ const THINKING_LABELS: Record = { low: 'Low', minimal: 'Min', }; +const MODEL_MENU_ORDER: readonly BuiltinGeminiModelKey[] = ['pro', 'flash', 'flash-lite']; function renderModelMenuItems(selectedModel: string): string { - return BUILTIN_GEMINI_MODEL_CATALOG.map((entry) => { + return MODEL_MENU_ORDER.map((key) => { + const entry = getBuiltinGeminiModelByKey(key); const selected = entry.model === selectedModel ? ' aria-selected="true"' : ''; return ``; }).join(''); diff --git a/src/options/dom.ts b/src/options/dom.ts index 6ea12d4..5d46284 100644 --- a/src/options/dom.ts +++ b/src/options/dom.ts @@ -5,11 +5,10 @@ export interface OptionsDom { apiKeyInput: HTMLInputElement; modelFlashNameInput: HTMLInputElement; modelFlashThinkingLevelSelect: HTMLSelectElement; + modelFlashLiteNameInput: HTMLInputElement; + modelFlashLiteThinkingLevelSelect: HTMLSelectElement; modelProNameInput: HTMLInputElement; modelProThinkingLevelSelect: HTMLSelectElement; - customModelRowsContainer: HTMLElement; - addCustomModelButton: HTMLButtonElement; - customModelRowTemplate: HTMLTemplateElement; systemInstructionInput: HTMLTextAreaElement; storeInteractionsInput: HTMLInputElement; maxToolRoundTripsInput: HTMLInputElement; @@ -39,13 +38,14 @@ export function getOptionsDom(): OptionsDom { modelFlashThinkingLevelSelect: queryRequiredElement( '#model-thinking-level-flash', ), + modelFlashLiteNameInput: queryRequiredElement('#model-name-flash-lite'), + modelFlashLiteThinkingLevelSelect: queryRequiredElement( + '#model-thinking-level-flash-lite', + ), modelProNameInput: queryRequiredElement('#model-name-pro'), modelProThinkingLevelSelect: queryRequiredElement( '#model-thinking-level-pro', ), - customModelRowsContainer: queryRequiredElement('#custom-model-rows'), - addCustomModelButton: queryRequiredElement('#add-custom-model'), - customModelRowTemplate: queryRequiredElement('#custom-model-row-template'), systemInstructionInput: queryRequiredElement('#system-instruction'), storeInteractionsInput: queryRequiredElement('#store-interactions'), maxToolRoundTripsInput: queryRequiredElement('#max-tool-round-trips'), diff --git a/src/options/form-state.ts b/src/options/form-state.ts index 1b9bf94..ad47683 100644 --- a/src/options/form-state.ts +++ b/src/options/form-state.ts @@ -1,6 +1,4 @@ import { - CUSTOM_MODEL_THINKING_LEVELS, - DEFAULT_CUSTOM_MODEL_THINKING_LEVEL, type GeminiSettings, type ThinkingLevel, getBuiltinGeminiModelByKey, @@ -9,10 +7,8 @@ import { import type { OptionsDom } from './dom'; const FLASH_MODEL = getBuiltinGeminiModelByKey('flash'); +const FLASH_LITE_MODEL = getBuiltinGeminiModelByKey('flash-lite'); const PRO_MODEL = getBuiltinGeminiModelByKey('pro'); -const CUSTOM_MODEL_ROW_SELECTOR = '[data-custom-model-row]'; -const CUSTOM_MODEL_INPUT_SELECTOR = '[data-custom-model-input]'; -const CUSTOM_MODEL_THINKING_LEVEL_SELECTOR = '[data-custom-model-thinking-level]'; const THINKING_LEVEL_LABELS: Record = { minimal: 'Minimal (fastest)', low: 'Low', @@ -22,15 +18,14 @@ const THINKING_LEVEL_LABELS: Record = { export function initializeModelThinkingControls(dom: OptionsDom): void { dom.modelFlashNameInput.value = FLASH_MODEL.model; + dom.modelFlashLiteNameInput.value = FLASH_LITE_MODEL.model; dom.modelProNameInput.value = PRO_MODEL.model; replaceThinkingLevelOptions(dom.modelFlashThinkingLevelSelect, FLASH_MODEL.thinkingLevels); - replaceThinkingLevelOptions(dom.modelProThinkingLevelSelect, PRO_MODEL.thinkingLevels); - - const templateThinkingLevelSelect = queryRequiredInTemplate( - dom.customModelRowTemplate, - CUSTOM_MODEL_THINKING_LEVEL_SELECTOR, + replaceThinkingLevelOptions( + dom.modelFlashLiteThinkingLevelSelect, + FLASH_LITE_MODEL.thinkingLevels, ); - replaceThinkingLevelOptions(templateThinkingLevelSelect, CUSTOM_MODEL_THINKING_LEVELS); + replaceThinkingLevelOptions(dom.modelProThinkingLevelSelect, PRO_MODEL.thinkingLevels); } export function applySettingsToForm(dom: OptionsDom, settings: GeminiSettings): void { @@ -42,12 +37,16 @@ export function applySettingsToForm(dom: OptionsDom, settings: GeminiSettings): FLASH_MODEL.thinkingLevels, FLASH_MODEL.defaultThinkingLevel, ); + dom.modelFlashLiteThinkingLevelSelect.value = normalizeThinkingLevelForAllowed( + settings.modelThinkingLevelMap[FLASH_LITE_MODEL.model], + FLASH_LITE_MODEL.thinkingLevels, + FLASH_LITE_MODEL.defaultThinkingLevel, + ); dom.modelProThinkingLevelSelect.value = normalizeThinkingLevelForAllowed( settings.modelThinkingLevelMap[PRO_MODEL.model], PRO_MODEL.thinkingLevels, PRO_MODEL.defaultThinkingLevel, ); - replaceCustomModelRows(dom, settings.customModels, settings.modelThinkingLevelMap); dom.systemInstructionInput.value = settings.systemInstruction; dom.storeInteractionsInput.checked = settings.storeInteractions; @@ -78,38 +77,20 @@ export function readFormState(dom: OptionsDom): Partial { FLASH_MODEL.thinkingLevels, FLASH_MODEL.defaultThinkingLevel, ), + [FLASH_LITE_MODEL.model]: normalizeThinkingLevelForAllowed( + dom.modelFlashLiteThinkingLevelSelect.value, + FLASH_LITE_MODEL.thinkingLevels, + FLASH_LITE_MODEL.defaultThinkingLevel, + ), [PRO_MODEL.model]: normalizeThinkingLevelForAllowed( dom.modelProThinkingLevelSelect.value, PRO_MODEL.thinkingLevels, PRO_MODEL.defaultThinkingLevel, ), }; - const customModels: string[] = []; - const seenModels = new Set(); - - for (const row of listCustomModelRows(dom)) { - const modelInput = queryRequiredInRow(row, CUSTOM_MODEL_INPUT_SELECTOR); - const thinkingLevelInput = queryRequiredInRow( - row, - CUSTOM_MODEL_THINKING_LEVEL_SELECTOR, - ); - const model = modelInput.value.trim(); - if (!model || seenModels.has(model)) { - continue; - } - - seenModels.add(model); - customModels.push(model); - modelThinkingLevelMap[model] = normalizeThinkingLevelForAllowed( - thinkingLevelInput.value, - CUSTOM_MODEL_THINKING_LEVELS, - DEFAULT_CUSTOM_MODEL_THINKING_LEVEL, - ); - } return { apiKey: dom.apiKeyInput.value.trim(), - customModels, modelThinkingLevelMap, systemInstruction: dom.systemInstructionInput.value.trim(), storeInteractions: dom.storeInteractionsInput.checked, @@ -134,79 +115,6 @@ export function readFormState(dom: OptionsDom): Partial { }; } -export function addCustomModelRow( - dom: OptionsDom, - model = '', - thinkingLevel: string | undefined = DEFAULT_CUSTOM_MODEL_THINKING_LEVEL, -): HTMLElement { - const templateRoot = dom.customModelRowTemplate.content.firstElementChild as HTMLElement | null; - if (!templateRoot) { - throw new Error('Custom model row template must have a root element.'); - } - - const row = templateRoot.cloneNode(true) as HTMLElement; - const thinkingLevelInput = queryRequiredInRow( - row, - CUSTOM_MODEL_THINKING_LEVEL_SELECTOR, - ); - if (thinkingLevelInput.options.length === 0) { - replaceThinkingLevelOptions(thinkingLevelInput, CUSTOM_MODEL_THINKING_LEVELS); - } - - queryRequiredInRow(row, CUSTOM_MODEL_INPUT_SELECTOR).value = model; - thinkingLevelInput.value = normalizeThinkingLevelForAllowed( - thinkingLevel, - CUSTOM_MODEL_THINKING_LEVELS, - DEFAULT_CUSTOM_MODEL_THINKING_LEVEL, - ); - - dom.customModelRowsContainer.appendChild(row); - return row; -} - -export function removeCustomModelRow(row: HTMLElement): void { - row.remove(); -} - -function replaceCustomModelRows( - dom: OptionsDom, - customModels: readonly string[], - modelThinkingLevelMap: Record, -): void { - dom.customModelRowsContainer.replaceChildren(); - for (const model of customModels) { - addCustomModelRow(dom, model, modelThinkingLevelMap[model]); - } -} - -function listCustomModelRows(dom: OptionsDom): HTMLElement[] { - return Array.from( - dom.customModelRowsContainer.querySelectorAll(CUSTOM_MODEL_ROW_SELECTOR), - ); -} - -function queryRequiredInRow( - row: HTMLElement, - selector: string, -): TElement { - const element = row.querySelector(selector); - if (!element) { - throw new Error(`Custom model row is missing required node: ${selector}`); - } - return element; -} - -function queryRequiredInTemplate( - template: HTMLTemplateElement, - selector: string, -): TElement { - const element = template.content.querySelector(selector); - if (!element) { - throw new Error(`Custom model template is missing required node: ${selector}`); - } - return element; -} - function replaceThinkingLevelOptions( select: HTMLSelectElement, levels: readonly ThinkingLevel[], diff --git a/src/options/options.html b/src/options/options.html index 2c30881..64cd725 100644 --- a/src/options/options.html +++ b/src/options/options.html @@ -31,6 +31,14 @@

Provider
Model thinking defaults
+ + +
+
@@ -39,36 +47,13 @@

Provider

- -
- Custom models -
- -